File Coverage

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


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2011 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-MenuView.
4             #
5             # Gtk2-Ex-MenuView 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-MenuView 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-MenuView. If not, see .
17              
18             package Gtk2::Ex::MenuView;
19 2     2   1875 use 5.008;
  2         6  
  2         169  
20 2     2   12 use strict;
  2         5  
  2         65  
21 2     2   11 use warnings;
  2         2  
  2         64  
22 2     2   10 use Carp;
  2         3  
  2         175  
23 2     2   1942 use Gtk2 1.200; # for GDK_PRIORITY_REDRAW, and bug fixes probably
  0            
  0            
24              
25             use Glib::Ex::SignalIds;
26             use Glib::Ex::SourceIds;
27             use Glib::Ex::SignalBits;
28             use Gtk2::Ex::MenuView::Menu;
29              
30             # uncomment this to run the ### lines
31             #use Smart::Comments;
32              
33             our $VERSION = 4;
34              
35             use constant _submenu_class => 'Gtk2::Ex::MenuView::Menu';
36              
37             BEGIN {
38             Glib::Type->register_enum ('Gtk2::Ex::MenuView::WantActivate',
39             no => 0,
40             leaf => 1,
41             all => 2);
42             Glib::Type->register_enum ('Gtk2::Ex::MenuView::WantVisible',
43             no => 0,
44             show => 1,
45             show_all => 2);
46             }
47              
48             use Glib::Object::Subclass
49             _submenu_class(),
50             signals => { 'item-create-or-update'
51             => { param_types => ['Gtk2::MenuItem',
52             'Gtk2::TreeModel',
53             'Gtk2::TreePath',
54             'Gtk2::TreeIter'],
55             return_type => 'Gtk2::MenuItem',
56             flags => ['action','run-last'],
57             accumulator => \&Glib::Ex::SignalBits::accumulator_first_defined,
58             },
59             'separator-create-or-update'
60             => { param_types => ['Gtk2::MenuItem',
61             'Gtk2::TreeModel',
62             'Gtk2::TreePath',
63             'Gtk2::TreeIter'],
64             return_type => 'Gtk2::MenuItem',
65             flags => ['action'],
66             accumulator => \&Glib::Ex::SignalBits::accumulator_first_defined,
67             },
68             activate
69             => { param_types => ['Gtk2::MenuItem',
70             'Gtk2::TreeModel',
71             'Gtk2::TreePath',
72             'Gtk2::TreeIter'],
73             return_type => undef },
74             },
75             properties => [ Glib::ParamSpec->object
76             ('model',
77             'Model',
78             'TreeModel to display.',
79             'Gtk2::TreeModel',
80             Glib::G_PARAM_READWRITE),
81              
82             Glib::ParamSpec->enum
83             ('want-activate',
84             'Want activate',
85             'Whether to connect and generate a unified activate signal.',
86             'Gtk2::Ex::MenuView::WantActivate',
87             'leaf',
88             Glib::G_PARAM_READWRITE),
89              
90             Glib::ParamSpec->enum
91             ('want-visible',
92             'Want visible',
93             'Whether to automatically make items visible.',
94             'Gtk2::Ex::MenuView::WantVisible',
95             'show_all',
96             Glib::G_PARAM_READWRITE),
97              
98             ];
99              
100              
101             # TODO:
102             #
103             # dirty 0, 1=item, 2=separator
104             #
105             # current_item_at_indices ...
106             # $menu->get_model_items
107             # $menu->model_items_array
108              
109             # $menuview->menu_at_path
110             # $menuview->model_items
111             # $menuview->foreach_model_item
112             # item_get_indices
113              
114             # mnemonics
115             # accel key from model example
116             # circular protection pay attention to model changes ?
117              
118             #------------------------------------------------------------------------------
119              
120             # sub INIT_INSTANCE {
121             # my ($self) = @_;
122             # }
123              
124             sub SET_PROPERTY {
125             my ($self, $pspec, $newval) = @_;
126             my $pname = $pspec->get_name;
127             $self->{$pname} = $newval; # per default GET_PROPERTY
128              
129             if ($pname eq 'model') {
130             my $model = $newval;
131             Scalar::Util::weaken (my $weak_self = $self);
132             my $ref_weak_self = \$weak_self;
133             $self->{'model_ids'} = $model && Glib::Ex::SignalIds->new
134             ($model,
135             $model->signal_connect (row_changed => \&_do_row_changed,
136             $ref_weak_self),
137             $model->signal_connect (row_deleted => \&_do_row_deleted,
138             $ref_weak_self),
139             $model->signal_connect (row_inserted => \&_do_row_inserted,
140             $ref_weak_self),
141             $model->signal_connect (rows_reordered => \&_do_rows_reordered,
142             $ref_weak_self),
143             $model->signal_connect
144             (row_has_child_toggled => \&_do_row_has_child_toggled, $ref_weak_self));
145             _dirty_all_menus ($self);
146             }
147             }
148              
149             sub _freshen_item {
150             my ($self, $menu, $menu_path, $i) = @_;
151             ### _freshen_item() number: $i
152             my $model = $self->{'model'} || return;
153              
154             if (delete $menu->{'all_dirty'}) {
155             ### all_dirty, make dirty array
156             my $menu_iter = ($menu_path->get_depth
157             ? $model->get_iter($menu_path) || do {
158             ### no iter for menu_path
159             return;
160             }
161             : undef);
162             my $len = $model->iter_n_children ($menu_iter);
163             $menu->{'dirty'} ||= [];
164             @{$menu->{'dirty'}} = ((Gtk2::Ex::MenuView::Menu::_DIRTY_ITEM()
165             | Gtk2::Ex::MenuView::Menu::_DIRTY_SEPARATOR())
166             x $len);
167             }
168              
169             ### dirty_bits: $menu->{'dirty'}->[$i]
170             my $dirty_bits = delete $menu->{'dirty'}->[$i] || do {
171             ### not dirty, no freshen needed
172             return;
173             };
174             # still dirty if recursive freshens such as item_at_indices() look, but
175             # cleared when this _freshen_item() returns
176             local $menu->{'dirty'}->[$i] = $dirty_bits;
177              
178             my $item_path = $menu_path->copy;
179             $item_path->append_index ($i);
180              
181             my $item_iter = $model->get_iter($item_path) || do {
182             ### no iter for item_path
183             return;
184             };
185              
186             my $key = $item_path->to_string;
187             ### in progress: $self->{'item_update_in_progress'}
188             my $in_progress = $self->{'item_update_in_progress'} || {};
189             if ($in_progress->{$key}) {
190             ### croak for recursion
191             croak "Recursive item create or update for path=$key";
192             }
193             local $self->{'item_update_in_progress'} = { %$in_progress, $key => 1 };
194             ### flag in_progress to: $self->{'item_update_in_progress'}
195              
196             my $children = ($menu->{'children'} ||= []);
197             my $item = $children->[$i];
198             my $leaf = ! $model->iter_has_child ($item_iter);
199              
200             my ($old_separator, $submenu);
201             if ($item) {
202             $old_separator = $item->{'Gtk2::Ex::MenuView.separator'};
203             $submenu = $item->get_submenu;
204             }
205              
206             if ($dirty_bits & Gtk2::Ex::MenuView::Menu::_DIRTY_ITEM()) {
207             my $old_item = $item;
208             $item = $self->signal_emit ('item-create-or-update',
209             $old_item,
210             $model,
211             $item_path,
212             $item_iter);
213             ### _item_create: $item
214              
215             unless ($item && $old_item && $item == $old_item) {
216             if ($old_item) {
217             $menu->_remove_item ($old_item);
218             delete $children->[$i]; # so _item_index_to_menu_pos() doesn't see it
219             undef $old_separator; # destroyed by _remove_item()
220             }
221             if ($item) {
222             if ((my $want_activate = $self->get('want-activate')) ne 'no') {
223             # Connect to both leaf and non-leaf rows and filter in the handler,
224             # since a row might gain or lose a submenu at any time. There won't
225             # be many non-leafs so not much is wasted by this.
226             $item->signal_connect (activate => \&_do_item_activate);
227             }
228             if ((my $want_visible = $self->get('want-visible')) ne 'no') {
229             $item->$want_visible; # 'show' or 'show_all'
230             }
231             $menu->insert ($item, _item_index_to_menu_pos($menu,$i));
232             $children->[$i] = $item;
233             }
234             }
235             }
236              
237             if ($item) {
238             if ($leaf) {
239             undef $submenu;
240             } else {
241             if (! $submenu) {
242             ### create submenu
243             $submenu = $self->_submenu_class->new;
244             $submenu->{'all_dirty'} = 1;
245             }
246             }
247             $item->set_submenu ($submenu);
248             }
249              
250             if ($item
251             && ($dirty_bits & Gtk2::Ex::MenuView::Menu::_DIRTY_SEPARATOR())) {
252             my $item_iter = $model->get_iter($item_path) || return;
253             my $separator = $self->signal_emit ('separator-create-or-update',
254             $old_separator,
255             $model,
256             $item_path,
257             $item_iter);
258             unless ($old_separator && $separator && $old_separator == $separator) {
259             if ($old_separator) {
260             $menu->remove ($old_separator);
261             $old_separator->destroy;
262             }
263             if ($separator) {
264             my $pos = _item_index_to_menu_pos ($menu, $i);
265             $item->{'Gtk2::Ex::MenuView.separator'} = $separator;
266             $menu->insert ($separator, $pos);
267             } else {
268             delete $item->{'Gtk2::Ex::MenuView.separator'};
269             }
270             }
271             }
272              
273             ### freshen return 1
274             return 1;
275             }
276              
277             # 'activate' signal handler on each item child
278             sub _do_item_activate {
279             my ($item) = @_;
280             ### MenuView activate: $item
281              
282             # shouldn't normally get a signal when not within a menu, but allow for
283             # perhaps the model changing without signals yet processed
284             my ($menuview,$model,$path,$iter) = Gtk2::Ex::MenuView->item_get_mmpi($item)
285             or do {
286             ### no model row for activated item
287             return;
288             };
289             if ($menuview->get('want-activate') eq 'leaf'
290             && $model->iter_has_child ($iter)) {
291             ### no activate on leaf
292             return;
293             }
294             $menuview->signal_emit ('activate', $item, $model, $path, $iter);
295             }
296              
297             sub _item_index_to_menu_pos {
298             my ($menu, $i) = @_;
299             ### _item_index_to_menu_pos(): $i
300             ### menu: "@{[$menu->get_children]}"
301             my $children = $menu->{'children'} || return -1;
302             ### children: "@{[grep {defined} @$children]}"
303             my $pos = -1;
304             OUTER: for ( ; $i < @$children; $i++) {
305             if (my $after = $children->[$i]) {
306             $after = $after->{'Gtk2::Ex::MenuView.separator'} || $after;
307              
308             foreach my $child ($menu->get_children) {
309             $pos++;
310             if ($child == $after) { last OUTER; }
311             }
312             ### oops, not found in menu: "$after"
313             ### assert: 0
314             return -1;
315             }
316             }
317             ### _item_index_to_menu_pos(): "$i -> $pos"
318             return $pos;
319             }
320              
321             sub _dirty_all_menus {
322             my ($self) = @_;
323             ### _dirty_all_menus
324              
325             my $menu = $self;
326             my @pending;
327             do {
328             _dirty_menu($menu);
329             push @pending, map { my $submenu = $_->get_submenu;
330             ($submenu ? ($submenu) : ()) }
331             @{$menu->{'children'}};
332             } while ($menu = pop @pending);
333             }
334             # sub _menushellbits_menu_and_submenus {
335             # my ($menu) = @_;
336             # my @pending;
337             # my @ret;
338             # do {
339             # push @ret, $menu;
340             # push @pending, grep {defined} map {$_->get_submenu} $menu->get_children;
341             # } while ($menu = pop @pending);
342             # return @ret;
343             # }
344              
345              
346             #------------------------------------------------------------------------------
347             # dirtiness etc
348              
349             sub _idle_freshen {
350             my ($menu) = @_;
351             if ($menu->mapped) {
352             my $self = $menu->_get_menuview;
353             Scalar::Util::weaken (my $weak_self = $self);
354             $self->{'idle'} ||= Glib::Ex::SourceIds->new
355             (Glib::Idle->add (\&_do_idle, \$weak_self,
356             Gtk2::GTK_PRIORITY_RESIZE - 1)); # just before resize
357             }
358             }
359             sub _do_idle {
360             my ($ref_weak_self) = @_;
361             ### _do_idle
362             my $self = $$ref_weak_self || return;
363             delete $self->{'idle'};
364              
365             my $menu = $self;
366             my @pending;
367             do {
368             if ($menu->mapped) {
369             $menu->_freshen;
370             }
371             push @pending, map {$_->get_submenu || ()} @{$menu->{'children'}};
372             } while ($menu = pop @pending);
373              
374             return 0; # Glib::SOURCE_REMOVE
375             }
376              
377             # mark $menu as all dirty
378             sub _dirty_menu {
379             my ($menu) = @_;
380             delete $menu->{'dirty'};
381             $menu->{'all_dirty'} ||= do {
382             _idle_freshen ($menu);
383             1;
384             }
385             }
386              
387             # mark $menu item number $i as dirty
388             sub _dirty_add {
389             my ($self, $menu, $i, $dirty_bits) = @_;
390             ### _dirty_add(): $i, "$menu", $dirty_bits
391              
392             if (! $menu->{'all_dirty'}) {
393             my $dirty = ($menu->{'dirty'} ||= []);
394             $dirty->[$i] |= do {
395             _idle_freshen ($self);
396             $dirty_bits; # item
397             };
398             }
399             }
400              
401             sub _dirty_item_and_following_separator {
402             my ($self, $menu, $path) = @_;
403              
404             my $i = ($path->get_indices)[-1];
405             _dirty_add ($self, $menu, $i,
406             Gtk2::Ex::MenuView::Menu::_DIRTY_ITEM());
407              
408             if (my $model = $self->{'model'}) {
409             $path = $path->copy;
410             $path->next;
411             if ($model->get_iter($path)) {
412             _dirty_add ($self, $menu, $i+1,
413             Gtk2::Ex::MenuView::Menu::_DIRTY_SEPARATOR());
414             }
415             }
416             }
417              
418             # return item at $path if it currently exists, or undef if not
419             sub _current_item_at_path {
420             my ($self, $path) = @_;
421             my @indices = $path->get_indices
422             or return undef; # empty path
423             my $item;
424             my $menu = $self;
425             for (;;) {
426             $item = $menu->{'children'}->[shift @indices] || return undef;
427             @indices || last;
428             $menu = $item->get_submenu || return undef;
429             }
430             return $item;
431             }
432              
433             # return menu containing the item at $path, if that menu currently exists
434             # (the item itself doesn't have to), or undef if no such menu
435             sub _current_menu_at_path {
436             my ($self, $path) = @_;
437             if ($path->get_depth == 0) { return $self; }
438             my $item = $self->_current_item_at_path($path) || return undef;
439             return $item->get_submenu;
440             }
441              
442             # return menu containing the item at $path, if that menu currently exists
443             # (the item itself doesn't have to), or undef if no such menu
444             sub _current_menu_containing_path {
445             my ($self, $path) = @_;
446             if ($path->get_depth == 1) { return $self; }
447             $path = $path->copy;
448             $path->up;
449             return $self->_current_menu_at_path($path);
450             }
451              
452             #------------------------------------------------------------------------------
453             # model changes
454              
455             # 'row-has-child-toggled' callback from model
456             #
457             # Called only for rows, not when the toplevel goes between empty and
458             # non-empty. FIXME: Not yet documented that an item updates with subrow
459             # emptiness like this.
460             #
461             sub _do_row_has_child_toggled {
462             my ($model, $path, $iter, $ref_weak_self) = @_;
463             ### MenuView row-has-child-toggled: $path->to_string
464             my $self = $$ref_weak_self || return;
465              
466             my $item = $self->_current_item_at_path($path) || return;
467             if ($model->iter_has_child($iter)) {
468             $item->set_submenu (undef);
469             }
470             # update display for rows or no-rows and create submenu if became
471             # non-empty
472             _dirty_add ($self, $item->get_parent, ($path->get_indices)[-1],
473             Gtk2::Ex::MenuView::Menu::_DIRTY_ITEM());
474             }
475              
476             # 'row-changed' callback from model
477             sub _do_row_changed {
478             my ($model, $path, $iter, $ref_weak_self) = @_;
479             ### MenuView row changed: $path->to_string
480             my $self = $$ref_weak_self || return;
481              
482             my $menu = $self->_current_menu_containing_path($path) || do {
483             ### no menu for it currently
484             return;
485             };
486             _dirty_item_and_following_separator ($self, $menu, $path);
487             }
488              
489             # 'row-deleted' callback from model
490             sub _do_row_deleted {
491             my ($model, $path, $ref_weak_self) = @_;
492             ### MenuView row deleted: $path->to_string
493             my $self = $$ref_weak_self || return;
494              
495             my $menu = $self->_current_menu_containing_path($path) || do {
496             ### no menu for this yet
497             return;
498             };
499              
500             my $i = ($path->get_indices)[-1];
501             _splice_maybe ($menu->{'dirty'}, $i,1); # delete
502             if (my $item = _splice_maybe ($menu->{'children'}, $i,1)) {
503             $menu->_remove_item ($item);
504             }
505              
506             # update following row for its separator, if there's a following row
507             if ($model->get_iter($path)) {
508             _dirty_add ($self, $menu, $i,
509             Gtk2::Ex::MenuView::Menu::_DIRTY_SEPARATOR());
510             }
511             }
512              
513             # 'row-inserted' callback from model
514             sub _do_row_inserted {
515             my ($model, $path, $iter, $ref_weak_self) = @_;
516             ### MenuView row inserted: $path->to_string
517             my $self = $$ref_weak_self || return;
518              
519             my $menu = $self->_current_menu_containing_path($path) || do {
520             ### no menu for this yet
521             return;
522             };
523             my $i = ($path->get_indices)[-1];
524              
525             # shift up arrays if necessary
526             _splice_maybe ($menu->{'children'}, $i,0, undef); # insert
527             _splice_maybe ($menu->{'dirty'}, $i,0, 1); # insert
528              
529             _dirty_item_and_following_separator ($self, $menu, $path);
530             }
531              
532             # 'rows-reordered' callback from model
533             sub _do_rows_reordered {
534             my ($model, $path, $iter, $aref, $ref_weak_self) = @_;
535             my $self = $$ref_weak_self || return;
536             ### MenuView rows reordered
537              
538             my $menu = $self->_current_menu_at_path($path) || do {
539             ### no menu for this yet
540             return;
541             };
542             my $children = $menu->{'children'} || return;
543             my @new_children;
544             my $pos = _item_index_to_menu_pos ($menu, 0);
545             foreach my $newpos (0 .. $#$aref) {
546             my $oldpos = $aref->[$newpos];
547             if (my $item = $children->[$oldpos]) {
548             $new_children[$newpos] = $item;
549              
550             # ENHANCE-ME: defer this to _freshen(), with a 'dirty_reorder' or
551             # if 'all_dirty' reinserting coped with reorder
552             if (my $separator = $item->{'Gtk2::Ex::MenuView.separator'}) {
553             $menu->reorder_child ($separator, $pos++);
554             }
555             ### reorder_child item to: $pos
556             $menu->reorder_child ($item, $pos++);
557             }
558             }
559             @$children = @new_children;
560              
561             # ENHANCE-ME: could reorder the dirty flags and just update separators
562             # with different preceding
563             _dirty_menu ($menu);
564             }
565              
566             #------------------------------------------------------------------------------
567              
568             sub item_at_path {
569             my ($self, $path) = @_;
570             return $self->item_at_indices ($path->get_indices);
571             }
572              
573             sub item_at_indices {
574             my $self = shift;
575             ### item_at_indices(): @_
576             $self->{'model'} || return undef;
577              
578             my $menu = $self;
579             my $menu_path = Gtk2::TreePath->new; # of $menu
580             my $item;
581             while (@_) {
582             my $i = shift;
583             $self->_freshen_item ($menu, $menu_path, $i);
584             (($item = $menu->{'children'}->[$i])
585             && ($menu = $item->get_submenu))
586             or last;
587             $menu_path->append_index($i);
588             }
589             return $item;
590             }
591              
592             #------------------------------------------------------------------------------
593              
594             # _splice_maybe($aref,$offset,$len, $repl...)
595             # A splice() of @$aref, but only if $aref is not undef and $offset is not
596             # past its end.
597             sub _splice_maybe {
598             if (my $aref = shift) {
599             if ((my $pos = shift) <= $#$aref) {
600             splice @$aref, $pos, @_; # $len and values
601             }
602             }
603             }
604              
605             #------------------------------------------------------------------------------
606              
607             # sub insert {
608             # my ($self, $child, $pos) = @_;
609             # if ($pos >= 0 && $pos < $self->{'model_pos'}) {
610             # $self->{'model_pos'}--;
611             # }
612             # $self->SUPER::insert ($child, $pos);
613             # }
614             # sub prepend {
615             # my ($self, $child) = @_;
616             # $self->{'model_pos'}--;
617             # $self->SUPER::prepend ($child);
618             # }
619              
620             1;
621             __END__