File Coverage

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