X-Git-Url: http://git.annexia.org/?p=techtalk-pse.git;a=blobdiff_plain;f=techtalk-pse.pl;h=85104a6136f3b13e7f720f8d6c9be149977a4147;hp=84f83e88f4a01bc489c8fe422c828b3b6c3f1a59;hb=8cff160150d7af1c8a315c01fa9b9f99368394a2;hpb=951438c967ed3e46171015dc65918ed55f9c17c6 diff --git a/techtalk-pse.pl b/techtalk-pse.pl index 84f83e8..85104a6 100755 --- a/techtalk-pse.pl +++ b/techtalk-pse.pl @@ -3,7 +3,7 @@ # @configure_input@ # # Tech Talk PSE -# Copyright (C) 2010 Red Hat Inc. +# Copyright (C) 2010-2012 Red Hat Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -29,7 +29,9 @@ use Getopt::Long; use Cwd qw(getcwd abs_path); use Glib qw(TRUE FALSE); use Gtk2 -init; -use Gtk2::MozEmbed; +use Gtk2::Gdk::Keysyms; +use Gtk2::WebKit; +use Gnome2::Vte; =encoding utf8 @@ -67,10 +69,25 @@ there is a discussion on L. =head1 RUNNING THE TOOL FROM THE COMMAND LINE -A Tech Talk PSE talk is not a single file, but a directory full of -files. (If you want to start a new talk, see the L section -below). To display or run the talk, change into the directory -containing all those files and run the C command: +Tech Talk PSE talks are just directories containing C<*.html> and +C<*.sh> (shell script) files: + + 0010-introduction.html + 0500-demonstration.sh + 9900-conclusion.html + +The filenames that Tech Talk PSE considers to be slides have to match +the regular expression: + + ^(\d+)(?:-.*)\.(html|sh)$ + +(any other file or subdirectory is ignored). Shell scripts I +be executable. + +=head2 DISPLAYING AN EXISTING TALK + +To display or run a talk, change into the directory containing all +those files and run the C command: cd /path/to/talk/; techtalk-pse @@ -116,15 +133,6 @@ The default is to start at the first slide in the talk. =cut -my $splash = 1; - -=item B<--no-splash> - -Don't display the initial "splash" screen which advertises Tech Talk -PSE to your audience. Just go straight into the talk. - -=cut - my $verbose; =item B<--verbose> @@ -142,17 +150,9 @@ Display version number and exit. =cut -my $mozembed; -my $mozembed_first; -my $mozembed_last; - GetOptions ("help|?" => \$help, "last" => \$last, - "mozembed" => \$mozembed, - "mozembed-first" => \$mozembed_first, - "mozembed-last" => \$mozembed_last, "n=s" => \$start, - "splash!" => \$splash, "start=s" => \$start, "verbose" => \$verbose, "version" => \$version, @@ -170,10 +170,6 @@ if ($version) { die "techtalk-pse: cannot use --start and --last options together\n" if defined $last && defined $start; -# Run with --mozembed: see below. -run_mozembed () if $mozembed; - -# Normal run of the program. die "techtalk-pse: too many arguments\n" if @ARGV >= 2; # Get the true name of the program. @@ -199,6 +195,120 @@ $ENV{talkdir} = $talkdir; # Get the files. my @files; my %files; +my $current; +my $pid; +my $pipeline; + +&reread_directory (); + +print STDERR "read ", 0+@files, " files\n" if $verbose; +if (@files == 0) { + warn "techtalk-pse: no files found, continuing anyway ...\n" +} + +my $w = Gtk2::Window->new (); +my $vbox = Gtk2::VBox->new (); +my $webkit = Gtk2::WebKit::WebView->new (); +my $vte = Gnome2::Vte::Terminal->new (); +my $notebook = Gtk2::Notebook->new (); +my $splash = make_splash_page (); + +my $webkitscroll = Gtk2::ScrolledWindow->new (); +$webkitscroll->add ($webkit); +$webkitscroll->set_policy('automatic', 'automatic'); + +my $webkitpage = $notebook->append_page ($webkitscroll); +my $vtepage = $notebook->append_page ($vte); +my $splashpage = $notebook->append_page ($splash); + +my ($bbox, $bquit, $breload, $bnext, $bback, $brestart) = make_button_bar (); + +$vbox->pack_start($bbox, 0, 0, 0); +$vbox->pack_start($notebook, 1, 1, 0); + +$notebook->set_show_tabs(0); +$notebook->set_show_border(0); + +# Default font size is almost certainly too small +# for audience to see. +# XXX we should make font size configurable via +# @ARGV. +# XXX any way we can scale WebKit programmatically +# to set base size which CSS is relative to ? +# NB careful setting it too big, because it will +# force a min size on the terminal. Scaling 1.3 +# is biggest we can do while fitting 1024x768 +my $font = $vte->get_font; +$font->set_size($font->get_size * 1.3); + +# When an external command exits, automatically +# go to the next slide +$vte->signal_connect ( + 'child-exited' => sub { + if ($pid) { + $pid = 0; + &switch_slide("NEXT"); + } + }); + +# Exit if the window is closed +$w->signal_connect ( + destroy => sub { + Gtk2->main_quit; + return FALSE; + }); + +# Handle left/right arrows, page up/down & home/end +# as slide navigation commands. But not when there +# is a shell running +$w->signal_connect ( + 'key-press-event' => sub { + my $src = shift; + my $ev = shift; + + # If a shell is running, don't trap keys + if ($pid) { + return 0; + } + + if ($ev->keyval == $Gtk2::Gdk::Keysyms{Right} || + $ev->keyval == $Gtk2::Gdk::Keysyms{Page_Down}) { + &switch_slide("NEXT"); + return 1; + } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{Left} || + $ev->keyval == $Gtk2::Gdk::Keysyms{Page_Up}) { + &switch_slide("PREV"); + return 1; + } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{Home}) { + &switch_slide("FIRST"); + return 1; + } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{End}) { + &switch_slide("LAST"); + return 1; + } elsif ($ev->keyval == $Gtk2::Gdk::Keysyms{q} || + $ev->keyval == $Gtk2::Gdk::Keysyms{Escape}) { + Gtk2->main_quit; + return 1; + } + return 0; + }); + + +$w->add ($vbox); +$w->show_all (); + +$w->set_decorated (0); +$w->fullscreen (); +$w->move (0,0); + +my $scr = $w->get_screen(); + +&update_slide(); + +Gtk2->main(); + +exit 0; + sub reread_directory { @files = (); @@ -226,224 +336,281 @@ sub reread_directory $files[0]->{first} = 1; $files[$#files]->{last} = 1; } -} -reread_directory (); -print STDERR "read ", 0+@files, " files\n" if $verbose; -if (@files == 0) { - warn "techtalk-pse: no files found, continuing anyway ...\n" + + # Work out what slide we're starting on. + if (@files && !$current) { + if ($start) { + foreach my $file (@files) { + if ($file->{name} =~ /^$start/) { + $current = $file; + last; + } + } + } elsif ($last) { + $current = $files[$#files]; + } + if (!$current) { + $current = $files[0]; + } + } } -# Work out what slide we're starting on. -my $current; -if (defined $current) { - die "start slide not implemented yet XXX" +sub run_process +{ + $pid = $vte->fork_command("./" . $current->{name}, [], [], undef, 0, 0, 0); } -elsif (@files) { - $current = $files[0]; + +sub kill_process +{ + print STDERR "sending TERM signal to process group $pid\n" + if $verbose; + kill "TERM", -$pid; + + # Clears out any current displayed text + $vte->reset(1, 1); + $vte->set_default_colors(); + $pid = 0; } -# else $current is undefined - -if ($splash) { - my $w = Gtk2::AboutDialog->new; - $w->set_authors ("Richard W.M. Jones"); - $w->set_comments ( - "Superior technical demonstration software\n" - ); - $w->set_program_name ("Tech Talk Platinum Supreme Edition (PSE)"); - $w->set_version ("@VERSION@"); - $w->set_website ("http://people.redhat.com/~rjones"); - $w->set_license ("GNU General Public License v2 or above"); - $w->run; - print STDERR "calling \$w->destroy on about dialog\n" if $verbose; - $w->destroy; - - # The dialog doesn't really get destroyed here. We have - # to add this hack to really destroy it. - Glib::Idle->add (sub { Gtk2->main_quit; return FALSE; }); - Gtk2->main; + +sub switch_slide +{ + my $action = shift; + + if ($pid) { + kill_process (); + } + if ($pipeline) { + $pipeline->set_state('ready'); + $pipeline = undef; + } + print STDERR "action = $action\n" if $verbose; + + my $i = defined $current ? $current->{i} : 0; + + print STDERR "i = $i\n" if $verbose; + if ($action eq "PREV") { + if (defined $current) { + $i--; + } else { + $i = $#files; + } + } elsif ($action eq "NEXT") { + $i++; + } elsif ($action eq "FIRST") { + $i = 0; + } elsif ($action eq "LAST") { + $i = $#files; + } elsif ($action =~ /^I_(\d+)$/) { + $i = $1; + } + + $i = 0 if $i < 0; + if ($i > $#files) { + $current = undef; + } else { + $current = $files[$i]; + } + + &update_slide (); + } -MAIN: while (1) { - if (defined $current) { - my $go = show_slide ($current); - if (defined $go) { - print STDERR "go = $go\n" if $verbose; - last MAIN if $go eq "QUIT"; - - my $i = $current->{i}; - print STDERR "i = $i\n" if $verbose; - $i-- if $go eq "PREV" && $i > 0; - $i++ if $go eq "NEXT" && $i+1 < @files; - $current = $files[$i]; - } +sub update_slide +{ + if ($current) { + # Display an HTML page. + if ($current->{ext} eq "html") { + $notebook->set_current_page ($webkitpage); + my $name = $current->{name}; + my $url = "file://$talkdir/$name"; + + $webkit->load_uri ($url); + $webkit->grab_focus (); + } + # Run a shell command. + elsif ($current->{ext} eq "sh") { + $notebook->set_current_page ($vtepage); + $vte->grab_focus (); + run_process (); + } } else { - print "No slides found. Press any key to reload directory ...\n"; - $_ = ; + $notebook->set_current_page ($splashpage); } - # Reread directory between slides. - reread_directory (); + if ($pid) { + $brestart->show (); + } else { + $brestart->hide (); + } - if (defined $current && !exists $files{$current->{name}}) { - # Current slide was deleted. - undef $current; - $current = $files[0] if @files; + if (defined $current) { + $bquit->hide (); + $breload->hide (); + $bnext->set_sensitive (1); + $bback->set_sensitive (!exists $current->{first}); + } else { + $bquit->show (); + if (@files) { + $breload->hide (); + } else { + $breload->show (); + } + $bnext->set_sensitive (0); + $bback->set_sensitive (int(@files)); } } -sub show_slide + +sub make_splash_page { + my $box = Gtk2::VBox->new(); + + my $title = Gtk2::Label->new ("Tech Talk Platinum Supreme Edition (PSE)"); + $title->set_use_markup (1); + + $box->pack_start ($title, 0, 1, 0); + + my $vers = Gtk2::Label->new ("@VERSION@"); + $vers->set_use_markup (1); + $box->pack_start ($vers, 0, 1, 0); + + my $tagline = Gtk2::Label->new ("Superior technical demonstration software"); + $tagline->set_use_markup (1); + + $box->pack_start ($tagline, 0, 1, 0); + $box->pack_start (Gtk2::Label->new (""), 0, 1, 0); + $box->pack_start (Gtk2::Label->new ("Author: Richard W.M. Jones"), 0, 1, 0); + + my $url = Gtk2::Label->new ("http;//people.redhat.com/~rjones/"); + $url->set_use_markup (1); + $box->pack_start ($url, 0, 1, 0); + $box->pack_start (Gtk2::Label->new ("GNU General Public License v2 or above"), 0, 1, 0); + + return $box; +} + +# Make the standard button bar across the top of the page. +sub make_button_bar { - my $slide = shift; - - # Display an HTML page. - if ($slide->{ext} eq "html") { - # MozEmbed is incredibly crashy, so we run ourself as a - # subprocess, so when it segfaults we don't care. - my @cmd = ($0, "--mozembed"); - push @cmd, "--mozembed-first" if exists $slide->{first}; - push @cmd, "--mozembed-last" if exists $slide->{last}; - my $url = "file://$talkdir/" . $slide->{name}; - push @cmd, $url; - print STDERR "running subcommand: ", join (" ", @cmd), "\n" - if $verbose; - system (@cmd); - die "failed to execute subcommand: ", join(" ", @cmd), ": $!\n" - if $? == -1; - if ($? & 127) { - # Subcommand probably segfaulted, just continue to next slide. - return "NEXT"; - } else { - my $r = $? >> 8; - if ($r == 0) { - return "NEXT"; - } elsif ($r == 1) { - return "PREV"; - } elsif ($r == 2) { - return "QUIT"; - } - } - } - # Run a shell command. - elsif ($slide->{ext} eq "sh") { - my $pid; - # http://docstore.mik.ua/orelly/perl/cookbook/ch10_17.htm - local *run_process = sub { - $pid = fork (); - die "fork: $!" unless defined $pid; - unless ($pid) { - # Child. - POSIX::setsid (); - exec ("./".$slide->{name}); - die "failed to execute command: ", $slide->{name}, ": $!"; - } - # Parent returns. - }; - local *kill_process = sub { - print STDERR "sending TERM signal to process group $pid\n" - if $verbose; - kill "TERM", -$pid; - }; - run_process (); - - my $r = "NEXT"; - - my $w = Gtk2::Window->new (); - - my $s = $w->get_screen; - $w->set_default_size ($s->get_width, -1); - $w->move (0, 0); - $w->set_decorated (0); - - my $bbox = Gtk2::HButtonBox->new (); - $bbox->set_layout ('start'); - - my $bnext = Gtk2::Button->new ("Next slide"); - $bnext->signal_connect (clicked => sub { $r = "NEXT"; $w->destroy }); - $bnext->set_sensitive (!(exists $slide->{last})); - $bbox->add ($bnext); - - my $bback = Gtk2::Button->new ("Back"); - $bback->signal_connect (clicked => sub { $r = "PREV"; $w->destroy }); - $bback->set_sensitive (!(exists $slide->{first})); - $bbox->add ($bback); - - my $brestart = Gtk2::Button->new ("Kill & restart"); - $brestart->signal_connect (clicked => sub { - kill_process (); - run_process (); - }); - $bbox->add ($brestart); - - my $bquit = Gtk2::Button->new ("Quit"); - $bquit->signal_connect (clicked => sub { $r = "QUIT"; $w->destroy }); - $bbox->add ($bquit); - $bbox->set_child_secondary ($bquit, 1); - - $w->add ($bbox); - - $w->signal_connect (destroy => sub { - Gtk2->main_quit; - return FALSE; - }); - $w->show_all (); - - Gtk2->main; - - kill_process (); - print STDERR "returning r=$r\n" if $verbose; - return $r; + my $bbox = Gtk2::Toolbar->new (); + $bbox->set_style ("GTK_TOOLBAR_TEXT"); + + my $i = 0; + + my $bquit = Gtk2::ToolButton->new (undef, "Quit"); + $bquit->signal_connect (clicked => sub { Gtk2->main_quit }); + $bbox->insert ($bquit, $i++); + + my $breload = Gtk2::ToolButton->new (undef, "Reload"); + $breload->signal_connect (clicked => sub { reread_directory () }); + $bbox->insert ($breload, $i++); + + my $bnext = Gtk2::ToolButton->new (undef, "Next slide"); + $bnext->signal_connect (clicked => sub { &switch_slide ("NEXT") }); + $bbox->insert ($bnext, $i++); + + my $bback = Gtk2::ToolButton->new (undef, "Back"); + $bback->signal_connect (clicked => sub { &switch_slide ("PREV") }); + $bbox->insert ($bback, $i++); + + $bbox->insert (Gtk2::SeparatorToolItem->new (), $i++); + + my $brestart = Gtk2::ToolButton->new (undef, "Kill & restart"); + $brestart->signal_connect (clicked => + sub { + kill_process (); + run_process (); + }); + $bbox->insert ($brestart, $i++); + + my $sep = Gtk2::SeparatorToolItem->new (); + $sep->set_expand (TRUE); + $sep->set_draw (FALSE); + $bbox->insert ($sep, $i++); + + my $optsmenu = Gtk2::Menu->new (); + + my $mfirst = Gtk2::MenuItem->new ("First slide"); + $mfirst->signal_connect (activate => sub { &switch_slide ("FIRST") }); + $mfirst->show (); + $optsmenu->append ($mfirst); + + my $mlast = Gtk2::MenuItem->new ("Last slide"); + $mlast->signal_connect (activate => sub { &switch_slide ("LAST") }); + $mlast->show (); + $optsmenu->append ($mlast); + + my $slidesmenu = Gtk2::Menu->new (); + foreach (@files) { + my $item = Gtk2::MenuItem->new ($_->{name}); + my $index = $_->{i}; + $item->signal_connect (activate => sub { &switch_slide ("I_$index") }); + $item->set_sensitive ($current->{i} != $index); + $item->show (); + $slidesmenu->append ($item); } + + my $mslides = Gtk2::MenuItem->new ("Slides"); + $mslides->set_submenu ($slidesmenu); + $mslides->show (); + $optsmenu->append ($mslides); + + my $sep2 = Gtk2::SeparatorMenuItem->new (); + $sep2->show (); + $optsmenu->append ($sep2); + + my $mscreenshot = Gtk2::MenuItem->new ("Take a screenshot"); + $mscreenshot->signal_connect (activate => sub { screenshot () }); + $mscreenshot->show (); + $optsmenu->append ($mscreenshot); + + my $sep3 = Gtk2::SeparatorMenuItem->new (); + $sep3->show (); + $optsmenu->append ($sep3); + + my $mquit = Gtk2::MenuItem->new ("Quit"); + $mquit->signal_connect (activate => sub { Gtk2->main_quit }); + $mquit->show (); + $optsmenu->append ($mquit); + + my $moptions = Gtk2::MenuToolButton->new (undef, "Options"); + #$boptions->signal_connect (clicked => + # sub { $optsmenu->popup (undef, undef, undef, undef, ?, ?) } ); + $bbox->insert ($moptions, $i++); + $moptions->set_menu ($optsmenu); + + return ($bbox, $bquit, $breload, $bnext, $bback, $brestart); } -# If invoked with the --mozembed parameter then we just display a -# single page. This is just to prevent crashes in MozEmbed from -# killing the whole program. -sub run_mozembed +# Try running the external "gnome-screenshot" program, if it's +# available, else take a screenshot using gdk routines. +sub screenshot { - my $r = 0; - - my $w = Gtk2::Window->new (); - my $vbox = Gtk2::VBox->new (); - my $moz = Gtk2::MozEmbed->new (); - - my $bbox = Gtk2::HButtonBox->new (); - $bbox->set_layout ('start'); - - $vbox->pack_start ($bbox, 0, 0, 0); - $vbox->add ($moz); - $w->fullscreen (); - #$w->set_default_size (640, 480); - $w->add ($vbox); - - my $bnext = Gtk2::Button->new ("Next slide"); - $bnext->signal_connect (clicked => sub { $r = 0; $w->destroy }); - $bnext->set_sensitive (!$mozembed_last); - $bbox->add ($bnext); - - my $bback = Gtk2::Button->new ("Back"); - $bback->signal_connect (clicked => sub { $r = 1; $w->destroy }); - $bback->set_sensitive (!$mozembed_first); - $bbox->add ($bback); - - my $bquit = Gtk2::Button->new ("Quit"); - $bquit->signal_connect (clicked => sub { $r = 2; $w->destroy }); - $bbox->add ($bquit); - $bbox->set_child_secondary ($bquit, 1); - - $w->signal_connect (destroy => sub { - Gtk2->main_quit; - return FALSE; - }); - $w->show_all (); + system ("gnome-screenshot"); + + if ($? == -1) { + # We are going to save the entire screen. + my $root = Gtk2::Gdk->get_default_root_window (); + my ($width, $height) = $root->get_size; - $moz->load_url ($ARGV[0]); - Gtk2->main; + # Create blank pixbuf to hold the image. + my $gdkpixbuf = Gtk2::Gdk::Pixbuf->new ('rgb', + 0, 8, $width, $height); + + $gdkpixbuf->get_from_drawable ($root, $root->get_colormap (), + 0, 0, 0, 0, $width, $height); + + my $i = 0; + $i++ while -f "screenshot$i.png"; + $gdkpixbuf->save ("screenshot$i.png", 'png'); + } - exit $r; + return FALSE; } 1; +__END__ + =head1 TUTORIAL =head2 START WRITING A TALK @@ -587,9 +754,16 @@ terminal. In C, I have: # -*- shell-script -*- + + # Place any local environment variables required in 'local'. + if [ -f local ]; then source local; fi + export PS1="$ " - export HISTFILE=/tmp/history + + export HISTFILE=$talkdir/history + rm -f $HISTFILE + touch $HISTFILE add_history () { @@ -598,6 +772,11 @@ In C, I have: terminal () { + # Make $HISTFILE unwritable so the shell won't update it + # when it exits. + chmod -w $HISTFILE + + # Run gnome-terminal. exec \ gnome-terminal \ --window \