File Coverage

blib/lib/Gtk2/Ex/DateSpinner/PopupForEntry.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2013 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-DateSpinner.
4             #
5             # Gtk2-Ex-DateSpinner 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-DateSpinner 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-DateSpinner. If not, see .
17              
18             package Gtk2::Ex::DateSpinner::PopupForEntry;
19 1     1   792 use 5.008;
  1         4  
  1         39  
20 1     1   6 use strict;
  1         2  
  1         34  
21 1     1   5 use warnings;
  1         2  
  1         32  
22 1     1   640 use Gtk2;
  0            
  0            
23             use List::Util qw(min max);
24              
25             our $VERSION = 9;
26              
27             use Glib::Object::Subclass
28             'Gtk2::Window',
29             properties => [ Glib::ParamSpec->object
30             ('entry',
31             'Entry widget',
32             'The Gtk2::Entry widget to read/write with the popup.',
33             'Gtk2::Widget',
34             Glib::G_PARAM_READWRITE()),
35             ];
36              
37             sub INIT_INSTANCE {
38             my ($self) = @_;
39              
40             $self->set_decorated (0);
41             $self->set_flags('can-focus');
42              
43             my $hbox = Gtk2::HBox->new;
44             $self->add ($hbox);
45              
46             require Gtk2::Ex::DateSpinner;
47             my $datespinner = Gtk2::Ex::DateSpinner->new;
48             $self->{'datespinner'} = $datespinner;
49             $hbox->pack_start ($datespinner, 1,1,0);
50              
51             foreach my $field ('year', 'month', 'day') {
52             $datespinner->{$field}->signal_connect_after (activate => \&_do_activate);
53             }
54             $datespinner->signal_connect ('notify::value' => \&_do_datespinner_changed);
55              
56             my $ok = Gtk2::Button->new_from_stock ('gtk-ok');
57             $ok->set_flags('can-default');
58             $ok->signal_connect (clicked => \&_do_activate);
59             $hbox->pack_start ($ok, 0,0,0);
60              
61             my $cancel = Gtk2::Button->new_from_stock ('gtk-cancel');
62             $cancel->signal_connect (clicked => \&_do_cancel_button);
63             $hbox->pack_start ($cancel, 0,0,0);
64              
65             my $accelgroup = Gtk2::AccelGroup->new;
66             $self->add_accel_group ($accelgroup);
67             $accelgroup->connect (Gtk2::Gdk->keyval_from_name('Escape'), [], [],
68             \&_do_accel_cancel);
69              
70             $datespinner->{'day'}->grab_focus;
71             $hbox->show_all;
72              
73             # {
74             # ### year flag: $datespinner->{'year'}->flags
75             # ### ok flags : $ok->flags
76             # ### cancel flags: $cancel->flags
77             # $self->signal_connect ('notify::visible' => sub {
78             # print "PopupForEntry: notify:visible changed\n";
79             # });
80             # $self->signal_connect (map => sub {
81             # print "PopupForEntry: map (request)\n";
82             # });
83             # $self->signal_connect (map_event => sub {
84             # print "PopupForEntry: map_event\n";
85             # return 0; # Gtk2::EVENT_PROPAGATE
86             # });
87             # }
88             }
89              
90             # DEBUG
91             # {
92             # no warnings 'once';
93             # *FINALIZE_INSTANCE = sub {
94             # print "PopupForEntry FINALIZE_INSTANCE\n";
95             # };
96             # }
97              
98             # A 'border' decoration is probably worthwhile, but $toplevel->move doesn't
99             # seem to be based on window frame position in fvwm. Dunno who's at fault,
100             # but no decoration is easier to get right in all wm's.
101             #
102             # signals => { realize => \&_do_realize },
103             # sub _do_realize {
104             # my ($self) = @_;
105             # $self->signal_chain_from_overridden;
106             # $self->window->set_decorations (['border']);
107             # }
108              
109             sub SET_PROPERTY {
110             my ($self, $pspec, $newval) = @_;
111             my $pname = $pspec->get_name;
112             $self->{$pname} = $newval;
113              
114             if ($pname eq 'entry') {
115             my $entry = $newval;
116             Scalar::Util::weaken ($self->{'entry'});
117              
118             $self->{'entry_ids'} = $entry && do {
119             require Scalar::Util;
120             my $ref_weak_self = \$self;
121             Scalar::Util::weaken ($ref_weak_self);
122              
123             require Glib::Ex::SignalIds;
124             Glib::Ex::SignalIds->new
125             ($entry,
126             $entry->signal_connect (size_allocate => \&_do_position,
127             $ref_weak_self),
128             $entry->signal_connect (changed => \&_do_entry_changed,
129             $ref_weak_self),
130             $entry->signal_connect (editing_done => \&_do_entry_editing_done,
131             $ref_weak_self),
132             $entry->signal_connect (destroy => \&_do_entry_destroy,
133             $ref_weak_self))
134             };
135             }
136             }
137              
138             sub _do_entry_destroy {
139             my ($entry, $ref_weak_self) = @_;
140             my $self = $$ref_weak_self || return;
141             ### PopupForEntry _do_entry_destroy(), destroy self too ...
142             $self->destroy;
143             }
144              
145             sub _do_entry_changed {
146             my ($entry, $ref_weak_self) = @_;
147             ### PopupForEntry _do_entry_changed() ...
148             my $self = $$ref_weak_self || return;
149             if ($self->{'change_in_progress'}) { return; }
150              
151             local $self->{'change_in_progress'} = 1;
152             my $value = $entry->get_text;
153             if ($value =~ /^[0-9]+-[0-9]+-[0-9]+$/) {
154             my $datespinner = $self->{'datespinner'};
155             $datespinner->set (value => $value);
156             }
157             }
158             sub _do_datespinner_changed {
159             my ($datespinner, $pspec) = @_;
160             my $self = $datespinner->get_toplevel;
161             ### PopupForEntry _do_datespinner_value() to: "$self->{'entry'}"
162             if ($self->{'change_in_progress'}) { return; }
163             my $entry = $self->{'entry'} || return;
164              
165             local $self->{'change_in_progress'} = 1;
166             $entry->set_text ($datespinner->get_value);
167             }
168              
169             sub _do_entry_editing_done {
170             my ($entry, $ref_weak_self) = @_;
171             my $self = $$ref_weak_self || return;
172             ### PopupForEntry: _do_entry_editing_done(), hide popup ...
173             $self->hide;
174             }
175              
176             # 'activate' on the spin buttons
177             # 'clicked' on the 'gtk-ok' button
178             #
179             sub _do_activate {
180             my ($widget) = @_;
181             ### PopupForEntry _do_activate() ...
182             my $self = $widget->get_toplevel;
183              
184             $self->hide;
185             my $entry = $self->{'entry'} || return;
186             { local $self->{'change_in_progress'} = 1;
187             my $datespinner = $self->{'datespinner'};
188             $entry->set_text ($datespinner->get_value); }
189             $entry->activate;
190             }
191              
192             # Escape key from the Gtk2::AccelGroup
193             # func per GtkAccelGroupActivate
194             #
195             sub _do_accel_cancel {
196             my ($accelgroup, $widget, $keyval, $modifiers) = @_;
197             ### PopupForEntry _do_accel_cancel() ...
198             _do_cancel_button ($widget);
199             return 1; # accel handled
200             }
201              
202             # 'clicked' on the cancel buttons
203             #
204             sub _do_cancel_button {
205             my ($button) = @_;
206             ### PopupForEntry _do_cancel_button() ...
207             my $self = $button->get_toplevel;
208              
209             $self->hide;
210             my $entry = $self->{'entry'} || return; # maybe already gone
211             $entry->{'editing_cancelled'} = 1;
212             $entry->editing_done;
213             $entry->remove_widget;
214             }
215              
216             # 'size-allocate' on the entry widget
217             #
218             # Finding the right time to position the popup is a bit painful.
219             # GtkTreeView and GtkIconView add the editable to themselves and map it with
220             # default height 1, then focus to it, then size_allocate it up to the cell
221             # size. So to position underneath it we only know the right height after
222             # that size-allocate. Positioning earlier at the map or the focus state
223             # ends up with an unattractive visible move of the popup window downwards.
224             #
225             # FIXME: Depending on the sequence of actions in TreeView is a bit nasty,
226             # maybe it'd at least be worth a recheck of the position on getting to
227             # Glib::Idle after a start_editing.
228             #
229             sub _do_position {
230             my ($entry) = @_;
231             my $ref_weak_self = $_[-1];
232             my $self = $$ref_weak_self || return;
233              
234             # DEBUG
235             # {
236             # my $hint = $entry->signal_get_invocation_hint;
237             # print "_do_position for ",$hint->{'signal_name'},
238             # " visible=",($entry->get('visible')?"yes":"no"),
239             # " mapped=",($entry->mapped?"yes":"no"),
240             # "\n";
241             # if (my $win = $entry->window) {
242             # my ($width,$height) = $win->get_size;
243             # print " window ${width}x$height\n";
244             # }
245             # my $alloc = $entry->allocation;
246             # print " alloc ",$alloc->width,"x",$alloc->height,"\n";
247             # }
248              
249             my $toplevel = $entry->get_ancestor ('Gtk2::Window'); # undef if no toplevel
250             $self->set_transient_for ($toplevel);
251              
252             my $win = $entry->window;
253             if ($win) {
254             _window_move_underneath ($self, $entry);
255             }
256             $self->set (visible => defined $win);
257             }
258              
259             # _window_move_underneath ($toplevel, $widget)
260             # $toplevel is a Gtk2::Window widget, $widget is any realized widget.
261             #
262             # Move $toplevel with $toplevel->move to put it:
263             # - underneath $widget, if it fits,
264             # - otherwise above, if it fits,
265             # - otherwise at the bottom of the screen, but limited to y=0 if higher
266             # than the whole screen
267             #
268             # Horizontally, $toplevel is positioned lined up with the left of $widget,
269             # but pushed to the left so as not to extend past the right edge of the
270             # screen, but limited to x=0 if wider than the whole screen.
271             #
272             sub _window_move_underneath {
273             my ($toplevel, $widget) = @_;
274             ### _window_move_underneath() ...
275              
276             require Gtk2::Ex::WidgetBits;
277             my ($x, $y) = Gtk2::Ex::WidgetBits::get_root_position ($widget);
278             my $alloc = $widget->allocation;
279             my $height = $alloc->height;
280              
281             my $req; # either Gtk2::Gdk::Rectangle or Gtk2::Requisition
282             if (my $win = $toplevel->window) {
283             $req = $win->get_frame_extents;
284              
285             ### using get_frame_extents: $req->x.",".$req->y." ".$req->width."x".$req->height
286             ### cf get_size: join('x',$win->get_size)
287             ### cf allocation: $toplevel->allocation->width.'x'.$toplevel->allocation->height
288              
289             } else {
290             ### unrealized, using size_request() ...
291             $req = $toplevel->size_request;
292             }
293              
294             my $rootwin = $toplevel->get_root_window;
295             my ($root_width, $root_height) = $rootwin->get_size;
296             ### toplevel: $req->width."x".$req->height
297             ### under rect: "$x,$y ${width}x${height}"
298              
299             my $win_x = max (0, min ($root_width - $req->width, $x, ));
300              
301             my $win_y = $y + $height;
302             if ($win_y + $req->height > $root_height) {
303             # below is past bottom of screen, try above
304             $win_y = $y - $req->height;
305             if ($win_y < 0) {
306             # above is past top of screen, clamp to top
307             $win_y = 0;
308             }
309             }
310              
311             # 'gravity' (GdkGravity) doesn't really help to position above a selected
312             # position for a one-off move, it only works if set and left. Could be ok
313             # since this popup is supposed to be private, but a bit easier to stay
314             # default north-west for now.
315             #
316             $toplevel->move ($win_x, $win_y);
317             }
318              
319             1;
320             __END__