/usr/local/CPAN/PPresenter/PPresenter/Show.pm
# Copyright (C) 2000-2002, Free Software Foundation FSF.
# SHOW
#
# This packages drives the slide-show, while the main package is
# used to limit the user's access to the routines.
#
package PPresenter::Show;
use strict;
use Carp;
use PPresenter::Object;
use base 'PPresenter::Object';
use PPresenter::Viewport;
use PPresenter::Viewport::Control;
use PPresenter::Slide;
use PPresenter::StyleElem;
use PPresenter::Images;
use constant ObjDefaults =>
{ -name => 'Portable Presenter'
, -aliases => undef
, -trace => '/dev/null' # /dev/tty is also useful ;)
, -geometry => undef
, -controlDisplay => undef # $ENV{DISPLAY}
, -controlGeometry => '640x480'
, -imageSizeBase => undef
, -resizeImages => 1
, -enlargeImages => 0
, -scaledImagesDir => undef
, -style => undef # or use $show->addStyle
, -styles => undef
, -startSlide => 'FIRST'
, -totaltime => undef
, -tags => undef # initial selection of sites.
, -flushPhases => 0
, -enableCallbacks => 1
, -clockTics => 1.0
, -halted => 1
};
sub InitObject(@)
{ my $show = shift;
$show->SUPER::InitObject(@_);
# own exit executes END, default does not.
$SIG{INT} = $SIG{QUIT} = $SIG{TERM} = sub {exit};
my $tracefile = $show->{-trace};
open (PPresenter::TRACE, ">$tracefile")
or die "Unable to open file $tracefile for trace.\n";
$show->{slides} = [];
$show->{exporters} = [];
# Styles.
$show->add('style'
, 'PPresenter::Style::Default'
, 'PPresenter::Style::SlideNotes'
);
$show->add(style => $show->{-style})
if defined $show->{-style};
$show->add(style => @{$show->{-styles}})
if defined $show->{-styles};
$show->{selected_style} = $show->find_style('default')
|| $show->find_style('FIRST');
$show->{-totaltime} = $show->time2secs($show->{-totaltime})
if defined $show->{-totaltime};
# Images.
$show->{images} = PPresenter::Images->new
( show => $show
, tmpdir => $show->{-scaledImagesDir} || undef
);
# warn "Define -scaledImagesDir to safe scaled images.\n"
# if $^W && !defined $show->{-scaledImagesDir};
$show;
}
sub remove_dir($)
{ my ($show, $dir) = @_;
unless(opendir D, $dir)
{ die "Couldn't open directory $dir to cleanup.\n";
return;
}
while(defined (my $item = readdir D))
{ my $path = "$dir/$item";
if($item eq '.' || $item eq '..') {}
elsif(-d $path) {$show->remove_dir($path) }
else {unlink "$path"}
}
closedir D;
rmdir $dir;
}
#
# Find
#
sub find($;$)
{ my ($show, $type, $name) = @_;
$name = 'SELECTED' unless defined $name;
my $object = $type eq 'main' ? $show
: $type eq 'slide' ? $show->find_slide($name)
: $type eq 'viewport' ? $show->find_viewport($name)
: $type eq 'style' ? $show->find_style($name)
: $type eq 'image' ? $show->{images}->findImage($name)
: $show->{selected_style}->find($type, $name);
die "Cannot find $type $name.\n"
unless defined $object;
$object;
}
#
# Select
#
sub select($$)
{ my ($show, $type, $name) = @_;
return $show->{selected_viewport} = $show->find_viewport($name)
if $type eq 'viewport';
if($type eq 'slide')
{ warn "Cannot select a slide.\n";
return undef;
}
return $show->{selected_style}->select($type, $name)
unless $type eq 'style';
my $style = $show->find_style($name);
unless(defined $style)
{ warn "Cannot find style $name.\n";
return undef;
}
map {$_->select(style => $style)} @{$show->{viewports}};
}
sub add($$@)
{ my ($show, $type) = (shift, shift);
return $show->addSlide(@_) if $type eq 'slide';
return $show->addViewport(@_) if $type eq 'viewport';
return $show->addStyle(@_) if $type eq 'style';
return $show->image(@_) if $type eq 'image';
$show->{selected_style}->add($type, @_);
}
sub changeDefaults($$@)
{ my ($show, $type, $name) = (shift, shift, shift);
return $show->change_viewport($name, @_)
if $type eq 'viewport';
return $show->{selected_style}->change($type, $name, @_)
if $type ne 'style';
return map {$_->change(@_)} $show->find_style('ALL')
if $name eq 'ALL';
my $style = $show->find_style($name);
warn "Can't find style $name to change.\n", return
unless defined $style;
$show->addStyle($style->change(@_));
}
#
# Viewports
#
sub hasViewports()
{ my $show = shift;
return 0 unless defined $show->{viewports};
foreach (@{$show->{viewports}})
{ return 1 unless $_->showSlideNotes;
}
return 0;
}
sub addViewport(@)
{ my $show = shift;
die "Add viewports (screens) before the first slide.\n"
if @{$show->{slides}};
shift while @_ > 0 && !defined $_[0]; # skip undefs.
return unless @_; # nothing to add.
# Make flat arglist to hash.
return $show->addViewport( {@_} )
unless ref $_[0];
return map {$show->addViewport($_)} @{$_[0]}
if ref $_[0] eq 'ARRAY';
my $proto = shift;
my $screen = $proto->{-hasControl} || 0
? PPresenter::Viewport::Control->new(%$proto, show => $show)
: PPresenter::Viewport->new(%$proto, show => $show);
print PPresenter::TRACE "Defined viewport $screen.\n";
push @{$show->{viewports}}, $screen;
$screen;
}
sub findControlViewport()
{ my $show = shift;
my @controls = grep {$_->hasControl} @{$show->{viewports}};
die "No viewport has control (-hasControl=>1).\n" unless @controls;
die "Two show controls defined: @controls.\n" if @controls > 1;
$controls[0];
}
sub find_viewport($)
{ my ($show, $name) = @_;
$show->initViewports;
return $show->{selected_viewport} if $name eq 'SELECTED';
PPresenter::Viewport->fromList($show->{viewports}, $name);
}
sub change_viewport($@)
{ my ($show, $name) = (shift, shift);
$show->initViewports;
return map {$_->change(@_)} @{$show->{viewports}}
if $name eq 'ALL';
my $viewport = $show->find_style($name);
warn "Can't find viewport $name to change.\n", return
unless defined $viewport;
$viewport->change(@_);
}
sub showSlideControl() {$_[0]->{control}->showControl }
sub updateSlideControl() {$_[0]->{control}->updateSlides}
sub iconifyControl() {$_[0]->{control}->iconify }
sub viewports() {@{$_[0]->{viewports}} }
sub initViewports()
{ my $show = shift;
return if exists $show->{viewports_initialized};
$show->{viewports_initialized} = 1;
unless($show->hasViewports)
{ $show->addViewport
( -name => 'default'
, -hasControl => ! defined $show->{-controlDisplay}
);
}
$show->addViewport
( -name => 'control'
, -display => $show->{-controlDisplay}
, -geometry => $show->{-controlGeometry}
, -hasControl => 1
, -includeControls => 1
, -style => 'slidenotes'
, -showSlideNotes => 1
) if defined $show->{-controlDisplay};
$show->{selected_viewport} = $show->find_viewport('default')
|| $show->find_viewport('FIRST');
# Find-out which window has the controls.
my @controls = grep {$_->hasControl} @{$show->{viewports}};
die "Two show controls defined: @controls.\n" if @controls > 1;
die "No viewport has control.\n" unless @controls;
$show->{control} = $controls[0];
$show;
}
#
# Styles
#
sub addStyle(@)
{ my $show = shift;
shift while @_ > 0 && !defined $_[0];
return unless @_;
return map {$show->addStyle($_)} @_ if @_>1;
return map {$show->addStyle($_)} @$_ if ref $_ eq 'ARRAY';
my $style = shift;
if(ref $style && $style->isa('PPresenter::Style'))
{ unshift @{$show->{styles}}, $style;
return $show;
}
die "$style is not a style.\n" if ref $style;
push @{$show->{styles}},
PPresenter::StyleElem::load($style, show => $show);
}
sub find_style($)
{ my ($show, $name) = @_;
return $show->{selected_style} if $name eq 'SELECTED';
PPresenter::Style->fromList($show->{styles}, $name);
}
#
# Slides
#
sub addSlide(@)
{ my $show = shift;
$show->initViewports;
unless (ref $_[0]) # list of strings: one slide.
{ my $slide = PPresenter::Slide->new(show => $show, @_);
push @{$show->{slides}}, $slide;
return $slide;
}
my @slides;
foreach (@_)
{ if(ref $_ eq 'ARRAY')
{ push @slides, $show->addSlide(@$_);
}
elsif($_->isa('PPresenter::Slide'))
{ push @{$show->{slides}}, $_;
push @slides, $_;
}
else
{ die "You tried to add a ", ref $_, " named \"$_\" as slide.\n";
}
}
}
sub includeShow($)
{ my ($show, $show2) = @_;
$show->addSlide($show2->slides);
}
sub find_slide($)
{ my ($show, $name) = @_;
$name = 'LAST' unless defined $name;
$name = 'LAST' if $name eq 'SELECTED';
PPresenter::Slide->fromList($show->{slides}, $name);
}
sub slides() { @{shift->{slides}} }
sub activeSlides() { grep {$_->isActive} @{shift->{slides}} }
sub numberSlides() { scalar @{shift->{slides}} }
sub current() { shift->{current_slide} }
sub containsSlideNotes()
{ my $show = shift;
foreach ($show->slides)
{ return 1 if $_->hasSlideNotes;
}
return 0;
}
#
# Program
#
sub mustFlushPhases() { shift->{-flushPhases} }
sub flushPhases() { shift->addPhase(9) }
sub updatePhaseSymbols($$) { shift->{control}->setPhase(@_) }
sub addPhase($)
{ my ($show, $count) = @_;
$show->{current_slide}->nextPhase while $count-- > 0;
}
sub nextSelected($;)
{ my $show = shift;
my $current = $show->{current_slide};
while(defined $current)
{ $current = $show->find_slide(defined $current->{-nextSlide}
? $current->{-nextSlide} : $current->{number} +1);
return $current if defined $current && $current->isActive;
}
return undef;
}
sub previousSelected()
{ my $show = shift;
my $current = $show->{current_slide};
while(defined $current)
{ $current = $show->find_slide($current->number -1);
return $current if defined $current && $current->isActive;
}
return undef;
}
#
# Slide
#
sub showSlide($)
{ my ($show, $next_slide) = @_;
return unless defined $next_slide;
my $slides = $show->{slides};
my $current = $show->{current_slide};
if(ref $next_slide ne '' && $next_slide->isa("PPresenter::Slide"))
{ return if "$next_slide" eq "$current";
$next_slide->{previous} = $current;
}
elsif($next_slide eq 'FIRST')
{ $next_slide = $show->find_slide(0);
return unless defined $next_slide;
$next_slide->{previous} = $current || $next_slide;
}
elsif($next_slide eq 'LAST')
{ $next_slide = $show->find_slide($#{$slides});
return unless defined $next_slide;
$next_slide->{previous} = $current;
}
elsif($next_slide eq 'BACK')
{ return if $current->{number}==0;
$next_slide = $show->find_slide($current->{previous} || undef);
return unless defined $next_slide;
$next_slide->{previous} = $current;
}
elsif($next_slide eq 'NEXT')
{ return if $current->{number} == $#$slides;
$next_slide = $show->find_slide($current->{-nextSlide} || $current->{number} +1);
return unless defined $next_slide;
$next_slide->{previous} = $current;
}
elsif($next_slide eq 'NEXT_SELECTED')
{
$next_slide = $show->nextSelected;
return unless defined $next_slide;
$next_slide->{previous} = $current;
}
elsif($next_slide eq 'PREVIOUS')
{ $next_slide = $show->previousSelected;
return unless defined $next_slide;
$next_slide->{forward} = $current;
}
elsif($next_slide eq 'FORWARD')
{ $next_slide = $show->find_slide($current->{forward} || undef);
return unless defined $next_slide;
}
elsif($next_slide eq 'THIS')
{ $next_slide = $current;
}
elsif($next_slide !~ /\D/) # is a number
{ $next_slide = $show->find_slide($next_slide);
}
return unless defined $next_slide;
undef $show->{proceed_after};
print $show->timeStamp,": showing $next_slide->{number} \"$next_slide\".\n";
# Show new slide.
$show->busy(1);
$next_slide->prepare->show;
$show->busy(0);
$show->{current_slide} = $next_slide;
$show->{current_slide_number} = $next_slide->{number};
$show->{control}->update($show, $next_slide)->sync;
$next_slide->startProgram($show);
}
# Some of the information about the show will be copied to the presenter,
# but most not.
# The information stored for each object should contain all necessary
# information to produce the windows, because one must be able to switch
# between slides at random.
sub run()
{
my $show = shift;
die "No options allowed for run()" if @_;
unless(defined $show->{slides})
{ warn "No slides to show.";
return;
}
# Initialize tags.
$show->selectTags($show->{-tags})
if defined $show->{-tags};
# Initialize time.
my $totaltime = $show->{-totaltime};
my $slides = $show->{slides};
my $sumtime = 0;
my $not_active= 0;
foreach (@$slides)
{ if($_->isActive) { $sumtime += $_->requiredTime }
else { $not_active++ }
}
unless(defined $totaltime)
{ print "Total time $sumtime seconds for ",
@$slides-$not_active, " slides.\n";
$show->{-totaltime} = $sumtime;
}
elsif($sumtime > $totaltime)
{ my $load = $sumtime/$totaltime;
warn "Your ", @$slides-$not_active, " slides take $sumtime",
" seconds but you have only $totaltime seconds (",
int($load*100-100), "% too much)\n";
}
elsif($sumtime < $totaltime)
{ my $load = $sumtime/$totaltime;
my $spare = $totaltime - $sumtime;
$spare = $spare > 180
? int($spare/60 + .5)." minutes"
: $spare." seconds";
print "You have $spare spare on the "
, @$slides-$not_active, " slides"
, " (", 100-int($load*100), "% too short)\n";
}
print +($not_active==1 ? "One slide is" : "$not_active slides are"),
" not selected to be displayed.\n"
if $not_active;
# Fill-in all controls.
$show->{control} = $show->findControlViewport
unless defined $show->{control};
$show->{control}->createControl;
$show->slideSelectionChanged;
# Schedule to start the show.
$show->{runtime} = 0;
my $ascreen = $show->{control}->screen;
$ascreen->after(100, [ \&start, $show ] );
use Tk;
MainLoop;
}
#
# Show main control
#
sub stop
{ my $show = shift;
print $show->timeStamp, ": show stopped\n";
exit 0;
}
sub start
{ my $show = shift;
# When realization is slow, we have to wait for it.
my $ascreen = $show->{control}->screen;
$ascreen->after(100) until $ascreen->width > 1;
$show->showSlide($show->{-startSlide});
$show->{-starttime} = time;
print $show->timeStamp,": show started\n";
$ascreen->repeat(int ($show->{-clockTics}*1000), [ \&clockTic, $show ] );
}
#
# Tags
#
sub selectTags(@)
{ my $show = shift;
foreach (@_)
{ my $tag;
if(ref $_ eq 'ARRAY') {$show->selectTags(@$_)}
elsif(($tag) = /^\s*\-(\w+)/ ) {$show->clearTag($tag) }
elsif(($tag) = /^\s*\+?(\w+)/) {$show->setTag($tag) }
else {warn "Do not understand tag specification $_.\n"; }
}
}
sub setTag($)
{ my ($show, $tag) = @_;
map {$_->setActive(1) if $_->hasTag($tag)} @{$show->{slides}};
$show->slideSelectionChanged;
}
sub clearTag($)
{ my ($show, $tag) = @_;
map {$_->setActive(0) if $_->hasTag($tag)} @{$show->{slides}};
$show->slideSelectionChanged;
}
sub countSelectedTags()
{ my $show = shift;
my (%count, %set, %clear);
foreach (@{$show->{slides}})
{ if($_->isActive) {map {$set{$_}++; $count{$_}++} $_->tags}
else {map {$clear{$_}++; $count{$_}++} $_->tags}
}
map { [ $_, $count{$_}, $set{$_}||0, $clear{$_}||0 ] }
sort keys %count;
}
sub slideSelectionChanged() {shift->{control}->slideSelectionChanged}
sub busy($) {my ($show, $busy) = @_; $show->{control}->busy($busy)}
#
# TickTac
#
sub clockTic($)
{ my $show = shift;
my $interval = $show->{-clockTics};
my $slide = $show->{current_slide};
if($show->{-halted})
{ $slide->suspended($interval);
return $show;
}
$show->{runtime} += $interval;
$show->{control}->clockTic($interval, $slide);
$show->showSlide('NEXT_SELECTED') if $slide->wantNextSlide;
$show;
}
sub setRunning($)
{ my ($show,$running) = @_;
$show->{-halted} = not $running
if defined $running;
my $status = $show->{-halted}
? 'halted'
: $show->{runtime} > 0 ? 'continues' : 'started';
print $show->timeStamp,": run $status.\n";
}
sub setProceedAfter($) {$_[0]->{proceed_after} = $_[1]}
sub enableCallbacks()
{ my $show = shift;
my $old = $show->{-enableCallbacks};
$show->{-enableCallbacks} = shift if @_;
$old;
}
sub minSecs($)
{ my $secs = int $_[1];
return "??:??" unless defined $secs;
my $mins = 0;
if($secs > 60)
{ $mins = int($secs/60);
$secs -= $mins*60;
}
sprintf "%2d:%02d",$mins,$secs;
}
sub timeStamp(;$)
{ my $show = shift;
my $tic = shift || time;
my ($sec, $min, $hour) = localtime $tic;
sprintf "%02d:%02d:%02d (%s)",
$hour, $min, $sec, $show->minSecs($show->{runtime});
}
sub time2secs($)
{ my ($show, $string) = @_;
my ($hours, $mins, $secs);
if( ($hours, $mins, $secs)
= $string =~ /^\s*(?:(\d+)h)? # hours
\s*(?:(\d+)m)? # minutes
\s*(?:(\d+)s?)?\s*$/x) # seconds
{}
elsif( ($hours,$mins,$secs)
= $string =~ /^\s*(?:(?:(\d+):)? # hours
(\d*):)? # minutes
\s*:?(\d*)\s*$/x) # seconds
{}
else
{ warn "Cannot understand time specification: $string.\n";
return 60;
}
($hours||0)*3600 + ($mins||0)*60 + ($secs||0);
}
#
# Images
#
sub image(@) {shift->{images}->image(@_)}
sub imageSizeBase() {shift->{-imageSizeBase}}
sub resizeImages() {shift->{-resizeImages}}
sub enlargeImages() {shift->{-enlargeImages}}
sub addImageDir(@) {shift->{images}->addImageDir(@_)}
sub Photo(@) {shift->{selected_viewport}->Photo(@_)}
sub findImageFile(@) {shift->{images}->findImageFile(@_)}
sub printSlide() {shift->{current_slide}->print }
#
# Bootstrapping Exporters
#
sub addExporter($@)
{ my ($show, $name) = (shift,shift);
if(ref $name eq '')
{ eval "require $name";
if($@)
{ croak "Cannot use export $name: $@.\n";
return undef;
}
my $exporter = $name->new(@_);
# die "$name is not an exporter module.\n"
# unless $exporter->isa('PPresenter::Export');
push @{$show->{exporters}}, $exporter;
print PPresenter::TRACE "Loaded exporter $exporter.\n";
return $exporter;
}
if($name->isa('PPresenter::Exporter'))
{ push @{$show->{exporters}}, $name->change(@_);
print PPresenter::TRACE "Added exporter $name.\n";
return $name;
}
warn "WARNING: addExporter expects a module-name.\n";
return undef;
}
sub exporters() {@{shift->{exporters}}}
my $image_magick_installed;
sub hasImageMagick()
{ my $show = shift;
unless(defined $image_magick_installed)
{ eval 'require Image::Magick';
$image_magick_installed = ($@ eq '');
warn "Improve image quality by installing Image::Magick.\n"
unless $image_magick_installed;
}
return $image_magick_installed;
}
sub runsOnX()
{ my $show = shift;
exists $ENV{DISPLAY};
}
#
# Decorations
#
sub decodata($) # maintains decoration information over slide-bounds.
{ my ($show, $view) = @_;
my $label = 'deco_'.$view->viewport;
exists $show->{$label} ? $show->{$label} : ($show->{$label} = {});
}
1;