# Copyright 2008, 2009, 2010, 2011, 2012 Kevin Ryde
# This file is part of Chart.
#
# Chart is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
# Chart 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with Chart. If not, see <http://www.gnu.org/licenses/>.
# gravity up,down,centre
# stick to start/end if already there
# which of upper/lower to grow
# set_page_range wait until first size_allocate to decide vpp,ppv
# choice keep vpp same and expand page, or keep page and adjust vpp
# integer ppv
package App::Chart::Gtk2::AdjScale;
use 5.008;
use strict;
use warnings;
use Glib::Ex::FreezeNotify;
use Glib::Ex::SignalIds;
use Gtk2;
use Gtk2::Ex::AdjustmentBits 47; # v.47 for set_empty()
use POSIX ();
use Scalar::Util;
use App::Chart;
use App::Chart::Glib::Ex::MoreUtils;
use App::Chart::Glib::Ex::TieWeakNotify;
use constant DEBUG => 0;
BEGIN {
Glib::Type->register_enum ('App::Chart::Gtk2::AdjScale::Gravity',
'lower',
'upper',
'centre');
}
use constant {
DEFAULT_GRAVITY => 'centre',
DEFAULT_ORIENTATION => 'horizontal',
};
use Glib::Object::Subclass
'Gtk2::Adjustment',
signals => { },
properties => [Glib::ParamSpec->double
('pixel-per-value',
'pixel-per-value',
'Blurb',
0, POSIX::DBL_MAX(), 0,
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->double
('value-per-pixel',
'value-per-pixel',
'Blurb',
0, POSIX::DBL_MAX(), 0,
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->double
('page-increment-fraction',
'page-increment-fraction',
'Blurb',
0, POSIX::DBL_MAX(), 0.85,
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->double
('step-increment-fraction',
'step-increment-fraction',
'Blurb',
0, POSIX::DBL_MAX(), 0.1,
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->object
('widget',
'widget',
'Blurb',
'Gtk2::Widget',
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->enum
('orientation',
'orientation',
'Blurb',
'Gtk2::Orientation',
DEFAULT_ORIENTATION,
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->enum
('gravity',
'gravity',
'Blurb',
'App::Chart::Gtk2::AdjScale::Gravity',
DEFAULT_GRAVITY,
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->boolean
('inverted',
'inverted',
'Blurb',
0, # default
Glib::G_PARAM_READWRITE),
Glib::ParamSpec->boolean
('logarithmic',
'logarithmic',
'Blurb',
0, # default
Glib::G_PARAM_READWRITE),
];
sub INIT_INSTANCE {
my ($self) = @_;
$self->{'value_per_pixel'} = 0;
$self->{'pixel_per_value'} = 0;
$self->{'gravity'} = DEFAULT_GRAVITY;
$self->{'orientation'} = DEFAULT_ORIENTATION;
}
sub SET_PROPERTY {
my ($self, $pspec, $newval) = @_;
my $pname = $pspec->get_name;
if (DEBUG) { print "AdjScale set $pname $newval\n"; }
$self->{$pname} = $newval; # per default GET_PROPERTY
if ($pname eq 'value_per_pixel') {
$self->set_value_per_pixel ($newval);
} elsif ($pname eq 'pixel_per_value') {
$self->set_pixel_per_value ($newval);
} elsif ($pname eq 'widget') {
my $widget = $newval;
App::Chart::Glib::Ex::TieWeakNotify->set ($self, $pname, $widget);
$self->{'widget_ids'} = $widget && Glib::Ex::SignalIds->new
($widget,
$widget->signal_connect (size_allocate => \&_do_size_allocate,
App::Chart::Glib::Ex::MoreUtils::ref_weak($self)));
_update_page_size ($self);
}
}
sub set_page_range {
my ($self, $p_lo, $p_hi) = @_;
if (DEBUG) { print "AdjScale set_page_range $p_lo $p_hi\n"; }
my $page = $p_hi - $p_lo;
my $pixels = _widget_pixels ($self);
my $ppv = $self->{'pixel_per_value'} = ($page == 0 ? 0 : $pixels / $page);
$self->{'value_per_pixel'} = ($ppv == 0 ? 0 : 1.0 / $ppv);
if (DEBUG) { print "AdjScale set_page_range $p_lo, $p_hi ",
"is $page in pixels $pixels, ppv $ppv vpp ",
$self->{'value_per_pixel'},"\n"; }
Gtk2::Ex::AdjustmentBits::set_maybe
($self,
lower => $p_lo,
upper => $p_hi,
page_size => $page,
page_increment => $self->get('page-increment-fraction') *$page,
step_increment => $self->get('step-increment-fraction') *$page,
value => $p_lo);
$self->notify ('pixel-per-value');
$self->notify ('value-per-pixel');
}
sub set_value_per_pixel {
my ($self, $vpp) = @_;
if (DEBUG) { print "AdjScale set_value_per_pixel $vpp\n"; }
$self->{'value_per_pixel'} = $vpp;
$self->{'pixel_per_value'} = ($vpp == 0 ? 0 : 1.0 / $vpp);
_update_page_size ($self);
$self->notify ('value-per-pixel');
$self->notify ('pixel-per-value');
}
sub set_pixel_per_value {
my ($self, $ppv) = @_;
if (DEBUG) { print "AdjScale set_pixel_per_value $ppv\n"; }
$self->{'pixel_per_value'} = $ppv;
$self->{'value_per_pixel'} = ($ppv == 0 ? 0 : 1.0 / $ppv);
_update_page_size ($self);
$self->notify ('value-per-pixel');
$self->notify ('pixel-per-value');
}
sub set_value_range {
my ($self, $lo, $hi) = @_;
$self->value ($lo);
my $pixels = _widget_pixels ($self);
$self->set_pixel_per_value ($pixels / ($hi - $lo));
$self->notify ('value');
$self->value_changed;
}
sub get_value_per_pixel {
my ($self) = @_;
return $self->{'value_per_pixel'};
}
sub get_pixel_per_value {
my ($self) = @_;
return $self->{'pixel_per_value'};
}
sub value_range {
my ($self) = @_;
my $value = $self->value;
return ($self->exp ($value),
$self->exp ($value + $self->page_size));
}
sub value_range_inc {
my ($self) = @_;
my $value = $self->value;
return (POSIX::floor ($self->exp ($value)),
POSIX::ceil ($self->exp ($value + $self->page_size)));
}
sub value_to_pixel {
my ($self, $v) = @_;
my $value = $self->value;
my $ppv = $self->{'pixel_per_value'};
if ($self->{'inverted'}) {
$ppv = -$ppv;
$value += $self->page_size - $self->{'value_per_pixel'};
}
return POSIX::floor (($self->log($v) - $value) * $ppv);
}
sub value_to_pixel_proc {
my ($self) = @_;
my $value = $self->value;
my $ppv = $self->{'pixel_per_value'};
if ($self->{'inverted'}) {
$ppv = -$ppv;
$value += $self->page_size - $self->{'value_per_pixel'};
}
if ($self->{'logarithmic'}) {
return sub {
return POSIX::floor ((log($_[0]) - $value) * $ppv);
};
} else {
return sub {
return POSIX::floor (($_[0] - $value) * $ppv);
};
}
}
sub pixel_to_value {
my ($self, $pixel) = @_;
my $vpp = $self->{'value_per_pixel'};
my $base = 0;
if ($self->{'inverted'}) {
$vpp = -$vpp;
$base = _widget_pixels($self) - 1;
}
return $self->exp ($self->value + ($pixel - $base) * $vpp);
}
sub pixel_to_value_proc {
my ($self) = @_;
my $value = $self->value;
my $vpp = $self->{'value_per_pixel'};
my $base = 0;
if ($self->{'inverted'}) {
$vpp = -$vpp;
$base = _widget_pixels($self) - 1;
}
if ($self->{'logarithmic'}) {
return sub {
return exp ($value + ($_[0] - $base) * $vpp);
};
} else {
return sub {
return ($value + ($_[0] - $base) * $vpp);
};
}
}
sub exp {
my ($self, $x) = @_;
return ($self->{'logarithmic'} ? exp($x) : $x);
}
sub log {
my ($self, $x) = @_;
return ($self->{'logarithmic'} ? log($x) : $x);
}
sub exp_proc {
my ($self) = @_;
return ($self->{'logarithmic'} ? \&exp : \&identity);
}
sub log_proc {
my ($self) = @_;
return ($self->{'logarithmic'} ? \&log : \&identity);
}
sub identity { return $_[0]; }
# 'size-allocate' signal on widget
sub _do_size_allocate {
my ($widget, $alloc, $ref_weak_self) = @_;
my $self = $$ref_weak_self || return;
if (DEBUG) { print "AdjScale size_allocate\n"; }
_update_page_size ($self);
}
sub _update_page_size {
my ($self) = @_;
if (DEBUG) { print " _update_page_size\n"; }
my %values;
# whether currently showing the end, or roughly so
my $upper = $self->upper;
my $at_end = ($self->value >= $upper - $self->page_size * 1.01);
if (DEBUG) { print " at_end ",$at_end?"yes":"no"," with upper $upper\n"; }
my $height = ($self->{'widget'}
? $self->{'widget'}->allocation->height
: 0);
my $page = $height * $self->{'value_per_pixel'};
$values{'page_size'} = $page;
$values{'page_increment'} = $self->get('page-increment-fraction') * $page;
$values{'step_increment'} = $self->get('step-increment-fraction') * $page;
if (DEBUG) { print " page $page on $height pixels and value_per_pixel ",
$self->{'value_per_pixel'},"\n"; }
# if upper-lower smaller than new page size then extend upper
my $lower = $self->lower;
if ($upper - $lower < $page) {
$upper = $lower + $page;
$values{'upper'} = $upper;
}
# if bigger page pushes value+page above upper then reduce value to max;
# if we were showing the end and a smaller page size means we no longer
# are then increase value to its max
my $max_value = $upper - $page;
if ($self->value > $max_value || $at_end) {
if (DEBUG) { print " bigger page pushes value down to $max_value\n"; }
$values{'value'} = $max_value;
}
if (DEBUG) { print " page $page",
" vpp ",$self->{'value_per_pixel'},
" ppv ",$self->{'pixel_per_value'},"\n"; }
Gtk2::Ex::AdjustmentBits::set_maybe ($self, %values);
}
sub empty {
my ($self) = @_;
Gtk2::Ex::AdjustmentBits::set_empty($self);
}
sub is_empty {
my ($self) = @_;
return ($self->page_size == 0);
}
sub _widget_pixels {
my ($self) = @_;
my $widget = $self->{'widget'} || return 0;
my $alloc = $widget->allocation;
return ($self->{'orientation'} eq 'horizontal'
? $alloc->width : $alloc->height);
}
# scroll by $count many steps
sub scroll_step {
my ($self, $count) = @_;
Gtk2::Ex::AdjustmentBits::set_maybe
($self,
value => $self->value + $self->step_increment * $count);
}
1;
__END__