$mw->Busy(-recurse => 1); my $dlg = SimpleExecDialog->new(-parent => $mw);
$dlg->raise;
$dlg->set_text("Working . . .");
my $cmd = "cdrecord -blank=fast -dev=0,0,0";
$dlg->exec_cmd($cmd);
if ($dlg->status eq 'cancel'){
$dlg->withdraw;
$mw->Unbusy;
} #### ############################################################################ # # # FILE NAME: SimpleExecDialog.pm # # # # DESCRIPTION: This module provides the means for a Perl/Tk application # to execute a system command in a subprocess while displaying a status # dialog that allows the user to cancel the request. The dialog is # modal, and so prevents the parent window from accepting user input. # # The calling application can retrieve the return code of the # system command as well as any output sent to stdout. # # The SimpleExecDialog is controlled in stages to allow multiple # commands to be executed while displaying only one dialog. A typical # program might do something like: # # $d = SimpleExecDialog->new(-parent => $mw, -text => 'Doing stuff'); # $d->raise; # $out1 = $d->exec_cmd('/bin/ls'); # $out2 = $d->exec_cmd('/bin/ls -a'); # $d->withdraw; # # SimpleExecDialog is based on the ExecuteCommand package from # "Mastering Perl/Tk", Lidie and Walsh, O'Reilly 2002. # # HISTORY: # # # ############################################################################## package SimpleExecDialog; require SimpleExecDialog; use strict; use warnings; use IO::Handle; use POSIX ":sys_wait_h"; our @ISA = qw(Exporter); our @EXPORT = qw( ); our @EXPORT_OK = qw( ); our $VERSION = 1.00; ############################################################################## # Function: new # Description: Creates a new SimpleExecDialog. Parameters are given # as key-value pairs. The only required parameter is -parent. # Parameters: # -text => Text to display in the dialog # -title => Title to display at the top of the dialog window # -parent => Parent Toplevel widget. The dialog will be transient # for this Toplevel. # Returns: none. ############################################################################## sub new { my $invokant = shift; my $class = ref($invokant) || $invokant; my $self = { -text => 'Please wait...', -title => 'Processing' }; bless($self, $class); while (@_) { my $opt = shift; if ($opt eq '-text') { $self->{-text} = shift; } elsif ($opt eq '-title') { $self->{-title} = shift; } elsif ($opt eq '-parent') { $self->{-parent} = shift; } else { die "unknown option: $opt"; } } die 'missing parameters' if not defined ($self->{-parent}); # Create Toplevel to act as the cancel dialog. $self->{-toplevel} = $self->{-parent}->Toplevel(-title => $self->{-title}); my $top_frame = $self->{-toplevel}->Frame(-relief => 'groove', -borderwidth => 1)->pack(-side => 'top', -expand => 1, -fill => 'both'); $self->{-textlabel} = $top_frame->Label(-text => $self->{-text}, -justify => 'left') ->pack(-expand => 1, -padx => 10, -pady => 10, -expand => 1, -fill => 'both'); my $bottom_frame = $self->{-toplevel}->Frame(-relief => 'groove', -borderwidth => 1)->pack(-side => 'top', -fill => 'x'); my $cancel = $bottom_frame->Button(-text => 'Cancel', -command => [ \&_cancel_cb, $self ])->pack(-pady => 10); $self->{-toplevel}->protocol('WM_DELETE_WINDOW', [ \&_cancel_cb, $self ]); # The dialog will stay hidden until SimpleExecDialog::raise is called. $self->{-toplevel}->resizable(0, 0); $self->{-toplevel}->transient( $self->{-parent} ); $self->{-toplevel}->withdraw; return $self; } ############################################################################## # Function: raise # Description: Pops up the dialog, typically before exec_cmd is called. # Parameters: none. # Returns: none. ############################################################################## sub raise { my ($self) = @_; # The tricky part of this routine is figuring out how to center the # dialog on its parent. There are routines in Perl/Tk to provide window # width and height, but these do not include the title bar and borders. # That means we'll have to figure those out for ourselves using # calls to geometry. First we'll call raise and update on the # Toplevel to finalize its size without actually displaying it to # the user. $self->{-toplevel}->raise; $self->{-toplevel}->update; # By observing the difference between the window's offset according to # geometry (which includes the window manager's decorations) and its root-y # position, we can figure the extra pixels taken up by the title bar # and borders. my ($wm_y) = ( $self->{-parent}->geometry =~ /([+-]\d+$)/ ); my $parent_x = $self->{-parent}->rootx; my $parent_y = $self->{-parent}->rooty; my $parent_width = $self->{-parent}->width; my $parent_height = $self->{-parent}->height; my $my_width = $self->{-toplevel}->width; my $my_height = $self->{-toplevel}->height; # Since both the dialog and its parent have the same height title bar, # these cancel each other out in our calculations. Therefore, only need # to do "normal" centering arithmetic, but offset the y value by the window # manager y (which includes the title bar) rather than the widget's y. my $my_x = $parent_x + int(($parent_width - $my_width) / 2); my $my_y = $wm_y + int(($parent_height - $my_height) / 2); $my_x = 0 if $my_x < 0; $my_y = 0 if $my_y < 0; # Set the new location for the dialog and finally display it. my $new_geo = ($my_x >= 0 ? '+' : '') . $my_x . ($my_y >= 0 ? '+' : '') . $my_y; $self->{-toplevel}->geometry($new_geo); $self->{-toplevel}->deiconify; $self->{-toplevel}->grab; } ############################################################################## # Function: exec_cmd # Description: Executes the given system command in a subprocess. The # command can be stopped before completion by the user clicking the # Cancel button. # Parameters: # $cmd - The command to execute. # Returns: The final status of the request - 'ok', 'cancel', or 'error'. ############################################################################## sub exec_cmd { my ($self, $cmd) = @_; die "Missing command in SimpleExecDialog::exec_cmd" if not defined $cmd; # Initialize all the status variables before execution. $self->{-command} = $cmd; $self->{-finish} = 0; $self->{-output} = ''; $self->{-status} = 'ok'; $self->{-retcode} = undef; # The command will execute in a subprocess with its output tied to a # handle we can read from in a fileevent callback. Any output can thus # be read asynchronously through the normal Tk event mechanisms. my $h = IO::Handle->new; die "IO::Handle->new failed." unless defined $h; $self->{-handle} = $h; $self->{-pid} = open $h, $self->{-command} . ' 2>&1 |'; if (not defined $self->{-pid}) { $self->{-status} = 'error'; $self->{-output} = "Could not start the requested command:\n" . "$self->{-command}"; $self->kill_cmd; $self->{-toplevel}->messageBox(-title => 'Error', -message => $self->{-output}, -type => 'OK', -bitmap => 'error'); return $self->{-status}; } $h->autoflush(1); $self->{-toplevel}->fileevent($h, 'readable' => [ \&_read_stdout, $self ]); # Keep the dialog up until finished with it. $self->{-toplevel}->waitVariable(\$self->{-finish}); $self->kill_cmd; return $self->{-status}; } ############################################################################## # Function: set_text # Description: Used to change the text displayed inside the dialog window. # Parameters: # $text - the new string to display # $resize - if defined, causes the dialog to resize and reposition # itself to accomodate the new label. # Returns: none. ############################################################################## sub set_text { my ($self, $text, $resize) = @_; $self->{-text} = $text; $self->{-textlabel}->configure(-text => $text); if (defined $resize) { $self->withdraw; $self->raise; } } ############################################################################## # Function: _read_stdout # Description: Callback used to read output from an active subprocess. It # collects output in the -output attribute until the process completes, # at which point it sets -finish (which exec_cmd is waiting on) so the # dialog can return control to the calling app. # Parameters: none. # Returns: none. ############################################################################## sub _read_stdout { my ($self) = @_; print "@_"; if ($self->{-finish}) { $self->kill_cmd; } else { my $h = $self->{-handle}; # Use sysread to prevent buffering from getting in the way. if ( sysread $h, $_, 4096 ) { $self->{-output} .= $_; } else { $self->{-finish} = 1; } } } ############################################################################## # Function: kill_cmd # Description: Cleans up a command that was executing, killing it if still # executing. The cleanup includes calling waitpid to collect the # return code if the process completed, turning off the fileevent, and # closing any file handle. # Parameters: none. # Returns: none. ############################################################################## sub kill_cmd { my ($self) = @_; $self->{-finish} = 1; my $h = $self->{-handle}; return unless defined $h; $self->{-toplevel}->fileevent($h, 'readable' => ''); if (defined $self->{-pid}) { # Check to see if the process is still running. If not, grab the # return code from the process. my $pid = waitpid($self->{-pid}, &WNOHANG); if ($pid != 0 && $self->{-status} eq 'ok') { $self->{-retcode} = ($? >> 8); } else { $self->{-retcode} = undef; } # FIXME # Note: To be very thorough, should actually kill off the child and # all its descendants. Since the current Perl installation does not # have Proc::ProcessTable, this is complicated, so we'll just kill the # child. kill 'TERM', $self->{-pid}; } close $h; $self->{-handle} = undef; } ############################################################################## # Function: _cancel_cb # Description: Called when the SimpleExecDialog is closed before the # command it was executing completes. # Parameters: none. # Returns: none. ############################################################################## sub _cancel_cb { my ($self) = @_; return if $self->{-finish} != 0; $self->{-status} = 'cancel'; $self->{-finish} = 1; } ############################################################################## # Function: withdraw # Description: Hides the dialog without destroying it. # Parameters: none. # Returns: none. ############################################################################## sub withdraw { my ($self) = @_; if (defined($self->{-toplevel})) { $self->{-toplevel}->grabRelease; $self->{-toplevel}->withdraw; } } sub status { my ($self) = @_; return $self->{-status}; } sub output { my ($self) = @_; return $self->{-output}; } sub command { my ($self) = @_; return $self->{-command}; } sub retcode { my ($self) = @_; return $self->{-retcode}; } ############################################################################## # Function: DESTROY # Description: Destructor function that cleans up any allocated resources # when a SimpleExecDialog is no longer referenced. # Parameters: none. # Returns: none. ############################################################################## sub DESTROY { my ($self) = @_; } 1;