#!/usr/bin/perl -w # # Copyright (C) 2003 by the gtk2-perl team (see the file AUTHORS for the full # list) # # This library is free software; you can redistribute it and/or modify it under # the terms of the GNU Library General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This library is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for # more details. # # You should have received a copy of the GNU Library General Public License # along with this library; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA. # # $Header: /cvsroot/gtk2-perl/gtk2-perl-xs/Gtk2/examples/histogramplot.pl,v 1.8 2003/10/19 04:42:44 muppetman Exp $ # # originally written in C by muppet in 2001 or 2002, i can't remember. # ported from C to gtk2-perl 2003 by muppet package Histogram::Plot; use Gtk2; use warnings; use strict; use Data::Dumper; use constant FALSE => 0; use constant TRUE => 1; use constant MIN_CHART_WIDTH => 256; use constant MIN_CHART_HEIGHT => 100; my %drag_info; use constant DRAG_PAD => 2; sub screen_to_threshold { my ($plot, $sx) = @_; my $val = ($sx - $plot->{chartleft}) * 256 / $plot->{chartwidth}; return $val < 0 ? 0 : $val > 255 ? 255 : $val; } sub threshold_to_screen { $_[1] / 256.0 * $_[0]->{chartwidth} + $_[0]->{chartleft} } # # Glib::Objects are special; they're not normal perl objects (although # the bindings go out of their way to make them act like it). # # if you just want to add a new function for yourself to a Gtk2::DrawingArea, # the stuff we're about to get into is not strictly necessary; you could just # re-bless the object reference into the decendent class and add an @ISA for # it, like normal perl. # # however, adding signals, properties, or virtual function overrides to a # GObject-based class requires fiddling with a GObjectClass structure # specific to that subclass. if you added a new property to a re-blessed # Glib::Object, *all* instances of that reblessed object's GObject parent # would have the new property! that's because you didn't create a new # GObjectClass for that new subclass. # # in order to create a new type to which you can add signals and properties, # and which will be indistinguishable from "normal" GObjects at the C level # (which means you can pass it to other gtk functions), you need to # register your subclass with the Glib::Type subsystem. # # here, we're registering the current package as a new subclass of # Gtk2::DrawingArea, and in the process adding a signal and a few # object properties. # use Glib::Object::Subclass 'Gtk2::DrawingArea', signals => { # # create a new signal... # threshold_changed => { method => 'do_threshold_changed', flags => [qw/run-first/], return_type => undef, # void return param_types => [], # instance and data are automatic }, # # override some built-ins... note that for this to work # there has to be a signal to go along with the virtual # function you want to override... # # i chose do_size_request to keep from having the normal # size_request method being called. size_request => \&do_size_request, # just to show it off... you can use names, but you have # to use a qualified name, or it looks in the current package # at runtime, not setup time. expose_event => __PACKAGE__.'::expose_event', configure_event => \&configure_event, motion_notify_event => \&motion_notify_event, button_press_event => \&button_press_event, button_release_event => \&button_release_event, }, properties => [ Glib::ParamSpec->double ('threshold', 'Threshold', 'Diving line between above and below', 0.0, 255.0, 127.0, [qw/readable writable/]), Glib::ParamSpec->boxed ('histogram', 'Histogram Data', 'Array reference containing histogram data', 'Glib::Scalar', [qw/readable writable/]), Glib::ParamSpec->boolean ('continuous', 'Continuous updates', 'Emit the threshold_changed signal on every mouse event during drag, rather than just on release', FALSE, [qw/readable writable/]), ], ; # # at the lowest level, new Glib::Objects are created by Glib::Object::new. # that function creates the instance and calls the instance initializers # for all classes in the object's lineage, from the parent to the descendant. # if there's any setup you would need to do in a constructor, it goes here. # sub INIT_INSTANCE { my $plot = shift; warn "INIT_INSTANCE $plot"; $plot->{threshold} = 0; $plot->{histogram} = [ 0..255 ]; $plot->{pixmap} = undef; $plot->{th_gc} = undef; $plot->{dragging} = FALSE; $plot->{continuous} = FALSE; $plot->{origin_layout} = $plot->create_pango_layout ("0.0%"); $plot->{maxval_layout} = $plot->create_pango_layout ("100.0%"); $plot->{current_layout} = $plot->create_pango_layout ("0"); $plot->{maxscale_layout} = $plot->create_pango_layout ("255"); $plot->{minscale_layout} = $plot->create_pango_layout ("0"); $plot->{max} = 0; $plot->{chartwidth} = 0; $plot->{chartleft} = 0; $plot->{bottom} = 0; $plot->{height} = 0; $plot->set_events ([qw/exposure-mask leave-notify-mask button-press-mask button-release-mask pointer-motion-mask pointer-motion-hint-mask/]); } # # whenever anybody tries to get the value of a gobject property belonging # to this class, this function will be called. note that this call # signature is different from the C version -- here we return the requested # value. # sub GET_PROPERTY { my ($plot, $pspec) = @_; if ($pspec->get_name eq 'threshold') { return $plot->{threshold}; } elsif ($pspec->get_name eq 'histogram') { return $plot->{histogram}; } elsif ($pspec->get_name eq 'continuous') { return $plot->{continuous}; } } # # whenever anybody tries to set the value of a gobject property belonging # to this class, this function will be called. the provided Glib::Object::Base # method just stores the value in a hash key, but here we need to do other # bits of work when a value is changed. # # note that this one also is changed from the C call signature; the order # of the arguments has been swizzled to be more consistent with GET_PROPERTY. # sub SET_PROPERTY { my ($plot, $pspec, $newval) = @_; if ($pspec->get_name eq 'threshold') { $plot->set_plot_data ($newval, ()); } elsif ($pspec->get_name eq 'histogram') { $plot->set_plot_data (undef, @$newval); } elsif ($pspec->get_name eq 'continuous') { $plot->{continuous} = $newval; } } sub calc_dims { my $plot = shift; my $context = $plot->{origin_layout}->get_context; my $fontdesc = $context->get_font_description; my $metrics = $context->get_metrics ($fontdesc, undef); $plot->{textwidth} = 5 * $metrics->get_approximate_digit_width / Gtk2::Pango->scale; #PANGO_SCALE; $plot->{textheight} = ($metrics->get_descent + $metrics->get_ascent) / Gtk2::Pango->scale; #PANGO_SCALE; $plot->{chartleft} = $plot->{textwidth} + 2; $plot->{chartwidth} = $plot->allocation->width - $plot->{chartleft}; $plot->{bottom} = $plot->allocation->height - $plot->{textheight} - 3; $plot->{height} = $plot->{bottom}; } # this gets called when the widget's parent container wants to know # how much space we want. it's important to note that this sub will be # called from deep within the gtk library, not from perl code, which is # why it had to be implemented as a class closure override. # we modify the requisition passed to us. sub do_size_request { my ($plot, $requisition) = @_; warn "in class override for $_[0]\::do_size_request"; $requisition->width ($plot->{textwidth} + 2 + MIN_CHART_WIDTH); $requisition->height ($plot->{textheight} + MIN_CHART_HEIGHT); # chain up to the parent class. shift->signal_chain_from_overridden (@_); } sub expose_event { my ($plot, $event) = @_; $plot->window->draw_drawable ($plot->style->fg_gc($plot->state), $plot->{pixmap}, $event->area->x, $event->area->y, $event->area->x, $event->area->y, $event->area->width, $event->area->height); return FALSE; } sub configure_event { my ($plot, $event) = @_; $plot->{pixmap} = Gtk2::Gdk::Pixmap->new ($plot->window, $plot->allocation->width, $plot->allocation->height, -1); # same depth as window # update dims $plot->calc_dims; $plot->histogram_draw; return TRUE; } sub draw_th_marker { my ($plot, $w, $draw_text) = @_; if (!$plot->{th_gc}) { $plot->{th_gc} = Gtk2::Gdk::GC->new ($plot->{pixmap}); $plot->{th_gc}->copy ($plot->style->fg_gc ($plot->state)); $plot->{th_gc}->set_function ('invert'); } $w->draw_line ($plot->{th_gc}, $plot->threshold_to_screen ($plot->{threshold}), 0, $plot->threshold_to_screen ($plot->{threshold}), $plot->{bottom}); $plot->{current_layout}->set_text (sprintf '%d', $plot->{threshold}); my ($textwidth, $textheight) = $plot->{current_layout}->get_pixel_size; $plot->{marker_textwidth} = $textwidth; # erase text $w->draw_rectangle ($plot->style->bg_gc($plot->state), TRUE, $plot->threshold_to_screen ($plot->{threshold}) - $plot->{marker_textwidth} - 1, $plot->{bottom} + 1, $plot->{marker_textwidth} + 1, $textheight); $w->draw_layout ($plot->{th_gc}, $plot->threshold_to_screen ($plot->{threshold}) - $plot->{marker_textwidth}, $plot->{bottom} + 1, $plot->{current_layout}) if $draw_text; } # # the user can click either very near the vertical line of the marker # or on (actually in the bbox of) the marker text. # sub marker_hit { my ($plot, $screen_x, $screen_y) = @_; my $screen_th = $plot->threshold_to_screen ($plot->{threshold}); if ($screen_y > $plot->{bottom}) { # check for hit on text if ($screen_x > $screen_th - $plot->{marker_textwidth} && $screen_x <= $screen_th) { return $screen_th; } } else { # check for hit on line if ($screen_x > $screen_th - DRAG_PAD && $screen_x < $screen_th + DRAG_PAD) { return $screen_th; } } return undef; } sub button_press_event { my ($plot, $event) = @_; return FALSE if ($event->button != 1 || not defined $plot->{pixmap}); my $sx = $plot->marker_hit ($event->x, $event->y); return FALSE unless defined $sx; # erase the previous threshold line from the pixmap... $plot->{threshold_back} = $plot->{threshold}; $plot->draw_th_marker ($plot->{pixmap}, FALSE); $plot->window->draw_drawable ($plot->style->fg_gc($plot->state), $plot->{pixmap}, $plot->threshold_to_screen ($plot->{threshold}) - $plot->{marker_textwidth}, 0, $plot->threshold_to_screen ($plot->{threshold}) - $plot->{marker_textwidth}, 0, $plot->{marker_textwidth} + 1, $plot->allocation->height); # and draw the new one on the window. $plot->draw_th_marker ($plot->window, TRUE); $plot->{dragging} = TRUE; $drag_info{offset_x} = $plot->threshold_to_screen ($plot->{threshold}) - $event->x; return TRUE; } sub button_release_event { my ($plot, $event) = @_; return FALSE if ($event->button != 1 || !$plot->{dragging} || not defined $plot->{pixmap}); # erase the previous threshold line from the window... $plot->draw_th_marker ($plot->window, FALSE); $plot->{threshold} = $plot->screen_to_threshold ($event->x + $drag_info{offset_x}); # and draw the new one on the pixmap. $plot->draw_th_marker ($plot->{pixmap}, TRUE); $plot->window->draw_drawable ($plot->style->fg_gc ($plot->state), $plot->{pixmap}, 0, 0, 0, 0, $plot->allocation->width, $plot->allocation->height); $plot->{dragging} = FALSE; # let any listeners know that if the threshold has changed $plot->signal_emit ("threshold-changed") if $plot->{threshold_back} != $plot->{threshold} and not $plot->{continuous}; return TRUE; } my $sizer; sub motion_notify_event { my ($plot, $event) = @_; my ($x, $y, $state); if ($event->is_hint) { (undef, $x, $y, $state) = $event->window->get_pointer; } else { $x = $event->x; $y = $event->y; $state = $event->state; } if ($plot->{dragging}) { return FALSE if (!(grep /button1-mask/, @$state) || not defined $plot->{pixmap}); $plot->draw_th_marker ($plot->window, FALSE); $x += $drag_info{offset_x}; # confine to valid region my $t = $plot->screen_to_threshold ($x); $x = $plot->threshold_to_screen (0) if $t < 0; $x = $plot->threshold_to_screen (255) if $t > 255; $plot->{threshold} = $plot->screen_to_threshold ($x); $plot->draw_th_marker ($plot->window, TRUE); $plot->signal_emit ("threshold-changed") if $plot->{continuous}; } else { my $c = undef; my $sx = $plot->marker_hit ($x, $y); if (defined $sx) { $sizer = Gtk2::Gdk::Cursor->new ('GDK_SB_H_DOUBLE_ARROW') if not defined $sizer; $c = $sizer; } $plot->window->set_cursor ($c); } return TRUE; } sub histogram_draw { my $plot = shift; my $gc = $plot->style->fg_gc ($plot->state); # erase (the hard way) $plot->{pixmap}->draw_rectangle ($plot->style->bg_gc ($plot->state), TRUE, 0, 0, $plot->allocation->width, $plot->allocation->height); if ($plot->{max} != 0 && scalar(@{$plot->{histogram}})) { ##GdkPoint points[256+2]; my @hist = @{ $plot->{histogram} }; my @points = (); for (my $i = 0; $i < 256; $i++) { push @points, $i/256.0 * $plot->{chartwidth} + $plot->{chartleft}, $plot->{bottom} - $plot->{height} * $hist[$i] / $plot->{max}; } $plot->{pixmap}->draw_polygon ($gc, TRUE, @points, $plot->allocation->width, $plot->{bottom} + 1, $plot->{chartleft}, $plot->{bottom} + 1); } # mark threshold # should draw this after the scale... draw_th_marker ($plot, $plot->{pixmap}, TRUE); # the annotations $plot->{pixmap}->draw_line ($gc, 0, 0, $plot->{chartleft}, 0); $plot->{pixmap}->draw_line ($gc, 0, $plot->{bottom}, $plot->{chartleft}, $plot->{bottom}); $plot->{pixmap}->draw_line ($gc, $plot->{chartleft}, $plot->{bottom}, $plot->{chartleft}, $plot->{bottom} + $plot->{textheight} + 1); $plot->{pixmap}->draw_line ($gc, $plot->allocation->width - 1, $plot->{bottom}, $plot->allocation->width - 1, $plot->{bottom} + $plot->{textheight} + 1); $plot->{pixmap}->draw_layout ($gc, $plot->{chartleft} - (1 + $plot->{textwidth}), 1, $plot->{maxval_layout}); $plot->{pixmap}->draw_layout ($gc, $plot->{chartleft} - (1 + $plot->{textwidth}), $plot->{bottom} - 1 - $plot->{textheight}, $plot->{origin_layout}); $plot->{pixmap}->draw_layout ($gc, $plot->{chartleft} + 2, $plot->{bottom} + 1, $plot->{minscale_layout}); } # # change the data displayed in the window, with all the necessary # work to get it properly updated. # # @threshold: new threshold. ignored if undef. # @histogram: new histogram. if not empty, copy to the histwin's # internal histogram cache. MUST be 256 items long. # sub set_plot_data { my ($plot, $threshold, @hist) = @_; $plot->{threshold} = $threshold if defined $threshold; if (@hist) { my $total = 0; my $max = 0; for (my $i = 0; $i < 256; $i++) { $total += $hist[$i]; $max = $hist[$i] if $hist[$i] > $max; } $plot->{max} = $max; $plot->{histogram} = \@hist; $plot->{maxval_layout}->set_text ( sprintf "%4.1f%%", (100.0 * $plot->{max}) / $total ); } # update dims since text may have changed $plot->calc_dims; # if the pixmap doesn't exist, we haven't been put on screen yet. # don't bother drawing anything. if ($plot->{pixmap}) { $plot->histogram_draw; $plot->queue_draw; } } sub do_threshold_changed { warn "default threshold handler"; } ################ # # public methods # # we inherit new from Glib::Object::Subclass, and all the stuff we'd need # to get to is available as object properties, so, well, there's no work # to do here. :-) # ########################################################################## # now let's take that code for a test drive... # package main; use Gtk2 qw/-init -locale/; use constant TRUE => 1; use constant FALSE => 0; my $window = Gtk2::Window->new; $window->signal_connect (delete_event => sub { Gtk2->main_quit; FALSE }); my $vbox = Gtk2::VBox->new; $window->add ($vbox); $window->set_border_width (6); # # a nicely framed histogram plot with some cheesy data # my $plot = Histogram::Plot->new ( threshold => 64, histogram => [ map { sin $_/256*3.1415 } (0..255) ] ); my $frame = Gtk2::Frame->new; $vbox->pack_start ($frame, TRUE, TRUE, 0); $frame->add ($plot); $frame->set_shadow_type ('in'); # # a way to manipulate one of the properties... # my $check = Gtk2::CheckButton->new ("Continuous"); $vbox->pack_start ($check, FALSE, FALSE, 0); $check->set_active ($plot->get ('continuous')); $check->signal_connect (toggled => sub { $plot->set (continuous => $check->get_active); 1; }); # # do something fun when the threshold changes. # my $label = Gtk2::Label->new (sprintf "threshold: %.1f", $plot->get ('threshold')); $vbox->pack_start ($label, FALSE, FALSE, 0); $plot->signal_connect (threshold_changed => sub { $label->set_text (sprintf 'threshold: %d', $plot->get('threshold')); }); # # all systems go! # $window->show_all; Gtk2->main; # explicit clean up makes us see various messages on a debug build. undef $plot; undef $window;