package Tk::Scope; use strict; use Tk; use Tk::widgets qw(Canvas Scrollbar DialogBox); use Audio::Data; use base qw(Tk::Derived Tk::Canvas); use File::Temp qw(tempfile); Construct Tk::Widget 'Scope'; sub ClassInit { my ($class,$mw) = @_; $mw->bind($class,'<4>',[Wheel => -1, Ev('x')]); $mw->bind($class,'<5>',[Wheel => 1, Ev('x')]); $mw->bind($class,'<1>',[Cursor => 1,Ev('x')]); $mw->bind($class,'',[Cursor => 2,Ev('x')]); $mw->bind($class,'',[Range => Ev('x'),-rangecmd => 1,2,Ev('s')]); $mw->bind($class,'<3>',[Cursor => 'z1',Ev('x')]); $mw->bind($class,'',[Cursor => 'z2',Ev('x')]); $mw->bind($class,'',[Range => Ev('x'),-zoomcmd => 'z1','z2',Ev('s')]); $mw->bind($class,'','scheduleRedisplay'); $mw->bind($class,'','Print'); return $class; } sub Wheel { my ($c,$n,$x) = @_; $c->xview(scroll => $n*0.1, 'pages'); } sub doZoom { my ($c,$t1,$t2,$s) = @_; # warn "Zoom '$s' $t1 -> $t2\n"; if ($s =~ /Shift/) { my $s = $c->start; my $e = $c->end; my $w = $e-$s; # width now my $dt = $t2-$t1; my $l1 = $w*$w/$dt; my $s1 = $s - ($t1-$s)*$w/$dt; $s1 = 0 if ($s1 < 0); my $e1 = $s1+$l1; $e1 = $c->xmax if ($e1 > $c->xmax); $c->start($s1); $c->end($e1); } else { $c->start($t1); $c->end($t2); } } sub Range { my ($c,$x,$callback,$n,$m,@args) = @_; $m = 1 if ($m eq '2' && !$c->cget('-range1')); $c->Cursor($m,$x); unless($n =~ /^\d+$/) { $c->itemconfigure($c->{"c$n"},-state => 'hidden'); $c->itemconfigure($c->{"c$m"},-state => 'hidden'); } if (($n eq '1' && !$c->cget('-range1')) || ($c->{"cursor$n"} != $c->{"cursor$m"})) { my ($t1,$t2) = ($c->{"cursor$n"},$c->{"cursor$m"}); ($t2,$t1) = ($t1,$t2) if $t1 > $t2; $c->Callback($callback => $t1,$t2,@args); } } sub Cursor { my ($c,$n,$x) = @_; $c->Tk::focus; $n = 1 if ($n eq '2' && !$c->cget('-range1')); if (@_ > 2) { $c->{"cursor$n"} = $c->x2val($x); $c->Callback(-command => "cursor$n"); } unless (exists $c->{"c$n"}) { my @args; push(@args,-dash => '.') unless $n =~ /^\d+$/; $c->{"c$n"} = $c->create(line => [0,0,0,0],@args); } if ($c->{xmax} && defined $c->{"cursor$n"}) { $x = $c->val2x($c->{"cursor$n"}); my $w = $c->Width; my $h = $c->Height; if ($x >= 0 && $x <= $w) { $c->coords($c->{"c$n"},[$x,0,$x,$h]); $c->itemconfigure($c->{"c$n"},-state => 'normal'); } else { $c->itemconfigure($c->{"c$n"},-state => 'hidden'); } } else { $c->itemconfigure($c->{"c$n"},-state => 'hidden'); } } sub Populate { my ($sc,$args) = @_; $sc->{trace} = {}; $sc->{xa} = $sc->create(line => [0,0,0,0]); $sc->{ya} = $sc->create(line => [0,0,0,0]); $sc->ConfigSpecs( -yscale => ['METHOD','yscale','Yscale',-1.0], -start => ['METHOD','start','Start',0.0], -end => ['METHOD','end','End',undef], -xmax => ['METHOD','xmax','Xmax',undef], -access => ['PASSIVE','access','Access','FETCH'], -command => ['CALLBACK','callback','Callback',undef], -zoomcmd => ['CALLBACK','zoom','Zoom','doZoom'], -rangecmd => ['CALLBACK','range','Range',undef], -range1 => ['PASSIVE','range1','Range1','1'], DEFAULT => [$sc], ); } sub xscale { my $sc = shift; my $t0 = $sc->{start} || 0; my $t1 = $sc->{end} || 0; my $w = $sc->Width; return ($t1-$t0)/$w; } sub x2val { my ($sc,$x) = @_; return $sc->start + $x*$sc->xscale; } sub val2x { my ($sc,$t) = @_; return ($t-$sc->start)/$sc->xscale; } sub scheduleRedisplay { my ($sc,@args) = @_; unless ($sc->{redisplay}) { $sc->{redisplay} = $sc->afterIdle([Redisplay => $sc, @args]); } } sub traces { my $sc = shift; return keys %{$sc->{trace}}; } sub trace { my ($sc,%args) = @_; my $id; if (exists $args{-data}) { my $data = delete $args{-data}; $id = $sc->create('line',[0,0,0,0],%args); $sc->traceconfigure($id,-data => $data); } else { $id = $sc->create('line',[0,0,0,0],%args); } return $id; } sub traceconfigure { my ($sc,$id,%args) = @_; if (exists $args{-data}) { my $data = delete $args{-data}; $sc->{trace}{$id} = $data; $sc->scheduleRedisplay('data'); } $sc->itemconfigure($id,%args) if keys %args; } sub tracecget { my ($sc,$id,$key) = @_; return $sc->{trace}{$id} if $key eq '-data'; return $sc->itemcget($id,$key); } sub attrib { my ($sc,$key,$val) = @_; if (@_ > 2) { # warn "$key = $val\n"; $sc->{$key} = $val; $sc->scheduleRedisplay($key); } return $sc->{$key}; } foreach my $meth (qw(yscale start end xmax cursor1 cursor2)) { no strict 'refs'; my $key = $meth; *$meth = sub { shift->attrib($key => @_) }; } sub audio { my ($sc,$t1,$t2,@tr) = @_; (@tr) = keys %{$sc->{trace}} unless @tr; my @result; foreach my $tr (@tr) { my $data = $sc->{trace}{$tr}->timerange($t1,$t2); return $data unless wantarray; push @result,$data; } return @result; } sub Redisplay { my ($sc,$why) = @_; delete $sc->{redisplay}; # warn "Redisplay $why\n"; my $w = $sc->Width; my $h = $sc->Height/2; my $dur = $sc->{xmax}; $sc->Callback(-xscrollcommand => $sc->start/$dur,$sc->end/$dur) if $dur; foreach my $n (1,2) { $sc->Cursor($n) if exists $sc->{"cursor$n"}; } foreach my $tr (keys %{$sc->{trace}}) { my $ys = $sc->{yscale}; my $data = $sc->{trace}{$tr}; my $rate = $data->samples/$sc->{xmax}; next unless $rate; my @coord; my $acc = $sc->cget('-access'); my $ds = ($sc->x2val(1)-$sc->start)*$rate; my $yb = $h; if ($ds > 1) { # several samples in one pixel # warn "$ds = max/min\n"; unless (defined $ys) { my ($max,$min) = $data->bounds($sc->x2val(0),$sc->x2val($w-1)); $ys = ($max == $min) ? -1 : -2/($max-$min); $yb = 2*$h-($min*$h*$ys); } for my $x (0..$w-1) { my $t = $sc->x2val($x); my $t1 = $sc->x2val($x+1); my $samp = int($t*$rate); my ($max,$min); if ($acc eq 'FETCH') { ($max,$min) = $data->bounds($t,$t1); $max = $max*$h*$ys+$yb; $min = $min*$h*$ys+$yb; } else { while ($t < $t1) { my $v = $data->$acc($samp); $v = 0 unless defined $v; my $y = $v*$h*$ys+$yb; $max = $y if (!defined($max) || $y > $max); $min = $y if (!defined($min) || $y < $min); $samp++; $t += 1/$rate; } } push(@coord,$x,$max,$x,$min); } } else { # several pixels in one sample # warn "$ds = lines\n"; unless (defined $ys) { my ($max,$min); for (my $t = $sc->start; $t < $sc->end; $t += 1/$rate) { my $samp = int($t*$rate); my $v = $data->$acc($samp); $v = 0 unless defined $v; $max = $v if (!defined($max) || $v > $max); $min = $v if (!defined($min) || $v < $min); } $ys = ($max == $min) ? -1 : -2/($max-$min); $yb = 2*$h-($min*$h*$ys); } for (my $t = $sc->start; $t < $sc->end; $t += 1/$rate) { my $samp = int($t*$rate); my $v = $data->$acc($samp); $v = 0 unless defined $v; my $y = $v*$h*$ys+$yb; push(@coord,$sc->val2x($t),$y); } } $sc->coords($tr,\@coord); } $sc->Callback(-command => $why); } sub scroll { my ($sc,$am,$what) = @_; # warn "scroll $am $what\n"; my $new = $sc->x2val($am*(($what eq 'pages') ? $sc->Width/2 : 1)); $sc->moveto($new/$sc->xmax); } sub moveto { my ($sc,$frac) = @_; # warn "moveto $frac\n"; my $w = $sc->end - $sc->start; my $s = $sc->xmax*$frac; my $e = $s+$w; if ($e > $sc->xmax) { $e = $sc->xmax; $s = $e-$w; } if ($s < 0) { $s = 0; $e = $s+$w; } $sc->start($s); $sc->end($e); } sub xview { my ($sc,$cmd,@args) = @_; $sc->$cmd(@args); } my %page_sizes; sub page_sizes { unless (keys %page_sizes) { my @list; my ($w,$h) = (297,420); for my $size (3..5) { $page_sizes{"A$size"} = [$w,$h]; ($h,$w) = ($w,$h/2); } } return \%page_sizes; } sub printers { my @list; if (open(my $fh,"/etc/printcap")) { while (<$fh>) { s/^\s+//; s/#.*$//; push(@list,$1) if (/^(\w+)/); } close($fh); } else { warn "Cannot open /etc/printcap:$!"; } return @list; } sub Print { my ($c) = @_; my $d = $c->DialogBox(-buttons => [qw(Ok Cancel)], -title => 'Print Options', -popover => 'cursor', -popanchor => 'nw'); my $ps = page_sizes(); my $psize = 'A4'; my $mode = 'color'; my $path = 'plot.ps'; my $what = 'All'; my $printer = 'File'; my @lopts = (-anchor => 'e', -justify => 'right'); Tk::grid( $d->add('Label',-text => 'Print:',@lopts), # $d->add('Optionmenu', -variable => \$what, -options => ['Window','All']), $d->add('Label',-text => 'Paper Size:',@lopts), $d->add('Optionmenu', -variable => \$psize, -options => [sort keys %$ps]), -sticky => 'nsew' ); Tk::grid( $d->add('Label',-text => 'Colour mode:',,@lopts), $d->add('Optionmenu', -variable => \$mode, -options => [qw(color gray mono)]), $d->add('Label',-text => 'Printer:',@lopts), $d->add('Optionmenu', -variable => \$printer, -options => [File => printers()]), -sticky => 'nsew' ); Tk::grid($d->add('Label',-text => 'File Name:',@lopts), $d->add('Entry', -textvariable => \$path, -width => 30),'-', $d->add('Button', -text => 'Browse ...', -command => sub { $path = $d->getSaveFile( -initialfile => $path, -initialdir => getcwd(), -filetypes => [ ['PostScript Files' => ['.ps']], ['All Files', '*'] ]) } ), -sticky => 'nsew' ); my $dst; do { my $opt = $d->Show(); return if $opt eq 'Cancel'; if ($printer eq 'File') { unless (length($path) && open($dst,">$path")) { $c->messagebox(-text => "Cannot open $path:$!"); } } else { ($dst,$path) = tempfile(); } } until (defined($dst) && fileno($dst)); my %opt = (-colormode => $mode); if ($what eq 'All') { @opt{'-x','-y','-width','-height'} = $c->bbox('all'); } else { @opt{'-x','-y','-width','-height'} = ($c->canvasx(0), $c->canvasy(0), $c->canvasx($c->width), $c->canvasy($c->height)); } $opt{'-width'} -= $opt{'-x'}; $opt{'-height'} -= $opt{'-y'}; # PS origin is normally SW so avoid deltaX, deltaY $opt{'-pageanchor'} = 'sw'; # Get raw page size - we are working in mm my ($pw,$ph) = @{$ps->{$psize}}; # Allow a margin on each edge my $margin = 25.4*0.25; # 1/4 inch on each edge $pw -= 2*$margin; $ph -= 2*$margin; $opt{-pagey} = $margin; $opt{-pagey} = $margin; # Decide if it fits better rotated $opt{'-rotate'} = ($opt{'-width'} > $opt{'-height'}) ? 1 : 0; if ($opt{-rotate}) { # x is bigger so rotate, pw/ph are swapped ($pw,$ph) = ($ph,$pw); # and we have to shift it right to allow for rotate as ->postscript # method does not take that into account the scaled image will be at # most the "height" of the paper $opt{-pagex} += $ph; } # Now compute scale. pw/ph correspond to x/y size of space my $xs = $pw / $opt{-width}; my $ys = $ph / $opt{-height}; # Core tk prefers -pagewidth to -pageheight if both specified # So only add the one which corresponds to smallest scale so # that whole image fits if ($xs < $ys) { $opt{-pagewidth} = $pw; } else { $opt{-pageheight} = $ph; } # Now we have finished doing calculations append unit marker for core tk. foreach my $key (qw(-pagewidth -pageheight -pagex -pagey)) { $opt{$key} .= 'm' if exists $opt{$key}; } # use Data::Dumper; # warn Dumper(\%opt); # pseudo_code($c,%opt); my ($fh,$temp) = tempfile(); $c->postscript(-file => $temp, %opt); # Now edit PostScript to get thin lines # 1 pixel at 300dpi is 72/300 points my $t = 72/300; while (<$fh>) { if (/^(\s*(\d+(\.\d+))){2}\s+scale/) { warn "scale $2\n"; $t /= $2; } else { s/^(\s*)(\d+)(\s+)setlinewidth/"$1".$t*$2."$3setlinewidth"/e; } print $dst $_; } close($fh); close($dst); unlink($temp); if ($printer ne 'File') { system(lpr => "-P$printer",$path); unlink($path); } } 1; __END__