# 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>-E<gt>B<RecCanvas>(?I<options>?);
=head1 DESCRIPTION
An RecCanvas behaves much like a Canvas, but also has minimal
ability to record certain configuration changes of canvas items.
Specifically, B<itemconfigure> and B<coords> are recorded,
but item creation and destruction are I<not>.
=head1 ADVERTISED SUBWIDGETS
None.
=head1 OPTIONS
=head1 METHODS
=head1 AUTHOR
B<Chao-Kuei Hung> ckhung AT cyut DOT edu DOT tw
This code is distributed under the same terms as Perl.
=cut
syntax highlighted by Code2HTML, v. 0.9.1