File Coverage

blib/lib/Gtk2/Ex/QuadButton.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             # Copyright 2010, 2011 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-QuadButton.
4             #
5             # Gtk2-Ex-QuadButton is free software; you can redistribute it and/or
6             # modify it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 3, or (at your option) any
8             # later version.
9             #
10             # Gtk2-Ex-QuadButton is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Gtk2-Ex-QuadButton. If not, see .
17              
18             package Gtk2::Ex::QuadButton;
19 3     3   2599 use 5.008;
  3         10  
  3         177  
20 3     3   19 use strict;
  3         7  
  3         110  
21 3     3   90 use warnings;
  3         5  
  3         118  
22 3     3   15 use List::Util 'min', 'max';
  3         5  
  3         417  
23 3     3   10414 use Gtk2 1.220;
  0            
  0            
24             use Gtk2::Ex::WidgetBits 40; # v.40 for pixel_size_mm()
25             use Gtk2::Ex::Units 13; # initial v.13
26              
27             # uncomment this to run the ### lines
28             #use Smart::Comments;
29              
30             our $VERSION = 1;
31              
32             use Glib::Object::Subclass
33             'Gtk2::DrawingArea',
34             signals => { size_request => \&_do_size_request,
35             expose_event => \&_do_expose,
36             style_set => \&_do_style_set,
37             hierarchy_changed => \&_do_hierarchy_changed,
38             motion_notify_event => \&_do_motion_or_enter,
39             enter_notify_event => \&_do_motion_or_enter,
40             leave_notify_event => \&_do_leave_notify,
41             button_press_event => \&_do_button_press,
42             scroll_event => \&_do_scroll_event,
43             clicked => { param_types => [ 'Gtk2::ScrollType' ],
44             flags => ['run-first','action'],
45             },
46             # GtkWidget "direction-changed" does a queue_draw() which is
47             # enough for the xalign "rtl" bit
48             },
49             properties => [
50             Glib::ParamSpec->double
51             ('xalign',
52             (do {
53             my $str = 'Horizontal alignment';
54             eval { require Locale::Messages;
55             Locale::Messages::dgettext('gtk20-properties',$str)
56             } || $str }),
57             'Blurb.',
58             0, 1.0, # min,max
59             0.5, # default
60             Glib::G_PARAM_READWRITE),
61              
62             Glib::ParamSpec->double
63             ('yalign',
64             (do {
65             my $str = 'Vertical alignment';
66             eval { require Locale::Messages;
67             Locale::Messages::dgettext('gtk20-properties',$str)
68             } || $str }),
69             'Blurb.',
70             0, 1.0, # min,max
71             0.5, # default
72             Glib::G_PARAM_READWRITE),
73             ];
74              
75             # priority level "gtk" treating this as widget level default, for overriding
76             # by application or user RC
77             Gtk2::Rc->parse_string (<<'HERE');
78             binding "Gtk2__Ex__QuadButton_keys" {
79             bind "Up" { "clicked" (step-up) }
80             bind "Down" { "clicked" (step-down) }
81             bind "Up" { "clicked" (page-up) }
82             bind "Down" { "clicked" (page-down) }
83             bind "Left" { "clicked" (step-left) }
84             bind "Right" { "clicked" (step-right) }
85             bind "Left" { "clicked" (page-left) }
86             bind "Right" { "clicked" (page-right) }
87             bind "Page_Up" { "clicked" (page-up) }
88             bind "Page_Down" { "clicked" (page-down) }
89             }
90             class "Gtk2__Ex__QuadButton" binding:gtk "Gtk2__Ex__QuadButton_keys"
91             HERE
92              
93             sub INIT_INSTANCE {
94             my ($self) = @_;
95             $self->{'drawn_dir'} = '';
96             $self->can_focus(1);
97             $self->add_events (['button-press-mask',
98             'pointer-motion-mask',
99             'enter-notify-mask',
100             'leave-notify-mask']);
101             }
102              
103             sub SET_PROPERTY {
104             my ($self, $pspec, $newval) = @_;
105             my $pname = $pspec->get_name;
106             $self->{$pname} = $newval;
107             ### Enum SET_PROPERTY: $pname, $newval
108              
109             # xalign,yalign
110             $self->queue_draw;
111             }
112              
113             # 'size-request' class handler
114             sub _do_size_request {
115             my ($self, $req) = @_;
116             ### QuadButton _do_size_request(): @_
117             my $size = max (5, 2.2 * Gtk2::Ex::Units::em($self));
118             $req->width (int ($size + .5));
119             if (defined (my $ratio = Gtk2::Ex::WidgetBits::pixel_aspect_ratio($self))) {
120             # ratio = pixwidth/pixheight
121             $size /= $ratio;
122             }
123             $req->height (int ($size + .5));
124             }
125              
126             # 'style-set' class handler
127             sub _do_style_set {
128             my ($self) = @_;
129             ### QuadButton _do_style_set(): @_
130             shift->signal_chain_from_overridden(@_);
131              
132             $self->queue_draw; # new colours
133             $self->queue_resize; # size request in new font
134             }
135              
136             # 'hierarchy-changed' class handler
137             sub _do_hierarchy_changed {
138             my ($self) = @_;
139             ### QuadButton _do_hierarchy_changed(): @_
140             shift->signal_chain_from_overridden(@_);
141              
142             $self->queue_resize; # size request new aspect ratio
143             }
144              
145             sub _use_rect {
146             my ($self) = @_;
147             my (undef,undef, $width, $height) = $self->allocation->values;
148             my ($xsize,$ysize);
149             my $ratio = Gtk2::Ex::WidgetBits::pixel_aspect_ratio($self); # width/height
150             ### $ratio
151             my $width_from_height = int ($height * $ratio + .5);
152             if ($width >= $width_from_height) {
153             $xsize = $width_from_height;
154             $ysize = $height;
155             } else {
156             $xsize = $width;
157             $ysize = min ($height, int ($width / $ratio + .5));
158             }
159             ### xsize/ysize: $xsize/$ysize
160             my $xalign = $self->get('xalign');
161             if ($self->get_direction eq 'rtl') {
162             $xalign = 1 - $xalign;
163             }
164             return (int (($width - $xsize) * $xalign + .5),
165             int (($height - $ysize) * $self->get('yalign') + .5),
166             $xsize,
167             $ysize);
168             }
169              
170             sub _do_expose {
171             my ($self, $event) = @_;
172             ### QuadButton _do_expose()
173              
174             my $win = $self->window;
175             my $state = $self->state;
176             my $style = $self->get_style;
177             my (undef,undef, $width, $height) = $self->allocation->values;
178             my ($xpos,$ypos,$xsize,$ysize) = _use_rect($self);
179              
180             my $dir = _xy_to_direction ($self, $self->{'x'}, $self->{'y'},
181             $xpos,$ypos,$xsize,$ysize);
182             $self->{'drawn_dir'} = $dir;
183              
184             ### $dir
185             ### $state
186             ### fg: $self->style->fg($state)->to_string, $self->style->fg('prelight')->to_string
187             ### bg: $self->style->bg($state)->to_string, $self->style->bg('prelight')->to_string
188             ### $xpos
189             ### $ypos
190              
191             my $xc = $xpos + int($xsize/2);
192             my $yc = $ypos + int($ysize/2);
193              
194             # clear background
195             {
196             my $gc = $style->bg_gc($state);
197             foreach my $rect ($event->region->get_rectangles) {
198             $win->draw_rectangle ($gc,
199             1, # filled
200             $rect->values);
201             }
202             }
203              
204             # prelight background for armed direction
205             if ($dir) {
206             my @points_bg = (0,0, # top left
207             ($dir eq 'up' || $dir eq 'down'
208             ? ($xsize-1,0) # top right
209             : (0,$ysize-1)), # bottom left
210             $xsize/2,$ysize/2, # centre
211             0,0); # top left again
212             if ($dir eq 'down') {
213             for (my $i = 1; $i < @points_bg; $i+=2) {
214             $points_bg[$i] = $ysize-1-$points_bg[$i]; # invert
215             }
216             }
217             if ($dir eq 'right') {
218             for (my $i = 0; $i < @points_bg; $i+=2) {
219             $points_bg[$i] = $xsize-1-$points_bg[$i]; # mirror
220             }
221             }
222             for (my $i = 0; $i < @points_bg; $i+=2) {
223             $points_bg[$i] += $xpos;
224             $points_bg[$i+1] += $ypos;
225             }
226             my $gc = $style->bg_gc('prelight');
227             $gc->set_clip_region ($event->region);
228             ### prelight bg: @points_bg
229             ### $gc
230             $win->draw_polygon ($gc, 0, @points_bg);
231             $win->draw_polygon ($gc, 1, @points_bg);
232             $gc->set_clip_region (undef);
233             }
234              
235             my $xmid = $xc - int(.28 * $xsize);
236             my $ymid = $yc - int(.28 * $ysize);
237             my $xbase_size = int(.2 * $xsize);
238             my $ybase_size = int(.2 * $ysize);
239             my $xshaft_size = max(1,int($xsize*.05));
240             my $yshaft_size = max(1,int($ysize*.05));
241             my $xshaft = $xc - $xshaft_size;
242             my $yshaft = $yc - $yshaft_size;
243             my $xshaft_end = $xc - $yshaft_size;
244             my $yshaft_end = $yc - $xshaft_size;
245             ### $xshaft_size
246             ### $yshaft_size
247              
248             my $gc = Gtk2::Gdk::GC->new ($win);
249             my $copied_gc = 0;
250              
251             # up/down arrows
252             {
253             # up arrow
254             my @points_fg = ($xc, $ypos, # top centre
255             $xc - $xbase_size, $ymid, # base left
256             $xc - $xshaft_size, $ymid,
257             $xc - $xshaft_size, $yshaft_end, # shaft end
258             $xc + $xshaft_size, $yshaft_end, # shaft end
259             $xc + $xshaft_size, $ymid,
260             $xc + $xbase_size, $ymid, # base right
261             $xpos+int(($xsize+1)/2), $ypos, # top centre, rounded
262             $xc, $ypos); # top centre again
263              
264             my $this_dir = 'up';
265             foreach (0, 1) {
266             {
267             my $want_gc = $style->fg_gc($dir eq $this_dir ? 'prelight' : $state);
268             if ($want_gc != $copied_gc) {
269             $copied_gc = $want_gc;
270             $gc->copy($want_gc);
271             $gc->set_clip_region ($event->region);
272             # line width 1 to have the outline pixels correct
273             $gc->set_line_attributes (1,'solid','butt','miter');
274             ### copy gc: $dir eq $this_dir && $state
275             ### copied fg: $copied_gc->get_values->{'foreground'}->pixel
276             }
277             }
278             ### arrow fg: $gc->get_values->{'foreground'}->pixel
279             $win->draw_polygon ($gc, 0, @points_fg);
280             $win->draw_polygon ($gc, 1, @points_fg);
281              
282             # invert
283             $this_dir = 'down';
284             for (my $i = 1; $i < @points_fg; $i+=2) {
285             $points_fg[$i] = $ypos+$ysize-1-($points_fg[$i]-$ypos);
286             }
287             }
288             }
289              
290             # left/right arrows
291             {
292             # left
293             my @points_fg = ($xpos, $yc, # left centre
294             $xmid, $yc - $ybase_size, # base upper
295             $xmid, $yc - $yshaft_size,
296             $xshaft_end, $yc - $yshaft_size, # shaft end
297             $xshaft_end, $yc + $yshaft_size,
298             $xmid, $yc + $yshaft_size,
299             $xmid, $yc + $ybase_size, # base lower
300             $xpos, $yc);
301             my $this_dir = 'left';
302             foreach (0, 1) {
303             {
304             my $want_gc = $style->fg_gc($dir eq $this_dir ? 'prelight' : $state);
305             if ($want_gc != $copied_gc) {
306             $copied_gc = $want_gc;
307             $gc->copy($want_gc);
308             $gc->set_clip_region ($event->region);
309             # line width 1 to have the outline pixels correct
310             $gc->set_line_attributes (1,'solid','butt','miter');
311             }
312             }
313             $win->draw_polygon ($gc, 0, @points_fg);
314             $win->draw_polygon ($gc, 1, @points_fg);
315              
316             # mirror left/right
317             $this_dir = 'right';
318             for (my $i = 0; $i < @points_fg; $i+=2) {
319             $points_fg[$i] = $xpos+$xsize-1-($points_fg[$i]-$xpos);
320             }
321             }
322             }
323              
324             # focus dashed line, if focused
325             if ($self->has_focus) {
326             $style->paint_focus ($win, # window
327             $state, # state
328             $event->area,
329             $self, # widget
330             __PACKAGE__, # detail
331             0,0,
332             $width,$height);
333             }
334              
335             return Gtk2::EVENT_PROPAGATE;
336             }
337              
338             # x <= 1-y off diagonal is left or up
339             # | x > 1-y is down or right
340             # | |
341             my @table = ('left','down', # x <= y so left or down
342             'up', 'right'); # x > y so up or right
343              
344             sub _xy_to_direction {
345             my ($self, $x, $y, $xpos,$ypos,$xsize,$ysize) = @_;
346             if (@_ < 4) {
347             ($xpos,$ypos,$xsize,$ysize) = _use_rect($self);
348             }
349             if (defined $x && defined $y) {
350             $x = ($x - $xpos) / $xsize; # 0.0 to 1.0
351             if ($x >= 0 && $x < 1) {
352             $y = ($y - $ypos) / $ysize; # 0.0 to 1.0
353             if ($y >= 0 && $y < 1) {
354              
355             return $table[(($x>$y)<<1) + ($x > 1-$y)];
356             }
357             }
358             }
359             return '';
360             }
361              
362             sub _do_motion_or_enter {
363             my ($self, $event) = @_;
364             my $x = $self->{'x'} = $event->x;
365             my $y = $self->{'y'} = $event->y;
366             if ($self->{'drawn_dir'} ne _xy_to_direction ($self, $x, $y)) {
367             $self->queue_draw;
368             }
369             return Gtk2::EVENT_PROPAGATE;
370             }
371              
372             sub _do_leave_notify {
373             my ($self, $event) = @_;
374             ### QuadButton _do_leave()
375             undef $self->{'x'};
376             undef $self->{'y'};
377             if ($self->{'drawn_dir'}) {
378             $self->queue_draw;
379             }
380             return Gtk2::EVENT_PROPAGATE;
381             }
382              
383             my $modifiers_for_page
384             = Gtk2::Gdk::ModifierType->new(['control-mask','shift-mask']);
385              
386             sub _do_button_press {
387             my ($self, $event) = @_;
388             ### QuadButton _do_button_press(): $event->x.','.$event->y
389             ### dir: _xy_to_direction ($self, $event->x, $event->y)
390              
391             if ($event->button == 1
392             && (my $dir = _xy_to_direction ($self, $event->x, $event->y))) {
393             $self->signal_emit ('clicked',
394             ($event->state & $modifiers_for_page
395             ? 'page-' : 'step-')
396             . $dir);
397             }
398             return $self->signal_chain_from_overridden ($event);
399             }
400              
401             sub _do_scroll_event {
402             my ($self, $event) = @_;
403             ### QuadButton _do_scroll_event(): $event->direction, $event->state
404             $self->signal_emit ('clicked',
405             ($event->state & $modifiers_for_page
406             ? 'page-' : 'step-')
407             . $event->direction);
408             return $self->signal_chain_from_overridden ($event);
409             }
410              
411             1;
412             __END__