File Coverage

blib/lib/App/Chart/Gtk2/Ex/ListModelPos.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 21 23 91.3


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2011 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart 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             # Chart 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 Chart. If not, see <http://www.gnu.org/licenses/>.
17              
18             package App::Chart::Gtk2::Ex::ListModelPos;
19 1     1   860 use 5.008;
  1         6  
20 1     1   5 use strict;
  1         1  
  1         20  
21 1     1   3 use warnings;
  1         2  
  1         27  
22 1     1   4 use Carp;
  1         2  
  1         61  
23 1     1   6 use List::Util qw(min max);
  1         3  
  1         95  
24 1     1   203 use Gtk2;
  0            
  0            
25             use POSIX ();
26              
27             use App::Chart::Glib::Ex::MoreUtils;
28             use Glib::Ex::SignalIds;
29              
30             use constant DEBUG => 0;
31              
32             BEGIN {
33             Glib::Type->register_enum ('App::Chart::Gtk2::Ex::ListModelPos::Type',
34             at => 0,
35             before => 1,
36             after => 2,
37             start => 3,
38             end => 4);
39             }
40             use Glib::Object::Subclass
41             'Glib::Object',
42             properties => [ Glib::ParamSpec->object
43             ('model',
44             'model',
45             'TreeModel to operate on.',
46             'Gtk2::TreeModel',
47             Glib::G_PARAM_READWRITE),
48              
49             Glib::ParamSpec->int
50             ('index',
51             'index',
52             'Current position as an integer.',
53             0, POSIX::INT_MAX(),
54             0,
55             Glib::G_PARAM_READWRITE),
56              
57             Glib::ParamSpec->enum
58             ('type',
59             'type',
60             'Position type.',
61             'App::Chart::Gtk2::Ex::ListModelPos::Type',
62             'start',
63             Glib::G_PARAM_READWRITE),
64              
65             Glib::ParamSpec->int
66             ('key-column',
67             'key-column',
68             'Column number in the model which is a unique key to identify a row.',
69             -1, POSIX::INT_MAX(),
70             -1,
71             Glib::G_PARAM_READWRITE),
72              
73             Glib::ParamSpec->scalar
74             ('key-func',
75             'key-func',
76             'Function returning a key (a string) uniquely identifying a row.',
77             Glib::G_PARAM_READWRITE),
78              
79             Glib::ParamSpec->scalar
80             ('key-equal',
81             'key-equal',
82             'Function testing equality of key strings.',
83             Glib::G_PARAM_READWRITE),
84              
85             ];
86              
87             sub INIT_INSTANCE {
88             my ($self) = @_;
89             $self->{'type'} = 'start'; # defaults
90             $self->{'index'} = 0;
91             $self->{'key_column'} = -1;
92             }
93              
94             sub SET_PROPERTY {
95             my ($self, $pspec, $newval) = @_;
96             my $pname = $pspec->get_name;
97             $self->{$pname} = $newval;
98              
99             if ($pname eq 'model') {
100             my $model = $newval;
101             my $ref_weak_self = App::Chart::Glib::Ex::MoreUtils::ref_weak($self);
102              
103             $self->{'ids'} = $model && Glib::Ex::SignalIds->new
104             ($model,
105             $model->signal_connect (row_changed => \&_do_row_changed,
106             $ref_weak_self),
107             $model->signal_connect (row_deleted => \&_do_row_deleted,
108             $ref_weak_self),
109             $model->signal_connect (row_inserted => \&_do_row_inserted,
110             $ref_weak_self),
111             $model->signal_connect (rows_reordered => \&_do_rows_reordered,
112             $ref_weak_self));
113             }
114             if ($pname eq 'type' || $pname eq 'index'
115             || $pname eq 'key_column' || $pname eq 'key_func') {
116             if ($self->{'type'} eq 'at') {
117             _at_key ($self, $self->{'index'}); # record key
118             }
119             }
120             }
121              
122             # the types which have 'index' referring to a particular row
123             my %_on_row_offsets = (before => 0,
124             at => 1,
125             after => 1);
126              
127             sub _do_row_changed {
128             my ($model, $chg_path, $chg_iter, $ref_weak_self) = @_;
129             my $self = $$ref_weak_self || return;
130             if (DEBUG) { print "ListModelPos changed '", $chg_path->to_string, "'\n"; }
131              
132             $chg_path->get_depth == 1 or return;
133             my ($chg_index) = $chg_path->get_indices;
134              
135             # when current row changes remember its possibly changed key
136             if ($chg_index == $self->{'index'} && $self->{'type'} eq 'at') {
137             my $key = _get_key ($self, $chg_index, $chg_iter);
138             if (defined $key) { $self->{'at_key'} = $key; }
139             return;
140             }
141              
142             # when seeking 'want_key' see if newly changed row matches
143             if (exists $self->{'want_key'}) {
144             my $this_key = _get_key ($self, $chg_index, $chg_iter);
145             if (_match_key ($self, $self->{'want_key'}, $this_key)) {
146             _at_key ($self, $chg_index, $this_key);
147             }
148             }
149             }
150              
151             sub _do_row_inserted {
152             my ($model, $ins_path, $ins_iter, $ref_weak_self) = @_;
153             my $self = $$ref_weak_self || return;
154             if (DEBUG) { print "ListModelPos inserted ", $ins_path->to_string, "\n"; }
155              
156             $ins_path->get_depth == 1 or return;
157             my ($ins_index) = $ins_path->get_indices;
158              
159             # when seeking 'want_key' see if new row matches
160             if (exists $self->{'want_key'}) {
161             my $this_key = _get_key ($self, $ins_index, $ins_iter);
162             if (_match_key ($self, $self->{'want_key'}, $this_key)) {
163             _at_key ($self, $ins_index, $this_key);
164             return;
165             }
166             }
167              
168             # when "at", "before" or "after", adjust index if we're after the insertion
169             my $offset = $_on_row_offsets{$self->{'type'}};
170             if (defined $offset) {
171             if ($ins_index < $self->{'index'} + $offset) {
172             $self->{'index'} ++;
173             }
174             }
175             }
176              
177             sub _do_row_deleted {
178             my ($model, $del_path, $ref_weak_self) = @_;
179             my $self = $$ref_weak_self || return;
180             if (DEBUG) { print "ListModelPos deleted ", $del_path->to_string, "\n"; }
181              
182             $del_path->get_depth == 1 or return;
183             my ($del_index) = $del_path->get_indices;
184              
185             my $type = $self->{'type'};
186              
187             # when current row deleted, become "before" what was the following row
188             if ($type eq 'at' && $del_index == $self->{'index'}) {
189             $self->{'type'} = 'before';
190             $self->notify ('type');
191             if (exists $self->{'at_key'}) {
192             $self->{'want_key'} = delete $self->{'at_key'};
193             }
194             return;
195             }
196              
197             # when "at", "before" or "after", adjust index if we're after the deletion
198             my $offset = $_on_row_offsets{$type};
199             if (defined $offset) {
200             if ($del_index < $self->{'index'} + $offset) {
201             if (-- $self->{'index'} < 0) {
202             $self->{'type'} = 'before';
203             $self->{'index'} = 0;
204             $self->notify ('type');
205             $self->notify ('index');
206             }
207             }
208             }
209             }
210              
211             sub _do_rows_reordered {
212             my ($model, $reorder_path, $reorder_iter, $aref, $ref_weak_self) = @_;
213             my $self = $$ref_weak_self || return;
214             if (DEBUG) { print "ListModelPos reorder ", join(' ',@$aref), "\n"; }
215              
216             $reorder_path->get_depth == 0 or return;
217             exists $_on_row_offsets{$self->{'type'}} or return;
218              
219             # when "at", "before" or "after" move $self->{'index'} old value to new,
220             # searching through $old_index == $aref->[$new_index]
221             my $index = $self->{'index'};
222             foreach my $new_index (0 .. $#$aref) {
223             if ($aref->[$new_index] == $index) {
224             $self->{'index'} = $new_index;
225             $self->notify ('index');
226             last;
227             }
228             }
229             }
230              
231             # optional $iter is the $index row
232             sub _get_key {
233             my ($self, $index, $iter) = @_;
234              
235             my $key_func = $self->{'key_func'};
236             my $key_column = $self->{'key_column'};
237             defined $key_func or $key_column>=0 or return;
238              
239             my $model = $self->{'model'};
240             $iter ||= $model->iter_nth_child (undef, $index);
241             if (! $iter) { return; }
242              
243             if (defined $key_func) {
244             return $key_func->($model, $iter);
245             } else {
246             return $model->get_value ($iter, $key_column);
247             }
248             }
249             sub _match_key {
250             my ($self, $want, $got) = @_;
251             if (DEBUG) {
252             print " _match_key ",
253             (defined $want ? $want : 'undef'),
254             " ",(defined $got ? $got : 'undef'),"\n";
255             }
256             if (! defined $got) { return 0; }
257             if (my $key_equal = $self->{'key_equal'}) {
258             return $key_equal->($want, $got);
259             } else {
260             return $want eq $got;
261             }
262             }
263              
264             sub model {
265             my ($self) = @_;
266             return $self->{'model'};
267             }
268             sub index {
269             my ($self) = @_;
270             return $self->{'index'};
271             }
272             sub iter {
273             my ($self) = @_;
274             if ($self->{'type'} eq 'at') {
275             my $model = $self->{'model'};
276             return $model->iter_nth_child ($self->{'index'});
277             } else {
278             return undef;
279             }
280             }
281              
282             sub goto {
283             my ($self, $index, $type) = @_;
284             $type ||= 'at';
285             if (DEBUG) { print "ListModelPos goto $index, $type\n"; }
286             $self->{'index'} = $index;
287             $self->{'type'} = $type;
288             if ($type eq 'at') {
289             _at_key ($self, $index); # record key
290             }
291             $self->notify ('index');
292             $self->notify ('type');
293             }
294              
295             sub goto_start {
296             my ($self) = @_;
297             $self->{'type'} = 'start';
298             $self->notify ('type');
299             }
300             sub goto_end {
301             my ($self) = @_;
302             $self->{'type'} = 'end';
303             $self->notify ('type');
304             }
305              
306             sub next_index {
307             my ($self) = @_;
308             if (DEBUG) { print "ListModelPos next ",$self->{'model'},"\n"; }
309              
310             if ($self->{'type'} eq 'end') {
311             return undef;
312             }
313              
314             my $index = $self->{'index'};
315             if ($self->{'type'} eq 'start') {
316             $index = 0;
317             } elsif ($self->{'type'} ne 'before') {
318             $index++;
319             }
320              
321             my $model = $self->{'model'};
322             my $mlen = $model->iter_n_children(undef);
323             if ($index >= $mlen) {
324             return undef;
325             }
326              
327             _at_key ($self, $index);
328             return $self->{'index'};
329             }
330              
331             sub prev_index {
332             my ($self) = @_;
333             if (DEBUG) { print "ListModelPos prev $self->{'model'}\n"; }
334              
335             my $type = $self->{'type'};
336             if ($type eq 'start') {
337             return undef;
338             }
339              
340             my $model = $self->{'model'};
341             my $mlen = $model->iter_n_children(undef);
342             if ($mlen == 0) {
343             return undef;
344             }
345              
346             my $index = $self->{'index'};
347             if ($type eq 'end') {
348             $index = $mlen - 1;
349             } elsif ($type eq 'at' || $type eq 'before') {
350             $index--;
351             }
352             $index = min ($index, $mlen-1);
353              
354             if ($index < 0) {
355             return undef;
356             }
357              
358             _at_key ($self, $index);
359             return $self->{'index'};
360             }
361              
362             # optional $key is row key data for $index, if not given then _get_key() runs
363             sub _at_key {
364             my ($self, $index, $key) = @_;
365             if (DEBUG) { print " at $index\n"; }
366             $self->{'type'} = 'at';
367             $self->{'index'} = $index;
368             $self->notify ('type');
369             $self->notify ('index');
370              
371             delete $self->{'want_key'};
372             if (@_ < 3) {
373             $key = _get_key ($self, $index);
374             }
375             if (defined $key) {
376             $self->{'at_key'} = $key;
377             }
378             }
379              
380             sub next_iter {
381             my ($self) = @_;
382             my $index = $self->next_index;
383             if (! defined $index) { return undef; }
384             return $self->{'model'}->iter_nth_child (undef, $index);
385             }
386             sub prev_iter {
387             my ($self) = @_;
388             my $index = $self->prev_index;
389             if (! defined $index) { return undef; }
390             return $self->{'model'}->iter_nth_child (undef, $index);
391             }
392              
393             1;
394             __END__
395              
396             =for stopwords submodel submodels ListOfLists TreeDragSource TreeDragDest toplevel Gtk ie ListofLists Eg TreeModel iter iters Ryde ListOfListsModel TreeView TreeRowReference ListModelPos enum Enum coderef
397              
398             =head1 NAME
399              
400             App::Chart::Gtk2::Ex::ListModelPos -- position within a list type tree model
401              
402             =for test_synopsis my ($my_model)
403              
404             =head1 SYNOPSIS
405              
406             use App::Chart::Gtk2::Ex::ListModelPos;
407             my $listpos = App::Chart::Gtk2::Ex::ListModelPos->new (model => $my_model);
408              
409             my $index = $listpos->next_index;
410              
411             =head1 OBJECT HIERARCHY
412              
413             C<App::Chart::Gtk2::Ex::ListModelPos> is a subclass of C<Glib::Object>,
414              
415             Glib::Object
416             App::Chart::Gtk2::Ex::ListModelPos
417              
418             =head1 DESCRIPTION
419              
420             A C<App::Chart::Gtk2::Ex::ListModelPos> object keeps track of a position in
421             a list type TreeModel (meaning any C<Glib::Object> implementing the
422             C<Gtk2::TreeModel> interface). It's intended to track a user's position in
423             a list of files, documents, etc.
424              
425             The position can be "at" a given row, or "before" or "after" one. The
426             position adjusts with inserts, deletes and reordering to follow that row.
427             Special positions "start" and "end" are the ends of the list, not following
428             any row.
429              
430             A row data "key" scheme allows a row to be followed across a delete and
431             re-insert done by TreeView drag-and-drop, or by a user delete and undo, or
432             re-add.
433              
434             =head2 TreeRowReference
435              
436             L<C<Gtk2::TreeRowReference>|Gtk2::TreeRowReference> does a similar thing to
437             ListModelPos, but a TreeRowReference is oriented towards tracking just a
438             particular row. If its row is deleted then a TreeRowReference points
439             nowhere, whereas ListModelPos remembers a position in between remaining rows.
440              
441             =head1 FUNCTIONS
442              
443             =over 4
444              
445             =item C<< $listpos = App::Chart::Gtk2::Ex::ListModelPos->new (key => value, ...) >>
446              
447             Create and return a new ListModelPos object. Optional key/value pairs set
448             initial properties as per C<< Glib::Object->new() >>. Eg.
449              
450             my $listpos = App::Chart::Gtk2::Ex::ListModelPos->new (model => $my_model,
451             key_column => 2);
452              
453             =item C<< $index = $listpos->model() >>
454              
455             =item C<< $index = $listpos->type() >>
456              
457             =item C<< $index = $listpos->index() >>
458              
459             Return the C<model>, C<type> and C<index> properties per L</PROPERTIES>
460             below.
461              
462             =item C<< $index = $listpos->iter() >>
463              
464             Return a L<C<Gtk2::TreeIter>|Gtk2::TreeIter> which is the current row. If
465             C<$listpos> is not type "at" or its index is out of range then the return is
466             C<undef>.
467              
468             =item C<< $index = $listpos->next_index() >>
469              
470             =item C<< $index = $listpos->prev_index() >>
471              
472             =item C<< $iter = $listpos->next_iter() >>
473              
474             =item C<< $iter = $listpos->prev_iter() >>
475              
476             Move C<$listpos> to the next or previous row from its current position and
477             return an integer index or L<C<Gtk2::TreeIter>|Gtk2::TreeIter> for the new
478             position. If there's no more rows in the respective direction (including if
479             the model is empty) then the return is C<undef> instead.
480              
481             =item C<< $listpos->goto ($index) >>
482              
483             =item C<< $listpos->goto ($index, $type) >>
484              
485             Move C<$listpos> to the given C<$index> row. The C<$type> parameter
486             defaults to "at", or you can give "before" or "after" instead.
487              
488             $listpos->goto (4, 'before');
489              
490             C<goto> is the same as setting the respective property values (but changed
491             in one operation).
492              
493             =item C<< $listpos->goto_start() >>
494              
495             =item C<< $listpos->goto_end() >>
496              
497             Move C<$listpos> to the start or end of its model, so that C<next> returns
498             the first row or C<prev> the last row (respectively). These functions are
499             the same as setting the C<type> property to "start" or "end", respectively.
500              
501             =back
502              
503             =head1 PROPERTIES
504              
505             =over 4
506              
507             =item C<model> (C<Glib::Object> implementing C<Gtk2::TreeModel> interface)
508              
509             The model to operate on.
510              
511             =item C<type> (C<Gtk2::Ex::ListModelPos::Type> enum, default "start")
512              
513             Enum values "at", "before", "after", "start", "end".
514              
515             The default type is C<"start">, but you can Initialize to a particular row
516             explicitly,
517              
518             my $listpos = App::Chart::Gtk2::Ex::ListModelPos->new (model => $my_model,
519             type => 'at',
520             index => 3);
521              
522             =item C<index> (integer, default 0)
523              
524             Current row number in the model. When C<type> is "start" or "end" the index
525             value is unused.
526              
527             =item C<key_column> (integer, default -1)
528              
529             Column number of row key data. The default -1 means no key column.
530              
531             =item C<key_func> (coderef, default C<undef>)
532              
533             Function to extract a key from a row. When set it's called
534              
535             $str = &$key_func ($model, $iter)
536              
537             =item C<key_equal> (coderef, default C<undef>)
538              
539             Row key equality function. The default C<undef> means use C<eq>. When set
540             it's called as
541              
542             $bool = &$key_equal ($value1, $value2)
543              
544             with values from the C<key_func> or C<key_column>.
545              
546             =back
547              
548             =head1 OTHER NOTES
549              
550             When a ListModelPos is "at" a given row and that row is deleted there's a
551             choice between becoming "after" the previous row, or "before" the next row.
552             This can make a difference in a reorder if the two rows move to different
553             places. The current code always uses "after the previous", or if the first
554             row is deleted then "start".
555              
556             =head1 SEE ALSO
557              
558             L<Gtk2::TreeModel>, L<Gtk2::TreeRowReference>
559              
560             =cut