File Coverage

blib/lib/Gtk2/Ex/Lasso.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


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-Xor.
4             #
5             # Gtk2-Ex-Xor is free software; you can redistribute it and/or modify it
6             # under the terms of the GNU General Public License as published by the Free
7             # Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Gtk2-Ex-Xor is distributed in the hope that it will be useful, but WITHOUT
11             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12             # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13             # more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Gtk2-Ex-Xor. If not, see .
17              
18             package Gtk2::Ex::Lasso;
19 1     1   1189 use 5.008;
  1         4  
  1         44  
20 1     1   6 use strict;
  1         1  
  1         35  
21 1     1   6 use warnings;
  1         2  
  1         34  
22 1     1   5 use Carp;
  1         1  
  1         76  
23 1     1   6 use List::Util qw(min max);
  1         1  
  1         104  
24 1     1   6 use Scalar::Util 1.18 'blessed','refaddr'; # 1.18 for pure-perl refaddr() fix
  1         20  
  1         69  
25 1     1   607 use Gtk2::Ex::GdkBits 38; # v.38 for draw_rectangle_corners()
  0            
  0            
26             use Glib::Ex::SignalIds;
27              
28             # 1.200 for Gtk2::GC auto-release and GDK_CURRENT_TIME
29             use Gtk2 1.200;
30             use Gtk2::Ex::Xor;
31              
32             # uncomment this to run the ### lines
33             #use Smart::Comments;
34              
35             our $VERSION = 22;
36              
37             use constant DEFAULT_LINE_STYLE => 'on_off_dash';
38              
39             use Glib::Object::Subclass
40             'Glib::Object',
41             signals => { moved => { param_types => [ 'Glib::Int',
42             'Glib::Int',
43             'Glib::Int',
44             'Glib::Int' ],
45             return_type => undef },
46             ended => { param_types => [ 'Glib::Int',
47             'Glib::Int',
48             'Glib::Int',
49             'Glib::Int' ],
50             return_type => undef },
51             aborted => { param_types => [ ],
52             return_type => undef },
53             },
54             properties => [ Glib::ParamSpec->object
55             ('widget',
56             'Widget',
57             'Widget to draw the lasso on.',
58             'Gtk2::Widget',
59             Glib::G_PARAM_READWRITE),
60              
61             Glib::ParamSpec->boolean
62             ('active',
63             'Active',
64             'True if lassoing is being drawn, moved, etc.',
65             0, # default
66             Glib::G_PARAM_READWRITE),
67              
68             Glib::ParamSpec->scalar
69             ('foreground',
70             (do { # translation from Gtk2::TextTag
71             my $str = 'Foreground colour';
72             eval { require Locale::Messages;
73             Locale::Messages::dgettext('gtk20-properties',$str)
74             } || $str }),
75             'The colour to draw the lasso, either a string name, an allocated Gtk2::Gdk::Color object, or undef for the widget\'s style foreground.',
76             Glib::G_PARAM_READWRITE),
77              
78             Glib::ParamSpec->string
79             ('foreground-name',
80             (do { # translation from Gtk2::TextTag
81             my $str = 'Foreground colour name';
82             eval { require Locale::Messages;
83             Locale::Messages::dgettext('gtk20-properties',$str)
84             } || $str }),
85             'The colour to draw the lasso, as a string colour name.',
86             (eval {Glib->VERSION(1.240);1}
87             ? undef # default
88             : ''), # no undef/NULL before Perl-Glib 1.240
89             Glib::G_PARAM_READWRITE),
90              
91             Glib::ParamSpec->boxed
92             ('foreground-gdk',
93             'Foreground colour object',
94             'The colour to draw the lasso, as a Gtk2::Gdk::Color object with red,greed,blue fields set (a pixel is looked up on the target widget).',
95             'Gtk2::Gdk::Color',
96             Glib::G_PARAM_READWRITE),
97              
98             Glib::ParamSpec->scalar
99             ('cursor',
100             'Cursor',
101             'Cursor while lassoing, anything accepted by Gtk2::Ex::WidgetCursor, default \'hand1\'.',
102             Glib::G_PARAM_READWRITE),
103              
104             Glib::ParamSpec->string
105             ('cursor-name',
106             'Cursor name',
107             'Cursor to show while lassoing, as cursor type enum nick, or "invisible".',
108             'hand1',
109             Glib::G_PARAM_READWRITE),
110              
111             Glib::ParamSpec->boxed
112             ('cursor-object',
113             'Cursor object',
114             'Cursor to show while lassoing, as cursor object.',
115             'Gtk2::Gdk::Cursor',
116             Glib::G_PARAM_READWRITE),
117              
118             ];
119              
120              
121             sub INIT_INSTANCE {
122             my ($self) = @_;
123             $self->{'button'} = 0;
124             $self->{'cursor'} = 'hand1';
125             }
126              
127             sub FINALIZE_INSTANCE {
128             my ($self) = @_;
129             if ($self->{'active'}) {
130             # don't emit 'ended' or 'aborted' during destroy
131             _end ($self);
132             }
133             }
134              
135             sub GET_PROPERTY {
136             my ($self, $pspec) = @_;
137             my $pname = $pspec->get_name;
138             ### Lasso GET_PROPERTY(): $pname
139              
140             if ($pname eq 'foreground_name') {
141             my $foreground = $self->{'foreground'};
142             ### $foreground
143             if (blessed($foreground) && $foreground->isa('Gtk2::Gdk::Color')) {
144             ### str: $foreground->to_string
145             ### blue: $foreground->blue
146             $foreground = $foreground->to_string; # string "#RRRRGGGGBBBB"
147             }
148             return $foreground;
149             }
150             if ($pname eq 'foreground_gdk') {
151             my $foreground = $self->{'foreground'};
152             ### $foreground
153             if (defined $foreground && ! blessed($foreground)) {
154             # Perl-Glib 1.220 doesn't copy a boxed return like Gtk2::Gdk::Color,
155             # must keep the block of memory in a field
156             $foreground = $self->{'_foreground_gdk'}
157             = Gtk2::Gdk::Color->parse($foreground);
158             }
159             return $foreground;
160             }
161             if ($pname eq 'cursor_name') {
162             my $cursor = $self->{'cursor'};
163             if (blessed($cursor)) {
164             $cursor = $cursor->type;
165             # think prefer undef over cursor-is-pixmap for the get()
166             if ($cursor eq 'cursor-is-pixmap') {
167             undef $cursor;
168             }
169             }
170             return $cursor;
171             }
172             if ($pname eq 'cursor_object') {
173             my $cursor = $self->{'cursor'};
174             return (blessed($cursor)
175             && $cursor->isa('Gtk2::Gdk::Cursor')
176             && $cursor);
177             }
178              
179             return $self->{$pname};
180             }
181              
182             sub SET_PROPERTY {
183             my ($self, $pspec, $newval) = @_;
184             my $pname = $pspec->get_name;
185             ### Lasso SET_PROPERTY(): $pname
186              
187             if ($pname =~ /^foreground/) {
188             # must copy if 'foreground_gdk' since $newval points to a malloced
189             # copy or something, copy scalar 'foreground' too just in case
190             if (blessed($newval) && $newval->isa('Gtk2::Gdk::Color')) {
191             $newval = $newval->copy;
192             }
193             if ($self->{'drawn'}) { _draw ($self); } # undraw old
194             delete $self->{'gc'}; # discard old colour gc
195             $self->{'foreground'} = $newval;
196             if ($self->{'active'}) { _draw ($self); } # draw new colour
197             $self->notify('foreground');
198             $self->notify('foreground-name');
199             $self->notify('foreground-gdk');
200             return;
201             }
202             if ($pname =~ /^cursor/) {
203             # copy boxed GdkCursor in case the caller frees it, and in particular
204             # for $pname eq 'cursor_object' it might be freed immediately by the
205             # GValue call-out stuff
206             if (blessed($newval) && $newval->isa('Gtk2::Gdk::Cursor')) {
207             $newval = $newval->copy;
208             }
209             $self->{'cursor'} = $newval;
210             _update_widgetcursor ($self);
211             $self->notify('cursor');
212             $self->notify('cursor-name');
213             $self->notify('cursor-object');
214             return;
215             }
216              
217             my $oldval = $self->{$pname};
218             if ($pname eq 'widget') {
219             my $active = $self->{'active'};
220             my $button = $self->{'button'};
221             _end ($self);
222             $self->{$pname} = $newval; # per default GET_PROPERTY
223             my $widget = $newval;
224              
225             Scalar::Util::weaken ($self->{'widget'});
226             $self->{'style_sig'} = $widget && Glib::Ex::SignalIds->new
227             ($widget,
228             $widget->signal_connect (style_set => \&_do_style_set,
229             Gtk2::Ex::Xor::_ref_weak($self)));
230             delete $self->{'gc'}; # new colours etc in new widget
231              
232             require Gtk2::Ex::WidgetEvents;
233             $self->{'wevents'} = Gtk2::Ex::WidgetEvents->new
234             ($widget,
235             ['button-motion-mask',
236             'button-release-mask']);
237              
238             # preserve activeness onto new widget
239             if ($active) {
240             $self->start;
241             # keep button number to let _do_button_release match it
242             $self->{'button'} = $button;
243             }
244             return;
245             }
246              
247             if ($pname eq 'active') {
248             if ($newval && ! $oldval) {
249             $self->start;
250             } elsif ($oldval && ! $newval) {
251             $self->abort;
252             }
253              
254             }
255             }
256              
257             # 'style-set' signal handler on widget
258             sub _do_style_set {
259             my ($widget, $prev_style, $ref_weak_self) = @_;
260             my $self = $$ref_weak_self || return;
261             delete $self->{'gc'}; # for new colour
262             }
263              
264             sub start {
265             my ($self, $event) = @_;
266             my $widget = $self->{'widget'} || croak 'Lasso has no widget';
267             ### Lasso start()
268              
269             my $window = $widget->Gtk2_Ex_Xor_window
270             || croak 'Lasso->start(): unrealized widget not (yet) supported';
271              
272             my $want_grab = 1;
273             $self->{'button'} = 0;
274             if (blessed($event) && $event->can('button')) {
275             ### button: $event->button
276             $self->{'button'} = $event->button;
277              
278             # Passive grab from the button is enough, so normally $want_grab false
279             # here. But if the button press was in some other widget then the grab
280             # there is no good, must have $want_grab true in that case.
281             $want_grab = (refaddr(Gtk2->get_event_widget($event)) != refaddr($widget));
282             }
283             if ($want_grab && ! $self->{'grabbed'}) {
284             my $status = Gtk2::Gdk->pointer_grab
285             ($window,
286             0, # owner events
287             ['pointer-motion-mask', 'button-release-mask'],
288             undef, # no confine window
289             undef, # cursor
290             _event_time_maybe($event));
291             ### pointer_grab: $status
292             if ($status eq 'success') {
293             $self->{'grabbed'} = 1;
294             } else {
295             carp "Lasso->start(): cannot grab pointer: $status";
296             }
297             }
298              
299             if ($self->{'active'}) {
300             ### already active
301             return;
302             }
303              
304             $self->{'active'} = 1;
305             _update_widgetcursor ($self);
306              
307             my @dynamic;
308             $self->{'dynamic_setups'} = \@dynamic;
309             my $ref_weak_self = Gtk2::Ex::Xor::_ref_weak ($self);
310              
311             push @dynamic, Glib::Ex::SignalIds->new
312             ($widget,
313             $widget->signal_connect (motion_notify_event => \&_do_motion_notify,
314             $ref_weak_self),
315             $widget->signal_connect (button_release_event => \&_do_button_release,
316             $ref_weak_self),
317             $widget->signal_connect (grab_broken_event => \&_do_grab_broken,
318             $ref_weak_self),
319             $widget->signal_connect_after (expose_event => \&_do_expose,
320             $ref_weak_self),
321             $widget->signal_connect_after (size_allocate => \&_do_size_allocate,
322             $ref_weak_self));
323              
324             require Gtk2::Ex::KeySnooper;
325             push @dynamic, Gtk2::Ex::KeySnooper->new
326             (\&_do_key_snooper, $ref_weak_self);
327              
328             my ($x, $y) = (blessed($event) && $event->can('x')
329             ? Gtk2::Ex::Xor::_event_widget_coords ($widget, $event)
330             : $widget->get_pointer);
331             ### initial "$x,$y"
332              
333             $self->{'x1'} = $x+1; # always initial "moved" emission
334             $self->{'y1'} = $y;
335             $self->{'x2'} = $x;
336             $self->{'y2'} = $y;
337             $self->{'drawn'} = 0;
338             $self->notify ('active');
339             _maybe_move ($self, $x,$y, $x,$y);
340             }
341              
342             sub end {
343             my ($self, $event) = @_;
344             if (! $self->{'active'}) { return; }
345              
346             if (blessed($event)
347             && ($event->isa('Gtk2::Gdk::Event::Button')
348             || $event->isa('Gtk2::Gdk::Event::Motion'))) {
349             _do_motion_notify ($self->{'widget'}, $event, \$self);
350             }
351             _end ($self, $event);
352             $self->notify ('active');
353             $self->signal_emit ('ended',
354             $self->{'x1'}, $self->{'y1'},
355             $self->{'x2'}, $self->{'y2'});
356             }
357              
358             sub abort {
359             my ($self, $event) = @_;
360             ### Lasso abort(): $self->{'active'}
361             if (! $self->{'active'}) { return; }
362             _end ($self, $event);
363             $self->notify ('active');
364             $self->signal_emit ('aborted');
365             }
366              
367             sub _end {
368             my ($self, $event) = @_;
369             $self->{'active'} = 0;
370             delete $self->{'dynamic_setups'};
371             _update_widgetcursor ($self);
372              
373             if ($self->{'drawn'}) {
374             ### undraw
375             _draw ($self);
376             $self->{'drawn'} = 0;
377             }
378             if (delete $self->{'grabbed'}) {
379             ### ungrab
380             Gtk2::Gdk->pointer_ungrab (_event_time_maybe ($event));
381             }
382             }
383              
384             sub _update_widgetcursor {
385             my ($self) = @_;
386              
387             if ($self->{'active'} && $self->{'cursor'}) {
388             if ($self->{'wcursor'}) {
389             $self->{'wcursor'}->cursor ($self->{'cursor'});
390             } else {
391             require Gtk2::Ex::WidgetCursor;
392             $self->{'wcursor'}
393             = Gtk2::Ex::WidgetCursor->new (widget => $self->{'widget'},
394             cursor => $self->{'cursor'},
395             active => 1);
396             }
397             } else {
398             delete $self->{'wcursor'};
399             }
400             }
401              
402             # 'expose' signal handler on widget
403             sub _do_expose {
404             my ($widget, $event, $ref_weak_self) = @_;
405             my $self = $$ref_weak_self || return 0; # Gtk2::EVENT_PROPAGATE
406             ### lasso _do_expose() "$widget", active=" . ($self->{'active'}||0)
407             if ($self->{'drawn'}) {
408             _draw ($self, $event->region);
409             }
410             return 0; # Gtk2::EVENT_PROPAGATE
411             }
412              
413             # 'motion-notify' signal handler on widget, and also called for $lasso->end
414             # if it gets a button or motion event
415             sub _do_motion_notify {
416             my ($widget, $event, $ref_weak_self) = @_;
417             my $self = $$ref_weak_self || return 0; # Gtk2::EVENT_PROPAGATE
418              
419             _maybe_move ($self,
420             $self->{'x1'}, $self->{'y1'},
421             Gtk2::Ex::Xor::_event_widget_coords ($widget, $event));
422             return 0; # Gtk2::EVENT_PROPAGATE
423             }
424              
425             # 'size-allocate' signal on the widget.
426             #
427             # The effect of _maybe_move() is to re-clamp the x1,y1 position, in case
428             # it's now outside the allocated area, and to recheck the pointer position
429             # at x2,y2 in case it's now outside, or now back inside, the allocated area
430             #
431             # x1,y1 is not moved (only reclamped), so if $widget moves then that
432             # x1,y1 stays at the same position relative to the widget top-left 0,0.
433             # There's probably other sensible things to do with it, like anchor to a
434             # different edge, or a proportion of the size, etc, but a widget move during
435             # a lasso should be rare, so as long as it doesn't catch fire it should be
436             # fine.
437             #
438             sub _do_size_allocate {
439             my ($widget, $alloc, $ref_weak_self) = @_;
440             my $self = $$ref_weak_self || return;
441             _maybe_move ($self,
442             $self->{'x1'}, $self->{'y1'},
443             $widget->get_pointer);
444             }
445              
446              
447             # 'button-release-event' handler on the widget
448             sub _do_button_release {
449             my ($widget, $event, $ref_weak_self) = @_;
450             my $self = $$ref_weak_self || return 0; # Gtk2::EVENT_PROPAGATE
451             ### Lasso _do_button_release(): $event->button
452             ### want: $self->{'button'}
453             if ($event->button == $self->{'button'}) {
454             $self->end ($event);
455             }
456             return 0; # Gtk2::EVENT_PROPAGATE
457             }
458              
459             sub _maybe_move {
460             my ($self, $x1,$y1, $x2,$y2) = @_;
461             my $widget = $self->{'widget'};
462              
463             ($x1,$y1) = _widget_constrain_xy ($widget, $x1,$y1);
464             ($x2,$y2) = _widget_constrain_xy ($widget, $x2,$y2);
465              
466             if ( $x1 == $self->{'x1'}
467             && $y1 == $self->{'y1'}
468             && $x2 == $self->{'x2'}
469             && $y2 == $self->{'y2'}) {
470             return;
471             }
472             $self->{'pending_x1'} = $x1;
473             $self->{'pending_y1'} = $y1;
474             $self->{'pending_x2'} = $x2;
475             $self->{'pending_y2'} = $y2;
476              
477             $self->{'sync_call'} ||= do {
478             require Gtk2::Ex::SyncCall;
479             Gtk2::Ex::SyncCall->sync ($widget,
480             \&_sync_call_handler,
481             Gtk2::Ex::Xor::_ref_weak ($self));
482             };
483             }
484             sub _sync_call_handler {
485             my ($ref_weak_self) = @_;
486             my $self = $$ref_weak_self || return;
487             $self->{'sync_call'} = undef;
488             if (! $self->{'active'}) { return; }
489              
490             if ($self->{'drawn'}) {
491             #### undraw
492             _draw ($self);
493             }
494             $self->{'x1'} = $self->{'pending_x1'};
495             $self->{'y1'} = $self->{'pending_y1'};
496             $self->{'x2'} = $self->{'pending_x2'};
497             $self->{'y2'} = $self->{'pending_y2'};
498             _draw ($self);
499             $self->signal_emit ('moved',
500             $self->{'x1'}, $self->{'y1'},
501             $self->{'x2'}, $self->{'y2'});
502             }
503              
504             sub _draw {
505             my ($self, $clip_region) = @_;
506             #### _draw: $clip_region
507             my $widget = $self->{'widget'};
508              
509             my $win = $widget->Gtk2_Ex_Xor_window
510             || return; # possible undef when unrealized in destruction
511             my ($off_x, $off_y) = ($win != $widget->window
512             ? $win->get_position : (0, 0));
513             my $gc = ($self->{'gc'} ||= do {
514             Gtk2::Ex::Xor::shared_gc (widget => $widget,
515             foreground_xor => $self->{'foreground'},
516             background_xor => 0, # no change
517             line_width => ($self->{'line_width'} || 0),
518             line_style => ($self->{'line_style'}
519             || DEFAULT_LINE_STYLE));
520             });
521              
522             if ($clip_region) { $gc->set_clip_region ($clip_region); }
523             Gtk2::Ex::GdkBits::draw_rectangle_corners
524             ($win, $gc,
525             0, # unfilled
526             $self->{'x1'} - $off_x, $self->{'y1'} - $off_y,
527             $self->{'x2'} - $off_x, $self->{'y2'} - $off_y);
528             if ($clip_region) { $gc->set_clip_region (undef); }
529             $self->{'drawn'} = 1;
530             }
531              
532             sub swap_corners {
533             my ($self) = @_;
534             if (! $self->{'active'}) { return; }
535              
536             my $widget = $self->{'widget'};
537             if (! Gtk2::Gdk::Display->can('warp_pointer')) {
538             return;
539             }
540              
541             my $x1 = $self->{'x1'};
542             my $y1 = $self->{'y1'};
543             _maybe_move ($self,
544             $self->{'x2'}, $self->{'y2'},
545             $self->{'x1'}, $self->{'y1'});
546              
547             require Gtk2::Ex::WidgetBits;
548             Gtk2::Ex::WidgetBits::warp_pointer ($widget, $x1, $y1);
549             }
550              
551              
552             # KeySnooper callback
553             sub _do_key_snooper {
554             my ($target_widget, $event, $ref_weak_self) = @_;
555             my $self = $$ref_weak_self || return 0; # Gtk2::EVENT_PROPAGATE
556             # ignore key releases
557             $event->type eq 'key-press' || return 0; # Gtk2::EVENT_PROPAGATE
558             ### Lasso _do_key_snooper(): $event->keyval
559              
560             if ($event->keyval == Gtk2::Gdk->keyval_from_name('Escape')) {
561             $self->abort;
562             return 1; # Gtk2::EVENT_STOP
563              
564             } elsif ($event->keyval == Gtk2::Gdk->keyval_from_name('space')) {
565             $self->swap_corners;
566             return 1; # Gtk2::EVENT_STOP
567              
568             } elsif ($event->keyval == Gtk2::Gdk->keyval_from_name('Return')) {
569             $self->end ($event);
570             return 1; # Gtk2::EVENT_STOP
571             }
572              
573             return 0; # Gtk2::EVENT_PROPAGATE
574             }
575              
576             # 'grab-broken' signal handler on the widget
577             sub _do_grab_broken {
578             my ($widget, $event, $ref_weak_self) = @_;
579             my $self = $$ref_weak_self || return 0; # Gtk2::EVENT_PROPAGATE
580             ### Lasso _do_grab_broken()
581              
582             $self->{'grabbed'} = 0;
583              
584             # if lassoing with a button drag then a break of the implicit grab almost
585             # certainly means something else is now happening and lassoing should stop
586             #
587             # if lassoing from a non-button start then a break of our explicit grab
588             # also almost certain means something else is happening and lassoing
589             # should stop
590             #
591             # abort() doesn't use $event for anything, since the grab is already gone,
592             # but pass it for the sake of completeness
593             #
594             $self->abort ($self, $event);
595              
596             return 0; # Gtk2::EVENT_PROPAGATE
597             }
598              
599             #------------------------------------------------------------------------------
600             # generic helpers
601              
602             sub _widget_constrain_xy {
603             my ($widget, $x, $y) = @_;
604             my $alloc = $widget->allocation;
605             return (max (0, min ($alloc->width-1, $x)),
606             max (0, min ($alloc->height-1, $y)));
607             }
608              
609             # $window->get_size is the most recent configure-event report, it doesn't
610             # make a server round-trip
611             sub _window_constrain_xy {
612             my ($window, $x, $y) = @_;
613             my ($width, $height) = $window->get_size;
614             return (max (0, min ($width-1, $x)),
615             max (0, min ($height-1, $y)));
616             }
617              
618             # Return a server timestamp from $event, if it's not undef and if it's an
619             # event type which has a timestamp.
620             sub _event_time_maybe {
621             my ($event) = @_;
622             if (blessed($event) && $event->can('time')) {
623             return $event->time;
624             } else {
625             return Gtk2::GDK_CURRENT_TIME;
626             }
627             }
628              
629              
630             1;
631             __END__