File Coverage

blib/lib/Gtk2/Ex/Dragger.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-Dragger.
4             #
5             # Gtk2-Ex-Dragger is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Gtk2-Ex-Dragger 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-Dragger. If not, see .
17              
18             package Gtk2::Ex::Dragger;
19 2     2   2582 use 5.008;
  2         7  
  2         87  
20 2     2   11 use strict;
  2         3  
  2         72  
21 2     2   59 use warnings;
  2         8  
  2         73  
22 2     2   18 use Carp;
  2         4  
  2         451  
23 2     2   2560 use POSIX ();
  2         17839  
  2         73  
24 2     2   2842 use Glib 1.220; # 1.220 for Glib::SOURCE_REMOVE
  0            
  0            
25             use Gtk2 1.220; # 1.220 for Gtk2::EVENT_PROPAGATE
26             use List::Util qw(min max);
27             use Scalar::Util;
28              
29             use Glib::Ex::SignalIds;
30             use Gtk2::Ex::WidgetEvents;
31              
32             # uncomment this to run the ### lines
33             #use Smart::Comments;
34              
35             our $VERSION = 10;
36              
37             use constant DELAY_MILLISECONDS => 250;
38              
39             BEGIN {
40             Glib::Type->register_enum ('Gtk2::Ex::Dragger::UpdatePolicy',
41             'default',
42             'continuous',
43             'delayed',
44             'discontinuous',
45             );
46             }
47              
48             use Glib::Object::Subclass
49             'Glib::Object',
50             properties => [ Glib::ParamSpec->object
51             ('widget',
52             'Target widget',
53             'The target widget whose contents are to be moved around. (For a widget inside a Gtk2::ViewPort this property should be the ViewPort.)',
54             'Gtk2::Widget',
55             Glib::G_PARAM_READWRITE),
56              
57             Glib::ParamSpec->object
58             ('hadjustment',
59             (do {
60             my $str = 'Horizontal adjustment';
61             # translation if available
62             eval { require Locale::Messages;
63             Locale::Messages::dgettext('gtk20-properties',$str)
64             } || $str }),
65             'Horizontal adjustment to change, or undef for no horizontal drag.',
66             'Gtk2::Adjustment',
67             Glib::G_PARAM_READWRITE),
68              
69             Glib::ParamSpec->object
70             ('vadjustment',
71             (do {
72             my $str = 'Vertical adjustment';
73             # translation if available
74             eval { require Locale::Messages;
75             Locale::Messages::dgettext('gtk20-properties',$str)
76             } || $str }),
77             'Vertical adjustment to change, or undef for no vertical drag.',
78             'Gtk2::Adjustment',
79             Glib::G_PARAM_READWRITE),
80              
81             Glib::ParamSpec->boolean
82             ('hinverted',
83             'Horizontal inverted',
84             'Whether to invert horizontal movement, for hadjustment valuess increasing to the left (ie. decreasing X coordinate).',
85             0, # default no
86             Glib::G_PARAM_READWRITE),
87              
88             Glib::ParamSpec->boolean
89             ('vinverted',
90             'Vertical inverted',
91             'Whether to invert vertical movement, for vadjustment values increasing up the screen (ie. decreasing Y coordinate).',
92             0, # default no
93             Glib::G_PARAM_READWRITE),
94              
95             Glib::ParamSpec->boolean
96             ('confine',
97             'Confine pointer',
98             'Confine the mouse pointer to the draggable extent per upper/lower range of the adjustments.',
99             0, # default no
100             Glib::G_PARAM_READWRITE),
101              
102             Glib::ParamSpec->enum
103             ('update-policy',
104             'Update policy',
105             'How often to update the hadjustment and vadjustment objects for drag movement.',
106             'Gtk2::Ex::Dragger::UpdatePolicy',
107             'default',
108             Glib::G_PARAM_READWRITE),
109              
110             Glib::ParamSpec->scalar
111             ('cursor',
112             'Cursor',
113             'Cursor to show while dragging, as any name or object accepted by Gtk2::Ex::WidgetCursor.',
114             Glib::G_PARAM_READWRITE),
115              
116             Glib::ParamSpec->string
117             ('cursor-name',
118             'Cursor name',
119             'Cursor to show while dragging, as cursor type enum nick, or "invisible".',
120             (eval {Glib->VERSION(1.240);1}
121             ? undef # default
122             : ''), # no undef/NULL before Perl-Glib 1.240
123             Glib::G_PARAM_READWRITE),
124              
125             Glib::ParamSpec->boxed
126             ('cursor-object',
127             'Cursor object',
128             'Cursor to show while dragging, as cursor object.',
129             'Gtk2::Gdk::Cursor',
130             Glib::G_PARAM_READWRITE),
131              
132             ];
133              
134             sub INIT_INSTANCE {
135             my ($self) = @_;
136             $self->{'h'} = {};
137             $self->{'v'} = {};
138             }
139              
140             sub FINALIZE_INSTANCE {
141             my ($self) = @_;
142             $self->stop;
143             }
144              
145             sub GET_PROPERTY {
146             my ($self, $pspec) = @_;
147             my $pname = $pspec->get_name;
148             ### Dragger GET_PROPERTY(): $pname
149              
150             if ($pname eq 'cursor_name') {
151             my $cursor = $self->{'cursor'};
152             if (Scalar::Util::blessed($cursor)) {
153             $cursor = $cursor->type;
154             # think prefer undef over cursor-is-pixmap for the get()
155             if ($cursor eq 'cursor-is-pixmap') {
156             undef $cursor;
157             }
158             }
159             return $cursor;
160             }
161             if ($pname eq 'cursor_object') {
162             my $cursor = $self->{'cursor'};
163             return (Scalar::Util::blessed($cursor)
164             && $cursor->isa('Gtk2::Gdk::Cursor')
165             && $cursor);
166             }
167              
168             return $self->{$pname};
169             }
170              
171             sub SET_PROPERTY {
172             my ($self, $pspec, $newval) = @_;
173             ### Dragger SET_PROPERTY(): $pspec->get_name
174             my $pname = $pspec->get_name;
175              
176             my $pname_is_cursor = ($pname =~ s/^(cursor).*/$1/);
177             my $oldval = $self->{$pname};
178             $self->{$pname} = $newval;
179              
180             if ($pname_is_cursor) {
181             # copy boxed GdkCursor in case the caller frees it, and in particular
182             # for $pname eq 'cursor_object' it might be freed immediately by the
183             # GValue call-out stuff
184             # if (blessed($newval) && $newval->isa('Gtk2::Gdk::Cursor')) {
185             # $self->{$pname} = $newval->copy;
186             # }
187             _widget_cursor($self);
188             $self->notify('cursor');
189             $self->notify('cursor-name');
190             $self->notify('cursor-object');
191              
192             } elsif ($pname eq 'widget') {
193             my $widget = $newval;
194             if (! $newval
195             || ($oldval && $newval != $oldval)) {
196             # ENHANCE-ME: might be able to switch to an active grab on the new
197             # widget and continue
198             $self->stop;
199             }
200             if ($widget) {
201             Scalar::Util::weaken ($self->{'widget'});
202             }
203             $self->{'wevents'} = $widget && Gtk2::Ex::WidgetEvents->new
204             ($widget, ['button-press-mask',
205             'button-motion-mask',
206             'button-release-mask']);
207             _widget_signals($self);
208              
209             } elsif ($pname =~ /([hv])(adjustment|inverted)/) {
210             my $axis = $self->{$1};
211             my $field = $2;
212             $axis->{$field} = $newval;
213              
214             if ($field eq 'adjustment') {
215             $axis->{'last_value'} = $newval && $newval->value;
216             _axis_signals($self,$axis);
217             _do_adjustment_changed ($newval, \$self);
218             }
219              
220             } elsif ($pname eq 'confine') {
221             _resize_confine_win ($self);
222             }
223             }
224              
225             # create or update WidgetCursor according to $self->{'cursor'}
226             # doesn't load WidgetCursor until drag active
227             sub _widget_cursor {
228             my ($self) = @_;
229             $self->{'wcursor'} = ($self->{'active'}
230             && $self->{'cursor'}
231             && do {
232             require Gtk2::Ex::WidgetCursor;
233             Gtk2::Ex::WidgetCursor->new
234             (widget => $self->{'widget'},
235             cursor => $self->{'cursor'},
236             active => 1);
237             });
238             }
239              
240             # update $self->{'h'}->{'adjustment_ids'} or $self->{'v'}->... signal handlers
241             # handlers only applied while 'active'
242             sub _axis_signals {
243             my ($self, $axis) = @_;
244             my $adj;
245             $axis->{'adjustment_ids'} =
246             ($self->{'active'}
247             && ($adj = $axis->{'adjustment'})
248             && do {
249             my $ref_weak_self = _ref_weak ($self);
250              
251             ### _axis_signals() connect
252             Glib::Ex::SignalIds->new
253             ($adj,
254             $adj->signal_connect (changed => \&_do_adjustment_changed,
255             $ref_weak_self),
256             $adj->signal_connect (value_changed => \&_do_adjustment_value_changed,
257             $ref_weak_self))
258             });
259             }
260              
261             # make signal handler connections on $self->{'widget'} if active
262             sub _widget_signals {
263             my ($self) = @_;
264             my $widget;
265             $self->{'widget_ids'} =
266             ($self->{'active'}
267             && ($widget = $self->{'widget'})
268             && do {
269             my $ref_weak_self = _ref_weak ($self);
270              
271             ### _widget_signals() connect
272             Glib::Ex::SignalIds->new
273             ($widget,
274             $widget->signal_connect (motion_notify_event => \&_do_motion_notify,
275             $ref_weak_self),
276             $widget->signal_connect (button_release_event => \&_do_button_release,
277             $ref_weak_self),
278             $widget->signal_connect (configure_event => \&_do_configure_event,
279             $ref_weak_self),
280             $widget->signal_connect (grab_broken_event => \&_do_grab_broken,
281             $ref_weak_self))
282             });
283             }
284              
285             sub start {
286             my ($self, $event) = @_;
287             ### Dragger start()
288              
289             # maybe a second start() call could transition to a different button, but
290             # for now disallow it
291             if ($self->{'active'}) {
292             croak __PACKAGE__.'->start(): drag already active';
293             }
294             (Scalar::Util::blessed($event) && $event->isa('Gtk2::Gdk::Event::Button'))
295             or croak __PACKAGE__.'->start(): must have button press event';
296              
297             my $widget = $self->{'widget'};
298             my $win = $widget->Gtk2_Ex_Dragger_window
299             or croak __PACKAGE__.'->start(): widget not realized';
300              
301             $self->{'active'} = 1;
302             _widget_cursor($self);
303             _widget_signals($self);
304              
305             foreach my $axis ($self->{'h'}, $self->{'v'}) {
306             my $adj = $axis->{'adjustment'} or next;
307             $axis->{'unapplied'} = 0;
308             $axis->{'pending'} = 0;
309             $axis->{'last_value'} = $adj->value;
310             _axis_signals ($self, $axis);
311             }
312              
313             $self->{'button'} = $event->button;
314             ($self->{'h'}->{'last_pixel'}, $self->{'v'}->{'last_pixel'})
315             = $event->get_root_coords;
316              
317             if ($self->{'confine'}) {
318             my $confine_win = ($self->{'confine_win'} ||= Gtk2::Gdk::Window->new
319             ($widget->get_root_window,
320             { window_type => 'temp',
321             wclass => 'GDK_INPUT_ONLY',
322             override_redirect => 1 }));
323             ### confine_win: "$confine_win"
324             _resize_confine_win ($self);
325             $confine_win->show;
326              
327             # ENHANCE-ME: $win->get_events is a server round-trip, maybe fetch only
328             # the first time, or fetch once and then mask in the widget 'events'
329             # property subsequently; or something cooperating with WidgetEvents ...
330             #
331             ### widget events: $widget->get_events.''
332             ### window events: $widget->window->get_events.''
333             my $event_mask
334             = ($win->get_events & ['button-press-mask',
335             'pointer-motion-hint-mask',
336             'structure-mask',
337             'property-change-mask' ])
338             + ['button-motion-mask', 'button-release-mask'];
339             ### events: "$event_mask"
340             ### grab to : $widget->window.''
341             ### cf button press on: $event->window.''
342             ### cf size win : "$win"
343              
344             my $status = Gtk2::Gdk->pointer_grab ($widget->window,
345             0, # owner events
346             $event_mask,
347             $confine_win,
348             undef, # cursor inherited
349             $event->time);
350             ### grab: "$status time ".$event->time
351             if ($status eq 'success') {
352             $self->{'grabbed'} = 1;
353             } else {
354             carp __PACKAGE__."->start(): cannot grab: $status";
355             }
356             }
357             }
358              
359             # 'grab-broken-event' signal on the target widget
360             #
361             # This event is a client-side invention of gdk, we listen to it to know when
362             # gdk's grab tracking says we lost the grab due to a window unmap or another
363             # grab by a different part of the program. This (almost certainly) means we
364             # should stop dragging.
365             #
366             # Gtk2::Gdk->pointer_grab() above will itself enqueue a grab broken event if
367             # the $widget->window we supply there is different from the one the implicit
368             # grab of the button press was in. That can happen when there's multiple
369             # GdkWindows within $widget, with all of their events dispatched to $widget.
370             # For example if you put a no-window child into a Gtk2::Viewport then a
371             # button press on it goes to the "view_window" sub-window of the Viewport,
372             # which is the large moving subwindow of $widget->window (in fact
373             # sub-sub-window, since there's a "bin_window" in between too).
374             #
375             # The code here checks if $event->window is our pointer_grab()
376             # $widget->window losing the grab. (A pointer_grab() call asking for the
377             # same window as currently holding the grab doesn't produce a grab broken
378             # event, so that test is safe against a button press and grab to the same
379             # child window.)
380             #
381             # It'd also be possible to look at $event->grab_window to see who has the
382             # current grab window, to see if it's our desired $widget->window. That'd
383             # be a kind of more positive test, but $event->grab_window is not wrapped
384             # until Gtk2-Perl 1.190.
385             #
386             sub _do_grab_broken {
387             my ($widget, $event, $ref_weak_self) = @_;
388             my $self = $$ref_weak_self || return Gtk2::EVENT_PROPAGATE;
389             ### Dragger _do_grab_broken()
390             ### event window : $event->window.''
391             ### widget window: $widget->window.''
392              
393             if ($self->{'grabbed'} && $event->window == $widget->window) {
394             $self->{'grabbed'} = 0;
395             $self->stop ($event);
396             }
397             return Gtk2::EVENT_PROPAGATE;
398             }
399              
400             # 'button-release-event' signal on the target widget
401             sub _do_button_release {
402             my ($widget, $event, $ref_weak_self) = @_;
403             ### Dragger _do_button_release()
404             my $self = $$ref_weak_self || return Gtk2::EVENT_PROPAGATE;
405              
406             if ($event->button == $self->{'button'}) {
407             _do_motion_notify ($widget, $event, \$self); # final position
408             $self->stop ($event);
409             }
410             return Gtk2::EVENT_PROPAGATE;
411             }
412              
413             sub stop {
414             my ($self, $event) = @_;
415             ### Dragger stop()
416              
417             if (! delete $self->{'active'}) { return; }
418              
419             if (delete $self->{'grabbed'}) {
420             Gtk2::Gdk->pointer_ungrab (defined $event
421             ? $event->time : Gtk2::GDK_CURRENT_TIME);
422             }
423             if (my $confine_win = $self->{'confine_win'}) {
424             $confine_win->hide;
425             }
426             if (my $id = delete $self->{'idle_id'}) {
427             Glib::Source->remove ($id);
428             }
429             if (my $id = delete $self->{'timer_id'}) {
430             Glib::Source->remove ($id);
431             }
432             delete $self->{'widget_ids'};
433             delete $self->{'wcursor'};
434             delete $self->{'h'}->{'adjustment_ids'};
435             delete $self->{'v'}->{'adjustment_ids'};
436             _emit_pending ($self);
437             }
438              
439             # 'configure-event' signal on the target widget
440             sub _do_configure_event {
441             my ($widget, $event, $ref_weak_self) = @_;
442             my $self = $$ref_weak_self || return Gtk2::EVENT_PROPAGATE;
443              
444             # new window size changes the scale factor and hence how many pixels to
445             # the adjustable limits
446             _resize_confine_win ($self);
447             return Gtk2::EVENT_PROPAGATE;
448             }
449              
450             # 'changed' signal on either hadjustment or vadjustment
451             sub _do_adjustment_changed {
452             my ($adj, $ref_weak_self) = @_;
453             my $self = $$ref_weak_self || return;
454             ### Dragger _do_adjustment_changed(): "$adj"
455              
456             # new page size changes the scale factor and hence how many pixels to the
457             # adjustable limits
458             _resize_confine_win ($self);
459             }
460              
461             # 'value-changed' signal on either hadjustment or vadjustment
462             #
463             # If the value we see is what we set then no action. If it's something
464             # different then it's a change made by the keyboard or something else
465             # external.
466             #
467             # We must reset any 'unapplied' amount because we can't have a non-zero
468             # unapplied when the value is somewhere not at the upper or lower limits,
469             # because unapplied is essentially how far beyond those limits the mouse is.
470             # (The effect of leaving an 'unapplied' is for the value to jump down or up
471             # unnaturally on the next drag update.)
472             #
473             sub _do_adjustment_value_changed {
474             my ($adj, $ref_weak_self) = @_;
475             my $self = $$ref_weak_self || return;
476             my $axis = ($adj == ($self->{'h'}->{'adjustment'} || 0)
477             ? $self->{'h'}
478             : $self->{'v'});
479              
480             if ($adj->value == $axis->{'last_value'}) { return; }
481              
482             ### Dragger value changed externally to: $adj->value
483             $axis->{'last_value'} = $axis->{'adjustment'}->value;
484             $axis->{'unapplied'} = 0;
485              
486             # new positions for the limits relative to the mouse position
487             _resize_confine_win ($self);
488             }
489              
490             sub _resize_confine_win {
491             my ($self) = @_;
492             my $confine_win = $self->{'confine_win'} || return;
493              
494             my $widget = $self->{'widget'};
495             my $win = $widget->Gtk2_Ex_Dragger_window;
496             my ($win_width, $win_height) = $win->get_size;
497              
498             my $root = $widget->get_root_window;
499             my ($root_width, $root_height) = $root->get_size;
500              
501             # default full root window, no confine
502             my $confine_x = 0;
503             my $confine_y = 0;
504             my $confine_width = $root_width;
505             my $confine_height = $root_height;
506              
507             # The x position is so a move that far to the left would hit the limit of
508             # the adjustment. For normal direction a mouse move to the left increases
509             # adjustment value, so look at how far "value" is from "upper - page".
510             # For inverted a move to the left decreases adjustment value, so look at
511             # how far "value" is from "lower".
512             #
513             if (my $hadj = $self->{'h'}->{'adjustment'}) {
514             if (my $page_size = $hadj->page_size) {
515             $confine_x = $self->{'h'}->{'last_pixel'} -
516             ($win_width / $page_size)
517             * ($self->{'h'}->{'inverted'}
518             ? $hadj->value - $hadj->lower
519             : $hadj->upper - $hadj->page_size - $hadj->value);
520             $confine_width = $win_width
521             * ($hadj->upper - $hadj->lower - $hadj->page_size) / $hadj->page_size;
522             }
523             }
524             if (my $vadj = $self->{'v'}->{'adjustment'}) {
525             if (my $page_size = $vadj->page_size) {
526             $confine_y = $self->{'v'}->{'last_pixel'} -
527             ($win_height / $page_size)
528             * ($self->{'v'}->{'inverted'}
529             ? $vadj->value - $vadj->lower
530             : $vadj->upper - $vadj->page_size - $vadj->value);
531             $confine_height = $win_height
532             * ($vadj->upper - $vadj->lower - $vadj->page_size) / $vadj->page_size;
533             }
534             }
535              
536             # round x,y down to integers, increasing width,height by what's subtracted
537             {
538             my $frac;
539             ($confine_x, $frac) = _floor_and_frac ($confine_x);
540             $confine_width += $frac;
541             ($confine_y, $frac) = _floor_and_frac ($confine_y);
542             $confine_height += $frac;
543             }
544              
545             # round up width,height to integers
546             $confine_width = POSIX::ceil ($confine_width);
547             $confine_height = POSIX::ceil ($confine_height);
548              
549             # allow an extra pixel left,right,top and bottom just in case the rounding
550             # is a bit off, or whatever
551             $confine_x--;
552             $confine_y--;
553             $confine_width += 2;
554             $confine_height += 2;
555              
556             # Bring any negative top-left X,Y into range of the screen. This is in
557             # case X,Y are big negatives that overflow the signed 16-bit value in the
558             # X protocol.
559             if ($confine_x < 0) {
560             $confine_width += $confine_x; # reduce width accordingly
561             $confine_x = 0;
562             }
563             if ($confine_y < 0) {
564             $confine_height += $confine_y; # reduce height accordingly
565             $confine_y = 0;
566             }
567              
568             # If the X,Y position is off the right or bottom of the screen then go to
569             # a single pixel at that right or bottom. Suspect this shouldn't occur,
570             # because the confine window will contain the current mouse position, and
571             # that's certainly somewhere on-screen.
572             #
573             if ($confine_x >= $root_width) {
574             $confine_x = $root_width - 1;
575             $confine_width = 1;
576             }
577             if ($confine_y >= $root_height) {
578             $confine_y = $root_height - 1;
579             $confine_height = 1;
580             }
581              
582             # Chop off any width/height exceeding the screen. This is in case
583             # width,height are big values which overflow the 16-bit integers in the X
584             # protocol.
585             $confine_width = min ($confine_width, $root_width - $confine_x);
586             $confine_height = min ($confine_height, $root_height - $confine_y);
587              
588             ### confine to: "$confine_x,$confine_y ${confine_width}x${confine_height}"
589             $confine_win->move_resize ($confine_x, $confine_y,
590             $confine_width, $confine_height);
591             }
592              
593             # 'motion-notify-event' on widget, and also called for button release.
594             #
595             # The basic operation is simply to look at how many pixels the new x,y in
596             # $event has moved from our last_x,last_y and apply those amounts to the
597             # "value" in the adjustments. But with attention to the following:
598             #
599             # * Scale factor $value_per_pixel converts between a window worth of pixels
600             # equivalent to a page size amount in the adjust.
601             #
602             # * last_x and last_y are kept in root window coordinates. This makes no
603             # difference to each "delta" calculated, but means we're safe against any
604             # changes to the widget window position; and also makes the confine_win
605             # calculation a little easier.
606             #
607             # * The hinverted/vinverted settings are tricky to get the right way around.
608             # In normal state a move to the right reduces the value, and when inverted
609             # it's the other way around.
610             #
611             # * An "unapplied" amount of value is maintained horizontally and vertically
612             # if the prospective value would be outside the adjustment upper/lower
613             # bounds. It gets added back each time, with the effect of keeping the
614             # same widget contents position under the mouse if you go beyond the limit
615             # and then come back.
616             #
617             sub _do_motion_notify {
618             my ($widget, $event, $ref_weak_self) = @_;
619             #### Dragger _do_motion_notify()
620             my $self = $$ref_weak_self || return Gtk2::EVENT_PROPAGATE;
621              
622             # Believe no need for Gtk 2.12 $event->request_motions here since our
623             # device is only ever the mouse, so $win->get_pointer is enough. Besides,
624             # request_motions() looks pretty slack -- surely if you're going to do a
625             # server round trip (as $disp->get_pointer or $device->get_state) then you
626             # should use the position obtained, not throw it away.
627             #
628             # test can('is_hint') to allow for final $event a Gtk2::Gdk::Event::Button
629             # release; such an event doesn't have an is_hint field.
630             #
631             my ($x, $y);
632             if ($event->can('is_hint') && $event->is_hint) {
633             (undef, $x, $y) = $widget->get_root_window->get_pointer;
634             } else {
635             ($x, $y) = $event->get_root_coords;
636             }
637              
638             my $win = $widget->Gtk2_Ex_Dragger_window;
639             my ($win_width, $win_height) = $win->get_size;
640             _set_value ($self, $self->{'h'}, $win_width, $x);
641             _set_value ($self, $self->{'v'}, $win_height, $y);
642             return Gtk2::EVENT_PROPAGATE;
643             }
644              
645             sub _set_value {
646             my ($self, $axis, $win_size, $pixel) = @_;
647             ##### Dragger _set_value(): "pixel $pixel", $axis
648              
649             my $adj = $axis->{'adjustment'} || return;
650              
651             my $delta_pixel = $pixel - $axis->{'last_pixel'};
652             if ($delta_pixel == 0) { return; }
653             $axis->{'last_pixel'} = $pixel;
654             if ($axis->{'inverted'}) { $delta_pixel = - $delta_pixel; }
655              
656             my $page_size = $adj->page_size;
657             my $value_per_pixel = $page_size / $win_size;
658             my $old_value = $adj->value;
659             my $new_value
660             = $old_value + $axis->{'unapplied'} - $delta_pixel * $value_per_pixel;
661             my $unapplied = 0;
662              
663             my $lower = $adj->lower;
664             if ($new_value < $lower) {
665             $unapplied = $new_value - $lower; # negative
666             $new_value = $lower;
667             }
668             my $upper = $adj->upper - $page_size;
669             if ($new_value > $upper) {
670             $unapplied = $new_value - $upper; # positive
671             $new_value = $upper;
672             }
673             $axis->{'unapplied'} = $unapplied;
674              
675             ### set value: $new_value
676             $adj->value ($new_value);
677             $new_value = $axis->{'last_value'} = $adj->value; # refetch in case rounding
678             ### rounded from NV: $new_value
679             if ($old_value == $new_value) {
680             ### unchanged, no signals
681             return;
682             }
683              
684             $adj->notify ('value');
685              
686             my $update_policy = $self->{'update_policy'} || 'default';
687             if ($update_policy eq 'continuous') {
688             # emit on every set
689             $adj->value_changed;
690             return;
691             }
692              
693             $axis->{'pending'} = 1;
694              
695             if ($update_policy eq 'discontinuous') {
696             # don't emit at all until stop
697             return;
698             }
699              
700             if ($update_policy eq 'delayed') {
701             $self->{'timer_id'} ||= Glib::Timeout->add
702             (DELAY_MILLISECONDS, \&_do_timer_delayed, _ref_weak ($self));
703             return;
704             }
705              
706             # default policy
707             require Gtk2::Ex::SyncCall;
708             if (! $self->{'sync_obj'} && ! $self->{'timer_id'}) {
709             #### Dragger SyncCall send
710             my $ref_weak_self = _ref_weak ($self);
711             $self->{'sync_obj'} = Gtk2::Ex::SyncCall->sync
712             ($self->{'widget'}, \&_do_sync, $ref_weak_self);
713             $self->{'timer_id'} = Glib::Timeout->add
714             (DELAY_MILLISECONDS, \&_do_timer_sync, $ref_weak_self);
715             }
716             }
717              
718             # timer expiry for 'delayed' policy
719             # emit 'value-changed' when the timer expires
720             #
721             sub _do_timer_delayed {
722             my ($ref_weak_self) = @_;
723             my $self = $$ref_weak_self || return Glib::SOURCE_REMOVE;
724             #### Dragger _do_timer_delayed()
725              
726             $self->{'timer_id'} = 0;
727             _emit_pending ($self);
728             return Glib::SOURCE_REMOVE;
729             }
730              
731             # sync response for 'default' policy
732             #
733             # At this point we wait for the timer or for idle, whichever comes first.
734             # It's possible the timer has already gone off (zeroing 'timer_id'), if
735             # that's the case them we emit immediately; otherwise start an idle.
736             #
737             sub _do_sync {
738             my ($ref_weak_self) = @_;
739             #### Dragger _do_sync()
740             my $self = $$ref_weak_self || return;
741              
742             $self->{'sync_obj'} = 0;
743             if ($self->{'timer_id'}) {
744             $self->{'idle_id'} ||= Glib::Idle->add
745             (\&_do_idle, _ref_weak ($self), Gtk2::GDK_PRIORITY_REDRAW - 1);
746             } else {
747             _emit_pending ($self);
748             }
749             }
750              
751             # timer expiry for 'default' policy
752             #
753             # If the sync response hasn't yet been received then we do nothing, instead
754             # wait for that. If it has been received then we can emit now, and cancel
755             # the idle that was running.
756             #
757             sub _do_timer_sync {
758             my ($ref_weak_self) = @_;
759             my $self = $$ref_weak_self || return Glib::SOURCE_REMOVE;
760             #### Dragger _do_timer_sync() with sync: "$self->{'sync_obj'}"
761              
762             if (my $id = $self->{'idle_id'}) {
763             $self->{'idle_id'} = 0;
764             Glib::Source->remove ($id);
765             }
766             $self->{'timer_id'} = 0;
767              
768             if (! $self->{'sync_obj'}) {
769             _emit_pending ($self);
770             }
771             return Glib::SOURCE_REMOVE;
772             }
773              
774             # idle handler for 'default' policy
775             sub _do_idle {
776             my ($ref_weak_self) = @_;
777             #### Dragger _do_idle() after sync
778             my $self = $$ref_weak_self || return Glib::SOURCE_REMOVE;
779              
780             if (my $id = $self->{'timer_id'}) {
781             $self->{'timer_id'} = 0;
782             Glib::Source->remove ($id);
783             }
784             $self->{'idle_id'} = 0;
785             _emit_pending ($self);
786             return Glib::SOURCE_REMOVE;
787             }
788              
789             sub _emit_pending {
790             my ($self) = @_;
791             foreach my $axis ($self->{'h'}, $self->{'v'}) {
792             if ($axis->{'pending'}) {
793             $axis->{'pending'} = 0;
794             $axis->{'adjustment'}->value_changed;
795             }
796             }
797             }
798              
799             #------------------------------------------------------------------------------
800             # $widget->Gtk2_Ex_Dragger_window() returns the window in $widget which the
801             # dragger should operate on (the size to page conversion).
802             #
803             # Crib notes:
804             #
805             # GtkLayout, and subclasses like GnomeCanvas
806             # Plain $widget->window is the visible extent, so nothing special
807             # needed. The scrolls move the bin_window subwindow, but how scrolling
808             # is drawn doesn't matter to us.
809             #
810              
811             sub Gtk2::Widget::Gtk2_Ex_Dragger_window {
812             my ($widget) = @_;
813             if (exists $widget->{'Gtk2_Ex_Dragger_window'}) {
814             # user override -- but this not (yet) a documented feature as such
815             return $widget->{'Gtk2_Ex_Dragger_window'};
816             }
817             # default
818             return $widget->window;
819             }
820              
821             # for TextView the "text" window is the visible extent
822             sub Gtk2::TextView::Gtk2_Ex_Dragger_window {
823             my ($textview) = @_;
824             return $textview->get_window ('text');
825             }
826              
827             # for TreeView the "bin" window is the visible extent
828             *Gtk2::TreeView::Gtk2_Ex_Dragger_window
829             = \&Gtk2::TreeView::get_bin_window;
830              
831             # For Viewport there's $widget->window then within that a "view_window"
832             # which is smaller by the border size. This view_window is the scrollable
833             # part we're interested in, but it's not a documented feature, so this is a
834             # nasty hack to pick it out.
835             #
836             # The $viewport->get_bin_window is a sub-window of the view_window. It
837             # contains the viewport children. (In the gtk manual in gtk 2.20 but not
838             # explained as such.)
839             #
840             sub Gtk2::Viewport::Gtk2_Ex_Dragger_window {
841             my ($viewport) = @_;
842             my $win;
843             return (($win = $viewport->window) # if realized
844             && ($win->get_children)[0]);
845             }
846              
847              
848             #------------------------------------------------------------------------------
849             # generic helpers
850              
851             # Return two values ($floor, $frac).
852             # $floor is $x rounded down to an integer towards negative infinity
853             # $frac is the fractional part subtracted from $x to get to $floor,
854             # so $floor+$frac == $x
855             #
856             sub _floor_and_frac {
857             my ($x) = @_;
858             my $f = POSIX::floor ($x);
859             return ($f, $x - $f);
860             }
861              
862             sub _ref_weak {
863             my ($self) = @_;
864             Scalar::Util::weaken ($self);
865             return \$self;
866             }
867              
868              
869             #------------------------------------------------------------------------------
870              
871             1;
872             __END__