File Coverage

blib/lib/App/Chart/Gtk2/Ex/TreeRowPosition.pm
Criterion Covered Total %
statement 18 20 90.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 25 27 92.5


line stmt bran cond sub pod time code
1             # notify after all crunching
2              
3              
4              
5              
6             # Copyright 2008, 2009, 2010 Kevin Ryde
7              
8             # This file is part of Chart.
9             #
10             # Chart is free software; you can redistribute it and/or modify
11             # it under the terms of the GNU General Public License as published by the
12             # Free Software Foundation; either version 3, or (at your option) any later
13             # version.
14             #
15             # Chart is distributed in the hope that it will be useful, but
16             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
18             # for more details.
19             #
20             # You should have received a copy of the GNU General Public License along
21             # with Chart. If not, see <http://www.gnu.org/licenses/>.
22              
23             package App::Chart::Gtk2::Ex::TreeRowPosition;
24 1     1   1285 use 5.008;
  1         5  
25 1     1   8 use strict;
  1         3  
  1         34  
26 1     1   8 use warnings;
  1         3  
  1         40  
27 1     1   8 use Carp;
  1         62  
  1         95  
28 1     1   10 use List::Util qw(min max);
  1         3  
  1         130  
29 1     1   11 use Scalar::Util 1.18 'refaddr'; # 1.18 for pure-perl refaddr() fix
  1         27  
  1         63  
30 1     1   4521 use Gtk2;
  0            
  0            
