#!/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;