# Author: Chao-Kuei Hung # For more info, including license, please see doc/index.html package RecCanvas; use strict; use Carp; use Tk::Canvas; use Tk::Derived; use base qw(Tk::Derived Tk::Canvas); # see perldoc Tk::mega and perldoc Tk::composite Construct Tk::Widget 'RecCanvas'; # Should I use InitObject (c.f. Tk::mega) or Populate (c.f. Tk::Derived)? # Both seem to work ok. According to Tk::composite, a "widget based" # composite should define InitObject. RecCanvas is not a composite, is it? sub InitObject { my ($self, $args) = @_; $self->{-name} = delete $args->{-name}; carp "Each RecCanvase must have a unique -name. RecCanvas'es" . "without names cause asynchrony in multiple window scenarios" unless defined $self->{-name}; $self->{-maxlevel} = (delete $args->{-maxlevel} or 3); $self->{-elevation} = (delete $args->{-elevation} or 0); $self->{"#bbox"} = [0, 0, 0, 0]; $self->SUPER::InitObject($args); $self->forget(); $self->ConfigSpecs( -recorder=>[qw(METHOD recorder Recorder 0)], -coordinator=>[qw(PASSIVE coordinator Coordinator), undef], ); } sub cget { my ($self, $opt) = @_; return $self->{$opt} if exists $self->{$opt}; return $self->SUPER::cget($opt); } sub forget { my ($self) = @_; # $self->delete("all"); $self->time_seek(0) if (exists $self->{"#now"} and $self->{"#now"} > 0); my ($t) = [ map {0} 0..$self->{-maxlevel} ]; $self->{"#history"} = [ {mark=>$t} ]; $self->{"#now"} = 0; } sub get_mark_at { my ($self, $pos, $level) = @_; my ($h) = $self->{"#history"}; return (defined $level) ? $h->[$pos]{mark}[$level] : @{ $h->[$pos]{mark} }; } sub total_marks { my ($self, $level) = @_; my ($h) = $self->{"#history"}; return $self->get_mark_at($#$h, $level); } sub get_mark { my ($self, $level) = @_; return $self->get_mark_at($self->{'#now'}, $level); } sub set_mark { my ($self, $level, $final) = @_; $level += $self->{-elevation}; die "level ", $level-$self->{-elevation}, " shifted $self->{-elevation} out of range (0..$self->{-maxlevel})" unless ($level >= 0 and $level <= $self->{-maxlevel}); return unless $self->cget(-recorder); my ($now, $h) = ($self->{"#now"}, $self->{"#history"}); @{ $h->[$now]{mark} } = $now>0 ? @{ $h->[$now-1]{mark} } : map {0} 0..$self->{-maxlevel}; foreach my $i (0..$level) { ++$h->[$now]{mark}[$i]; } my ($ctrl) = $self->cget(-coordinator); $ctrl->mark_other_canvases($self, $level) # note: $level is absolute if (ref $ctrl and not defined $final); } # state: 0<--->1<--->2<--->3<--->4<--->5<--->6<--->7<--->8 # mark: 0 1 2 3 4 5 6 7 # 0 0 1 1 2 2 3 3 # 0 0 0 0 1 1 1 1 # 0 0 0 0 0 0 0 0 sub no_op { # no operation; dummy step; useful only in multiple-canvas scenarios my ($self) = @_; return unless $self->cget(-recorder); my ($h) = $self->{"#history"}; my ($now) = $self->{"#now"}; my ($mark) = $h->[$now]{mark}; ++$now; @{ $h->[$now]{mark} } = @$mark; $self->{"#now"} = ++$h->[$now]{mark}[0]; } sub record_one_step { my ($self, $id, $frw_func, $frw_args, $bkw_func, $bkw_args) = @_; print "f($id): $frw_func(", join(",",@$frw_args), ")\n" unless ($#$frw_args % 2 == 1); print "b($id): $bkw_func(", join(",",@$bkw_args), ")\n" unless ($#$bkw_args % 2 == 1); my ($h, $now) = ($self->{"#history"}, $self->{"#now"}); @{ $h->[$now]{forward} }{"id", "method", "args"} = ($id, $frw_func, $frw_args); my ($mark) = $h->[$now]{mark}; ++$now; @{ $h->[$now]{backward} }{"id", "method", "args"} = ($id, $bkw_func, $bkw_args); @{ $h->[$now]{mark} } = @$mark; $self->{"#now"} = ++$h->[$now]{mark}[0]; my ($ctrl) = $self->cget(-coordinator); $ctrl->incr_other_canvases($self) if ref $ctrl; } sub itemconfigure { my ($self, $id, %opts) = @_; my ($method, $camouflage, $hidden, $k); $method = "SUPER::itemconfigure"; $camouflage = $self->cget(-bg); foreach $k (keys %opts) { print "<$k:$opts{$k}>\n" unless defined $k and defined $opts{$k}; if ($opts{$k} eq "hidden" and $k ne "-state") { $opts{$k} = $camouflage; $hidden = 1; } } if ($self->cget(-recorder)) { my (%bkw) = %opts; map { $bkw{$_} = $self->itemcget($id, $_) } keys %bkw; $self->record_one_step($id, $method, [%opts], $method, [%bkw]); # why? this one-step statement causes undef and "" to be lost, # resulting in odd number of hash elements, etc. # This happens only after I upgraded from perl 5.8.0 to perl 5.8.5 # $self->record_one_step($id, # $method, [%opts], # $method, [map { $_=>$self->itemcget($id, $_) } keys %opts] # ); # if ($hidden) { # $self->lower($id, "all"); # } else { # $self->raise($id, "all"); # } } return $self->$method($id, %opts); } sub coords { my ($self, $id, @coords) = @_; my ($method) = "SUPER::coords"; if ($self->cget(-recorder)) { $self->record_one_step($id, $method, \@coords, $method, [$self->$method($id)] ); # $self->raise($id, "all"); } return $self->$method($id, @coords); } sub item_raise { my ($self, $id, @args) = @_; my ($method) = "SUPER::raise"; if ($self->cget(-recorder)) { $self->record_one_step($id, $method, \@args, "SUPER::lower", \@args ); } return $self->$method($id, @args); } sub item_lower { my ($self, $id, @args) = @_; my ($method) = "SUPER::lower"; if ($self->cget(-recorder)) { $self->record_one_step($id, $method, \@args, "SUPER::raise", \@args ); } return $self->$method($id, @args); } sub ceiling { my ($self, $level, $now) = @_; $now = $self->{"#now"} unless defined $now; my ($h) = $self->{"#history"}; my ($maxpos) = $self->total_marks(0); return $maxpos if $now >= $maxpos or $level > $self->{-maxlevel}; while ($now < $maxpos and $h->[$now-1]{mark}[$level] == $h->[$now]{mark}[$level] ) { ++$now; } return $now; } sub floor { my ($self, $level, $now) = @_; $now = $self->{"#now"} unless defined $now; my ($h) = $self->{"#history"}; return 0 if $now <= 0 or $level > $self->{-maxlevel}; while ($now > 0 and $h->[$now-1]{mark}[$level] == $h->[$now]{mark}[$level] ) { --$now; } return $now; } sub find_forward_stop { my ($self, $level) = @_; return $self->ceiling($level, $self->{"#now"} + 1); } sub find_backward_stop { my ($self, $level) = @_; return $self->floor($level, $self->{"#now"} - 1); } sub relative_mark { my ($self, $mark) = @_; my ($level, $result); my ($h, $now) = ($self->{"#history"}, $mark->[0]); for ($level=0; $level<$#$mark; ++$level) { push @$result, $mark->[$level] - $h->[ $self->floor($level+1, $now) ]{mark}[$level] } push @$result, $mark->[$#$mark]; return @$result; } sub time_seek { my ($self, $target) = @_; my ($n) = $self->total_marks(0); die "target $target out of range (0..$n)" if ($target < 0 or $target > $n); my ($h) = ($self->{"#history"}); while ($self->{"#now"} < $target) { my ($step) = $h->[$self->{"#now"}]{forward}; no strict "refs"; # see perlobj(1) and search for the 2nd "WARNING" my ($method) = $step->{method}; $self->$method($step->{id}, @{$step->{args}}) if $method; use strict "refs"; ++$self->{"#now"}; } while ($self->{"#now"} > $target) { my ($step) = $h->[$self->{"#now"}]{backward}; no strict "refs"; # see perlobj(1) and search for the 2nd "WARNING" my ($method) = $step->{method}; $self->$method($step->{id}, @{$step->{args}}) if $method; use strict "refs"; --$self->{"#now"}; } } sub find_position { my ($self, $level, $target) = @_; die "level $level out of range (0..$self->{-maxlevel})" unless ($level >= 0 and $level <= $self->{-maxlevel}); my ($n) = $self->total_marks(0); die "target $target at level $level out of range (0..$n)" unless ($target >= 0 and $target <= $n); my ($h) = ($self->{"#history"}); my ($i); for ($i=0; $i<=$n and $h->[$i]{mark}[$level]<$target; ++$i) { } return $i; } sub bbox_update { my ($self) = @_; # $self->{canvas}->idletasks(); my (@t) = $self->bbox("all"); $self->{"#bbox"}[2] = $t[2] if (defined $t[2] and $t[2] > $self->{"#bbox"}[2]); $self->{"#bbox"}[3] = $t[3] if (defined $t[3] and $t[3] > $self->{"#bbox"}[3]); } sub bbox_ever { my ($self) = @_; return @{$self->{"#bbox"}}; } sub recorder { my ($self, $state) = @_; $self->{-recorder} = $state; $self->bbox_update(); $self->configure(-scrollregion=> [ $self->bbox_ever() ]); # bug! This should really be done after # every ->create*() call and every ->coords() call } if ($0 =~ /RecCanvas.pm$/) { # being tested as a stand-alone program, so run test code. use Tk; my ($main, $mb, $rc, $b); $main = MainWindow->new(); $mb = $main->Frame(); $mb->pack(-side=>"top", -fill=>"both"); $rc = $main->Scrolled("RecCanvas", -scrollbars=>"osow", -elevation=>4, -maxlevel=>6, -width=>300, -height=>200); $rc->pack(-expand=>"yes", -fill=>"both"); $b->[0] = $mb->Button(-text=>"|<-", -command=> sub { $rc->time_seek($rc->find_backward_stop(6)); }); $b->[1] = $mb->Button(-text=>"<<-", -command=> sub { $rc->time_seek($rc->find_backward_stop(5)); }); $b->[2] = $mb->Button(-text=>"<--", -command=> sub { $rc->time_seek($rc->find_backward_stop(4)); }); $b->[3] = $mb->Button(-text=>"-->", -command=> sub { $rc->time_seek($rc->find_forward_stop(4)); }); $b->[4] = $mb->Button(-text=>"->>", -command=> sub { $rc->time_seek($rc->find_forward_stop(5)); }); $b->[5] = $mb->Button(-text=>"->|", -command=> sub { $rc->time_seek($rc->find_forward_stop(6)); }); $b->[0]->pack(@{$b}[1..5], -side=>"left", -fill=>"both"); my ($i, $j, $block); for ($i=0; $i<4; ++$i) { for ($j=0; $j<4; ++$j) { $block->[$i][$j] = $rc->createRectangle( $j*40+20, $i*30+15, $j*40+40, $i*30+30); $rc->itemconfigure($block->[$i][$j], -outline=>"hidden"); } } $rc->configure(-recorder=>1); for ($i=0; $i<4; ++$i) { for ($j=0; $j<4; ++$j) { $rc->itemconfigure($block->[$i][$j], -outline=>"cyan"); $rc->itemconfigure($block->[$i][$j], -outline=>"yellow"); $rc->itemconfigure($block->[$i][$j], -outline=>"magenta"); $rc->set_mark(0); } $rc->set_mark(1); } $rc->configure(-recorder=>0); print "-maxlevel: ", $rc->cget(-maxlevel), "; -elevation: ", $rc->cget(-elevation), "\n"; MainLoop(); } 1; __END__ =head1 NAME Tk::RecCanvas - A Canvas widget with simple recording capability =head1 SYNOPSIS I<$reccanvas> = I<$parent>-EB(?I?); =head1 DESCRIPTION An RecCanvas behaves much like a Canvas, but also has minimal ability to record certain configuration changes of canvas items. Specifically, B and B are recorded, but item creation and destruction are I. =head1 ADVERTISED SUBWIDGETS None. =head1 OPTIONS =head1 METHODS =head1 AUTHOR B ckhung AT cyut DOT edu DOT tw This code is distributed under the same terms as Perl. =cut