31             use POSIX ();
32              
33             use Glib::Ex::SignalBits;
34             use Glib::Ex::SignalIds;
35              
36             # uncomment this to run the ### lines
37             #use Smart::Comments;
38              
39             BEGIN {
40             Glib::Type->register_enum ('App::Chart::Gtk2::Ex::TreeRowPosition::Type',
41             start => 0,
42             end => 1,
43             at => 2,
44             before => 3,
45             after => 4,
46             );
47             }
48             use Glib::Object::Subclass
49             'Glib::Object',
50             signals => { 'key-extract' => { param_types => [ 'Gtk2::TreeModel',
51             'Gtk2::TreePath',
52             'Gtk2::TreeIter' ],
53             return_type => 'Glib::String',
54             flags => ['action','run-last'],
55             accumulator => \&Glib::Ex::SignalBits::accumulator_first_defined,
56             },
57             'key-equal' => { param_types => [ 'Glib::String',
58             'Glib::String' ],
59             return_type => 'Glib::Boolean',
60             flags => ['action','run-last'],
61             class_closure => \&_default_key_compare,
62             accumulator => \&Glib::Ex::SignalBits::accumulator_first_defined,
63             },
64             },
65             properties => [ Glib::ParamSpec->object
66             ('model',
67             'model',
68             'TreeModel to operate on.',
69             'Gtk2::TreeModel',
70             Glib::G_PARAM_READWRITE),
71              
72             Glib::ParamSpec->boxed
73             ('path',
74             'path',
75             'Current position as a Gtk2::TreePath.',
76             'Gtk2::TreePath',
77             Glib::G_PARAM_READWRITE),
78              
79             Glib::ParamSpec->enum
80             ('type',
81             'type',
82             'Position type.',
83             'App::Chart::Gtk2::Ex::TreeRowPosition::Type',
84             'start',
85             Glib::G_PARAM_READWRITE),
86              
87             Glib::ParamSpec->int
88             ('key-column',
89             'key-column',
90             'Column number in the model which is a unique key to identify a row.',
91             -1, POSIX::INT_MAX(),
92             -1,
93             Glib::G_PARAM_READWRITE),
94             ];
95              
96             sub INIT_INSTANCE {
97             my ($self) = @_;
98             $self->{'type'} = 'start'; # defaults
99             $self->{'path'} = Gtk2::TreePath->new;
100             $self->{'key_column'} = -1;
101             ### INIT_INSTANCE: $self->{'path'}->to_string, $self->{'type'}
102             }
103              
104             sub FINALIZE_INSTANCE {
105             my ($self) = @_;
106             ### FINALIZE_INSTANCE: "$self"
107             if (my $model = $self->{'model'}) {
108             if (my $h = $model->{__PACKAGE__.'.instances'}) {
109             delete $h->{refaddr($self)};
110             if (! %$h) {
111             ### no more instances, disconnect model
112             delete $model->{__PACKAGE__.'.instances'};
113             delete $model->{__PACKAGE__.'.ids'};
114             }
115             }
116             }
117             }
118              
119             sub SET_PROPERTY {
120             my ($self, $pspec, $newval) = @_;
121             my $pname = $pspec->get_name;
122             ### SET_PROPERTY: $pname, $newval
123              
124             if ($pname eq 'path') {
125             ### pathstr: $newval->to_string
126             $newval = $newval->copy;
127             } elsif ($pname eq 'model') {
128             FINALIZE_INSTANCE($self);
129             if ($newval) {
130             Scalar::Util::weaken ($newval->{__PACKAGE__.'.instances'}->{refaddr($self)} = $self);
131             $newval->{__PACKAGE__.'.ids'} ||= Glib::Ex::SignalIds->new
132             ($newval,
133             $newval->signal_connect (row_changed => \&_do_row_changed),
134             $newval->signal_connect (row_deleted => \&_do_row_deleted),
135             $newval->signal_connect (row_inserted => \&_do_row_inserted),
136             $newval->signal_connect (rows_reordered => \&_do_rows_reordered));
137             }
138             }
139             $self->{$pname} = $newval;
140              
141             if ($pname eq 'type'
142             || $pname eq 'path'
143             || $pname eq 'key_column') {
144             if ($self->{'type'} eq 'at') {
145             _at_key ($self, $self->{'path'}); # record key
146             }
147             }
148             ### pathstr now: $self->{'path'}->to_string
149             }
150              
151             sub _default_key_compare {
152             my ($self, $key1, $key2) = @_;
153             ### _default_key_compare: $key1,$key2
154             return ($key1 eq $key2);
155             }
156              
157              
158             sub _do_row_changed {
159             my ($model, $chg_path, $chg_iter) = @_;
160             ### TreeRowPosition row_changed, pathstr: $chg_path->to_string
161              
162             foreach my $self (values %{$model->{__PACKAGE__.'.instances'}}) {
163             $self || next;
164              
165             if ($self->{'type'} eq 'at' && $chg_path->compare($self->{'path'}) == 0) {
166             # when current row changes remember its possibly changed key
167             $self->{'at_key'} = _get_key ($self, $chg_path, $chg_iter);
168              
169             } elsif (exists $self->{'want_key'}) {
170             # when seeking 'want_key' see if newly changed row matches
171             my $this_key = _get_key ($self, $chg_path, $chg_iter);
172             if (_match_key ($self, $self->{'want_key'}, $this_key)) {
173             _at_key ($self, $chg_path->copy, $this_key);
174             }
175             }
176             }
177             }
178              
179             my %_on_row_offsets = (start => 1,
180             end => 1,
181             before => 0,
182             at => 1,
183             after => 1);
184              
185             sub _do_row_inserted {
186             my ($model, $ins_path, $ins_iter) = @_;
187             ### TreeRowPosition row_inserted, pathstr: $ins_path->to_string
188              
189             INSTANCE: foreach my $self (values %{$model->{__PACKAGE__.'.instances'}}) {
190             $self || next;
191             ### instance: "$self",$self->{'type'},$self->{'path'}->to_string,$self->{'want_key'}
192              
193             # when seeking 'want_key' see if new row matches
194             if (exists $self->{'want_key'}) {
195             my $this_key = _get_key ($self, $ins_path, $ins_iter);
196             if (_match_key ($self, $self->{'want_key'}, $this_key)) {
197             _at_key ($self, $ins_path->copy, $this_key);
198             next;
199             }
200             }
201              
202             my $type = $self->{'type'};
203             if ($type eq 'start' || $type eq 'end') {
204             next;
205             }
206              
207             my $path = $self->{'path'};
208             my $ins_depth = $ins_path->get_depth;
209             if ($ins_depth > $path->get_depth) {
210             ### something happening below us, so don't care
211             next;
212             }
213              
214             my @indices = $path->get_indices;
215             my @ins_indices = $ins_path->get_indices;
216             my $offset = $_on_row_offsets{$self->{'type'}};
217              
218             my $i = 0;
219             for (;;) {
220             if ($indices[$i] + $offset <= $ins_indices[$i]) {
221             ### we're before the insert, so no change
222             next INSTANCE;
223             }
224             if ($i == $ins_depth-1
225             || $indices[$i] > $ins_indices[$i] + $offset) {
226             last;
227             }
228             $i++;
229             }
230              
231             ### insert at or before our row, increment
232             $indices[$i]++;
233             $self->{'path'} = Gtk2::TreePath->new_from_indices (@indices);
234             $self->notify ('path');
235             }
236             }
237              
238             sub _do_row_deleted {
239             my ($model, $del_path) = @_;
240             ### TreeRowPosition row_deleted, pathstr: $del_path->to_string
241              
242             INSTANCE: foreach my $self (values %{$model->{__PACKAGE__.'.instances'}}) {
243             $self || next;
244              
245             my $type = $self->{'type'};
246             if ($type eq 'start' || $type eq 'end') {
247             next;
248             }
249              
250             my $path = $self->{'path'};
251             my $del_depth = $del_path->get_depth;
252             if ($del_depth > $path->get_depth) {
253             # something happening below us, don't need to worry
254             next;
255             }
256              
257             my @indices = $path->get_indices;
258             my @del_indices = $del_path->get_indices;
259              
260             for (my $i = 0; $i < $del_depth; $i++) {
261             if ($indices[$i] > $del_indices[$i]) {
262             # delete in an ancestor level and before our coords, decrement
263             $indices[$i]--;
264             $self->{'path'} = Gtk2::TreePath->new_from_indices (@indices);
265             $self->notify ('path');
266             next INSTANCE;
267             }
268             if ($indices[$i] < $del_indices[$i]) {
269             # delete is somewhere after our coords, so no change
270             next INSTANCE;
271             }
272             }
273              
274             ### delete of our exact row: $type
275             my $offset = $_on_row_offsets{$type};
276             if ($type eq 'at') {
277             $self->{'type'} = 'after';
278             $self->{'want_key'} = delete $self->{'at_key'};
279             }
280             if (($indices[-1] -= $offset) < 0) {
281             $self->{'type'} = 'before';
282             } else {
283             $self->{'path'} = Gtk2::TreePath->new_from_indices (@indices);
284             }
285              
286             if ($type ne $self->{'type'}) {
287             $self->notify ('type');
288             }
289             $self->notify ('path');
290             }
291             }
292              
293             sub _do_rows_reordered {
294             my ($model, $reorder_path, $reorder_iter, $aref) = @_;
295             ### TreeRowPosition rows_reordered, pathstr: $reorder_path->to_string, join(',',@$aref)
296              
297             my $lookup;
298             INSTANCE: foreach my $self (values %{$model->{__PACKAGE__.'.instances'}}) {
299             $self || next;
300              
301             my $path = $self->{'path'};
302             if (! $reorder_path->is_ancestor($path)) {
303             return;
304             }
305              
306             # $aref is what was previously there, ie. $aref->[$new_index]==$old_index,
307             # invert to $lookup[$old_index]==$new_index
308             #
309             $lookup ||= do {
310             my @lookup;
311             @lookup[@$aref] = 0 .. $#$aref; # array slice
312             \@lookup
313             };
314              
315             my @ind = $path->get_indices;
316             my $depth = $reorder_path->get_depth;
317             my $old_index = $ind[$depth];
318             my $new_index = $lookup->[$old_index];
319             if ($new_index != $old_index) {
320             $ind[$depth] = $new_index;
321             $self->{'path'} = Gtk2::TreePath->new_from_indices (@ind);
322             $self->notify ('path');
323             }
324             }
325             }
326              
327             # optional $iter is the $path row
328             sub _get_key {
329             my ($self, $path, $iter) = @_;
330             ### _get_key(), pathstr: $path->to_string, $iter
331              
332             if ($path->get_depth == 0) {
333             return undef;
334             }
335             my $model = $self->{'model'};
336             $iter ||= $model->get_iter ($path);
337             if (! $iter) {
338             ### no such row in model
339             return undef;
340             }
341              
342             if (defined (my $key = $self->signal_emit ('key-extract', $model, $path, $iter))) {
343             return $key;
344             }
345             if ((my $key_column = $self->{'key_column'}) >= 0) {
346             ### look at key_column: $key_column
347             return $model->get_value ($iter, $key_column);
348             }
349             return undef;
350             }
351             sub _match_key {
352             my ($self, $want, $got) = @_;
353             ### _match_key(): $got, $want
354             return (defined $got
355             && $self->signal_emit ('key-equal', $got, $want));
356             }
357              
358             sub model {
359             my ($self) = @_;
360             return $self->{'model'};
361             }
362             sub path {
363             my ($self) = @_;
364             return $self->{'path'};
365             }
366             sub iter {
367             my ($self) = @_;
368             if ($self->{'type'} eq 'at') {
369             my $model = $self->{'model'};
370             return $model->get_iter ($self->{'path'});
371             } else {
372             return undef;
373             }
374             }
375              
376             sub goto {
377             my ($self, $path, $type) = @_;
378             $type ||= 'at';
379             ### TreeRowPosition goto: $path->to_string, $type
380             $self->{'path'}
381             = (Scalar::Util::blessed($path) && $path->isa('Gtk2::TreePath')
382             ? $path->copy
383             : Gtk2::TreePath->new($path));
384             my $old_type = $self->{'type'};
385             $self->{'type'} = $type;
386             if ($type eq 'at') {
387             _at_key ($self, $self->{'path'}); # record key
388             }
389             if ($self->{'type'} ne $old_type) {
390             $self->notify ('type');
391             }
392             $self->notify ('path');
393             }
394              
395             sub goto_top_start {
396             my ($self) = @_;
397             $self->{'type'} = 'start';
398             $self->{'path'} = Gtk2::TreePath->new;
399             $self->notify ('type');
400             $self->notify ('path');
401             }
402             sub goto_top_end {
403             my ($self) = @_;
404             $self->{'type'} = 'end';
405             $self->{'path'} = Gtk2::TreePath->new;
406             $self->notify ('type');
407             $self->notify ('path');
408             }
409              
410             sub next {
411             my ($self, %options) = @_;
412             ### TreeRowPosition next, from pathstr: $self->{'path'}->to_string, $self->{'type'}, "$self->{'model'}"
413              
414             my $path = $self->{'path'}->copy;
415             my $type = $self->{'type'};
416             if ($type eq 'end') {
417             return undef;
418             }
419             if ($type eq 'start') {
420             $path->down;
421             } elsif ($self->{'type'} ne 'before') {
422             $path->next;
423             }
424             $type = 'at';
425             my $model = $self->{'model'};
426              
427             ### consider: $path->to_string, $type
428             if (! $model->get_iter($path)) {
429             for (;;) {
430             $path->up;
431             if ($path->get_depth == 0) {
432             # got to end of toplevel, nothing more to look at
433             $type = 'end';
434             goto DONE;
435             }
436             if ($model->get_iter($path)) {
437             last;
438             }
439             }
440             }
441              
442             for (;;) {
443             if (my $iter = $model->get_iter($path)) {
444             unless ($options{'want_leaf'} && $model->has_child($iter)) {
445             last;
446             }
447             $path->next;
448             } else {
449             $path->up;
450             $path->next;
451             $path->down;
452             }
453             }
454              
455             DONE:
456             if (! $options{'find_only'}) {
457             $self->{'path'} = $path;
458             _at_key ($self, $path);
459             if ($self->{'type'} ne $type) {
460             $self->{'type'} = $type;
461             $self->notify ('type');
462             }
463             $self->notify ('path');
464             }
465             return ($type eq 'at' ? $path : undef);
466             }
467              
468             sub prev_path {
469             my ($self) = @_;
470             ### TreeRowPosition prev: "$self->{'model'}"
471              
472             my $type = $self->{'type'};
473             if ($type eq 'start') {
474             return undef;
475             }
476              
477             my $model = $self->{'model'};
478             my $mlen = $model->iter_n_children(undef);
479             if ($mlen == 0) {
480             return undef;
481             }
482              
483             my $path = $self->{'path'};
484             if ($type eq 'end') {
485             $path = $mlen - 1;
486             } elsif ($type eq 'at' || $type eq 'before') {
487             $path--;
488             }
489             $path = min ($path, $mlen-1);
490              
491             if ($path < 0) {
492             return undef;
493             }
494              
495             _at_key ($self, $path);
496             return $self->{'path'};
497             }
498              
499             # optional $key is row key data for $path, if not given then _get_key() runs
500             sub _at_key {
501             my ($self, $path, $key) = @_;
502             ### _at_key(), pathstr: $path->to_string
503              
504             $self->{'type'} = 'at';
505             $self->{'path'} = $path;
506             $self->notify ('type');
507             $self->notify ('path');
508              
509             delete $self->{'want_key'};
510             if (@_ < 3) {
511             $key = _get_key ($self, $path);
512             }
513             ### key value: $key
514             if (defined $key) {
515             $self->{'at_key'} = $key;
516             }
517             }
518              
519             sub next_iter {
520             my $self = shift;
521             my $path = $self->next (@_) || return undef;
522             return $self->{'model'}->get_iter ($path);
523             }
524             sub prev_iter {
525             my $self = shift;
526             my $path = $self->prev (@_) || return undef;
527             return $self->{'model'}->get_iter ($path);
528             }
529              
530             1;
531             __END__
532              
533             =for stopwords TreeModel TreeView TreeRowReference TreeRowPosition Eg enum Enum coderef undef iter treerowpos
534              
535             =head1 NAME
536              
537             App::Chart::Gtk2::Ex::TreeRowPosition -- position within a list type tree model
538              
539             =for test_synopsis my ($my_model)
540              
541             =head1 SYNOPSIS
542              
543             use App::Chart::Gtk2::Ex::TreeRowPosition;
544             my $rowpos = App::Chart::Gtk2::Ex::TreeRowPosition->new (model => $my_model);
545              
546             my $path = $rowpos->next_path;
547              
548             =head1 OBJECT HIERARCHY
549              
550             C<App::Chart::Gtk2::Ex::TreeRowPosition> is a subclass of C<Glib::Object>,
551              
552             Glib::Object
553             App::Chart::Gtk2::Ex::TreeRowPosition
554              
555             =head1 DESCRIPTION
556              
557             A C<App::Chart::Gtk2::Ex::TreeRowPosition> object keeps track of a position in a list type
558             TreeModel (meaning any C<Glib::Object> implementing the C<Gtk2::TreeModel>
559             interface). It's intended to track a user's position in a list of files,
560             documents, etc.
561              
562             The position can be "at" a given row, or "before" or "after" one. The
563             position adjusts with inserts, deletes and reordering to follow that row.
564             Special positions "start" and "end" are the ends of the list, not following
565             any row.
566              
567             A row data "key" scheme allows a row to be followed across a delete and
568             re-insert done by TreeView drag-and-drop, or by a user delete and undo, or
569             re-add.
570              
571             =head2 TreeRowReference
572              
573             L<C<Gtk2::TreeRowReference>|Gtk2::TreeRowReference> does a similar thing to
574             TreeRowPosition, but a TreeRowReference is oriented towards tracking just a
575             particular row. If its row is deleted the TreeRowReference points nowhere.
576             TreeRowPosition instead then keeps a position in between remaining rows.
577              
578             =head1 FUNCTIONS
579              
580             =over 4
581              
582             =item C<< $rowpos = App::Chart::Gtk2::Ex::TreeRowPosition->new (key => value, ...) >>
583              
584             Create and return a new TreeRowPosition object. Optional key/value pairs set
585             initial properties as per C<< Glib::Object->new >>. Eg.
586              
587             my $rowpos = App::Chart::Gtk2::Ex::TreeRowPosition->new (model => $my_model,
588             key_column => 2);
589              
590             =item C<< $model = $rowpos->model >>
591              
592             =item C<< $type = $rowpos->type >>
593              
594             =item C<< $path = $rowpos->path >>
595              
596             Return the C<model>, C<type> and C<index> properties described below.
597              
598             =item C<< $iter = $rowpos->iter >>
599              
600             Return a L<C<Gtk2::TreeIter>|Gtk2::TreeIter> which is the current row. If
601             C<$rowpos> is not type "at" or the index is out of range then the return is
602             C<undef>.
603              
604             =item C<< $path = $rowpos->next_path >>
605              
606             =item C<< $path = $rowpos->prev_path >>
607              
608             =item C<< $iter = $rowpos->next_iter >>
609              
610             =item C<< $iter = $rowpos->prev_iter >>
611              
612             Move C<$rowpos> to the next or previous row from its current position and
613             return an integer index or L<C<Gtk2::TreeIter>|Gtk2::TreeIter> for the new
614             position. If there's no more rows in the respective direction (including if
615             the model is empty) then the return is C<undef> instead.
616              
617             =item C<< $rowpos->goto ($path) >>
618              
619             =item C<< $rowpos->goto ($path, $type) >>
620              
621             Move C<$rowpos> to the given C<$path> row. The C<$type> parameter
622             defaults to "at", or you can give "before" or "after" instead.
623              
624             $rowpos->goto (4, 'before');
625              
626             C<goto> is the same as setting the respective property values (but changed
627             in one operation).
628              
629             =item C<< $rowpos->goto_start >>
630              
631             =item C<< $rowpos->goto_end >>
632              
633             Move C<$rowpos> to the start or end of its model, so that C<next> returns
634             the first row or C<prev> the last row (respectively). These functions are
635             the same as setting the C<type> property to "start" or "end", respectively.
636              
637             =back
638              
639             =head1 PROPERTIES
640              
641             =over 4
642              
643             =item C<model> (C<Glib::Object> implementing C<Gtk2::TreeModel>)
644              
645             The model to operate on.
646              
647             =item C<type> (C<App::Chart::Gtk2::Ex::TreeRowPosition::Type> enum, default C<start>)
648              
649             Enum values "at", "before", "after", "start", "end".
650              
651             The default type is C<"start">, but you can Initialize to a particular row
652             explicitly,
653              
654             my $rowpos = App::Chart::Gtk2::Ex::TreeRowPosition->new (model => $my_model,
655             type => 'at',
656             index => 3);
657              
658             =item C<path> (C<Gtk2::TreePath>, default an empty path)
659              
660             Current path in the model.
661              
662             =item C<key-column> (integer, default -1)
663              
664             Column number of row key data. The default -1 means no key column.
665              
666             =back
667              
668             C<notify> signals are emitted for C<path> and/or C<type> when model row
669             changes alter those values, in the usual way. A notify handler must not
670             insert, delete or reorder model rows because doing so may invalidate the
671             path and/or iter objects passed to further handlers on the model.
672              
673             =head1 SIGNALS
674              
675             =over 4
676              
677             =item C<key-extract>, called (treerowpos, model, path, iter)
678              
679             Callback to extract a key from a row. When set it's called
680              
681             =item C<key-equal>, called (treerowpos, string, string)
682              
683             Row key equality function. The default handler compares with C<eq>.
684              
685             =back
686              
687             =head1 OTHER NOTES
688              
689             When a TreeRowPosition is "at" a given row and that row is deleted there's a
690             choice between becoming "after" the previous row, or "before" the next row.
691             This can make a difference in a reorder if the two rows move to different
692             places. The current code always uses "after the previous", or if the first
693             row is deleted then "start".
694              
695             =head1 SEE ALSO
696              
697             L<Gtk2::TreeModel>, L<Gtk2::TreeRowReference>
698              
699             =cut