File Coverage

blib/lib/Gtk2/Ex/TickerView.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-TickerView.
4             #
5             # Gtk2-Ex-TickerView 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-TickerView 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-TickerView. If not, see .
17              
18             package Gtk2::Ex::TickerView;
19 3     3   3089 use 5.008;
  3         11  
  3         113  
20 3     3   16 use strict;
  3         5  
  3         78  
21 3     3   13 use warnings;
  3         14  
  3         76  
22 3     3   15 use Carp;
  3         5  
  3         278  
23 3     3   18 use List::Util qw(min max);
  3         4  
  3         422  
24 3     3   2534 use POSIX ();
  3         21885  
  3         78  
25 3     3   239085 use Time::HiRes;
  3         6081  
  3         17  
26              
27 3     3   7319 use Glib;
  0            
  0            
28             # version 1.180 for Gtk2::CellLayout as an interface, also 1.180 for
29             # Gtk2::Buildable overriding superclass interface
30             use Gtk2 1.180;
31              
32             use Gtk2::Ex::SyncCall 12; # version 12 for gtk XID workaround
33             use Gtk2::Ex::CellLayout::Base 4; # version 4 for _cellinfo_starts()
34             our @ISA;
35             push @ISA, 'Gtk2::Ex::CellLayout::Base';
36              
37             our $VERSION = 15;
38              
39             # set this to 1 for some diagnostic prints, or 2 for even more prints
40             use constant DEBUG => 0;
41              
42             use constant { DEFAULT_FRAME_RATE => 4, # times per second
43             DEFAULT_SPEED => 30, # pixels per second
44             };
45              
46             # not wrapped until Gtk2-Perl 1.200
47             use constant GDK_PRIORITY_REDRAW => (Glib::G_PRIORITY_HIGH_IDLE + 20);
48              
49             use Glib::Object::Subclass
50             'Gtk2::DrawingArea',
51             interfaces =>
52             [ 'Gtk2::CellLayout',
53             # Gtk2::Buildable new in Gtk 2.12, omit if not available
54             Gtk2::Widget->isa('Gtk2::Buildable') ? ('Gtk2::Buildable') : ()
55             ],
56              
57             signals => { expose_event => \&_do_expose_event,
58             size_request => \&_do_size_request,
59             size_allocate => \&_do_size_allocate,
60             button_press_event => \&_do_button_press_event,
61             motion_notify_event => \&_do_motion_notify_event,
62             button_release_event => \&_do_button_release_event,
63             scroll_event => \&_do_scroll_event,
64             visibility_notify_event => \&_do_visibility_notify_event,
65             direction_changed => \&_do_direction_changed,
66             map => \&_do_map_or_unmap,
67             unmap => \&_do_map_or_unmap,
68             unrealize => \&_do_unrealize,
69             notify => \&_do_notify,
70             state_changed => \&_do_state_or_style_changed,
71             style_set => \&_do_state_or_style_changed,
72             },
73             properties => [ Glib::ParamSpec->object
74             ('model',
75             'Model object',
76             'TreeModel giving the items to display.',
77             'Gtk2::TreeModel',
78             Glib::G_PARAM_READWRITE),
79              
80             Glib::ParamSpec->boolean
81             ('run',
82             'Run ticker',
83             'Whether to run the ticker, ie. scroll across.',
84             1, # default yes
85             Glib::G_PARAM_READWRITE),
86              
87             Glib::ParamSpec->double
88             ('speed',
89             'Speed',
90             'Speed to move the items across, in pixels per second.',
91             0, POSIX::DBL_MAX(),
92             DEFAULT_SPEED,
93             Glib::G_PARAM_READWRITE),
94              
95             Glib::ParamSpec->double
96             ('frame-rate',
97             'Frame rate',
98             'How many times per second to move for scrolling.',
99             0, POSIX::DBL_MAX(),
100             DEFAULT_FRAME_RATE,
101             Glib::G_PARAM_READWRITE),
102              
103             Glib::ParamSpec->boolean
104             ('fixed-height-mode',
105             'Fixed height mode',
106             'Assume all cells have the same desired height.',
107             0, # default no
108             Glib::G_PARAM_READWRITE),
109              
110             Glib::ParamSpec->enum
111             ('orientation',
112             'Orientation', # dgettext('gtk20-properties')
113             'Horizontal or vertical display and scrolling.',
114             'Gtk2::Orientation',
115             'horizontal',
116             Glib::G_PARAM_READWRITE),
117              
118             ];
119              
120             # The private per-object fields are:
121             #
122             # vertical 0 or 1
123             # 0 when horizontal, 1 when vertical. Presented through GET_PROPERTY
124             # and SET_PROPERTY as Gtk2::Orientation 'horizontal' and 'vertical', but
125             # internally the number allows computed indexes instead of conditionals.
126             #
127             # pixmap Gtk2::Gdk::Pixmap or undef
128             # Established in _pixmap(), undef until then.
129             #
130             # pixmap_size
131             # The width of pixmap when horizontal, or height when vertical. This is
132             # established by _pixmap_desired_size() and thus may be set before
133             # pixmap is actually created.
134             #
135             # row_widths hashref { $index => $width }
136             # $index is an integer row number. $width is the total width of all the
137             # renderers' drawing of that row.
138             #
139             # This is a hash instead of an array so as to keep widths for roughly
140             # just the rows displayed, which may be a good thing on a big model.
141             # Widths are discarded when _pixmap_shift drops rows off the left edge,
142             # and when _pixmap_empty purges the whole pixmap. Widths are stored by
143             # _pixmap_extend, so the displayed rows are present, ready for
144             # _normalize() to use contemplating an increment of want_index.
145             #
146             # Widths are also saved by normalize actions like a big scroll_pixels()
147             # or a get_path_at_pos() of an off-screen position. Those widths end up
148             # remaining until a full redraw or until shifting passes them by. Which
149             # is probably no bad thing if one off-screen position calculation is
150             # reasonably likely to be followed by another. Might be worth thinking
151             # more about that though.
152             #
153             # The widths of drawn rows are implicitly in drawn_array as the
154             # difference between successive $x values, so having them in
155             # 'row_widths' is a bit of duplication. If _normalize() looked at
156             # drawn_array the drawn rows could be omitted, but almost certainly the
157             # code size would outweigh the data space.
158             #
159             # drawn_array arrayref [ $index, $x, $index, $x, ... ]
160             # Each $index is an integer row number. Each $x is the position in
161             # 'pixmap' where row $index has been drawn. There's two things helped
162             # by saveing all drawn positions,
163             #
164             # 1. _pixmap_shift() is easier. It can decrement each $x by the shift
165             # amount and look for the last $x <= 0 as the drawn rows retained.
166             # When the last row has been chopped off at the right edge (which is
167             # almost always) the second last position is immediately available to
168             # become pixmap_end_x, and the last index is pixmap_end_index to
169             # extend from.
170             #
171             # If all positions weren't recorded then _pixmap_shift() would have
172             # to reconstruct them to find that $x <= 0 point and the second last
173             # row pos, by adding up row widths.
174             #
175             # 2. _pixmap_find_want() can contemplate two drawn copies of a given
176             # row. There might be one at the start which starts a bit off screen
177             # at the left, and another later in the pixmap. When want_x allows
178             # the first to be used it makes best use of the current pixmap
179             # contents. If not then the second is a fallback and if it's too far
180             # to the right then might still be usable if _pixmap_shift() moves it
181             # down.
182             #
183             # Point 2 might be covered by retaining two positions for each row: its
184             # leftmost and then second leftmost (if any). _pixmap_shift() would
185             # still be tricky though, if it tried to find a new second leftmost for
186             # those rows whose leftmost had gone off-screen.
187             #
188             # Rows of zero width are still entered in drawn_array. This ensures
189             # _do_row_changed, _do_row_deleted, etc, notice that those rows are
190             # on-screen. It's possible want_index is a zero width row if an
191             # explicit scroll_to_start has gone there (or a hypothetical
192             # scroll_to_iter or something like that). _normalize() normally doesn't
193             # leave want_index on a zero width when moving though.
194             #
195             # want_x, want_index integers
196             # want_index is the desired row number (counting from 0) to be shown at
197             # the start of the ticker window. want_x is an x position where the
198             # left edge of the want_index row should start. want_x is zero or
199             # negative. Negative means the want_index item is partly off the left
200             # edge.
201             #
202             # Scrolling will soon make want_x a larger negative than the width of
203             # the want_index row. Expose (using _normalize()) looks for that and
204             # moves up by incrementing want_index and adding the skipped row width
205             # to want_x. It can go across multiple rows that way if necessary.
206             #
207             # Scrolling backwards can make want_x go positive. _normalize_want()
208             # and _normalize() again adjust, this time by decrementing want_index to
209             # a preceding row and subtracting that width from want_x, working back
210             # to find what row should be at or just before the left edge.
211             #
212             # pixmap_end_x
213             # The x just after the endmost drawn part of the pixmap. pixmap_end_x
214             # is truncated to pixmap_size when full or when the model is empty or
215             # all zero width rows.
216             #
217             # Until pixmap_end_x is faked to pixmap_size it's equal to the last
218             # drawn_array entry plus that entry's row_width. But the fakery to
219             # pixmap_size means it's easier to maintain pixmap_end_x separately than
220             # to build from drawn_array each time.
221             #
222             # The pixmap starts with only as much content as needed for expose to
223             # show the expose region the want_index/want_x position. As
224             # want_x,want_index advance _pixmap_extend() draws more content at
225             # pixmap_end_x.
226             #
227             # The "undrawn" area from pixmap_end_x onwards is always cleared to the
228             # background colour (in _pixmap_redraw() and _pixmap_shift()), so
229             # _pixmap_extend() can just draw. Some of that area might never be used
230             # but the idea is to do a single big XDrawRectangle instead of several
231             # small ones.
232             #
233             # visibility_state Gtk2::Gdk::VisibilityState enum string or 'initial'
234             # This is maintained from the 'visiblity-notify-event' handler. If
235             # 'fully-obscured' then the scroll timer is stopped, to save a bit of
236             # work when nothing can be seen.
237             #
238             # drag_xy arrayref [ $root_x, $root_y ]
239             # During a drag this is the last position of the mouse, in root window
240             # coordinates. When not in drag 'drag_xy' is not in $self at all. The
241             # timer is stopped while drag_xy is set. Only one of the two x or y are
242             # used, according to 'vertical', but storing not much extra and it
243             # smoothly handles the slightly freaky case of a change of orientation
244             # in the middle of a drag.
245             #
246             # model_empty boolean
247             # True when we believe $self->{'model'} is empty. Initialized in
248             # SET_PROPERTY when the model is first set, then kept up to date in
249             # _do_row_inserted() and _do_row_deleted().
250             #
251             # The aim of this is to give _do_row_inserted() a way to be sure when
252             # the model transitions from empty to non-empty, which provokes a resize
253             # and a possible timer restart.
254             #
255             # Testing for model length == 1 in _do_row_inserted() would be very
256             # nearly enough, but not perfect. If an earlier connected row-inserted
257             # handler inserts yet another row in response to the first insertion
258             # then by the time _do_row_inserted() runs it sees length==2. This
259             # would be pretty unusual, and probably needs 'iters-persist' on the
260             # model for the first iter to remain valid past the extra model change,
261             # but it's not completely outrageous.
262             #
263             # In RtoL mode all the x positions are measured from the right edge of the
264             # window or pixmap instead. Only the expose and the cell renderer drawing
265             # must mirror those RtoL logical positions into LtoR screen coordinates.
266             #
267             # In vertical orientation the "x" values are in fact y positions and the
268             # "width"s are in fact heights. Stand by for a search and replace to
269             # something neutral like "p" and "size" or whatever :-).
270             #
271              
272             sub INIT_INSTANCE {
273             my ($self) = @_;
274              
275             # the offscreen 'pixmap' already works as a form of double buffering, no
276             # need for DBE
277             $self->set_double_buffered (0);
278              
279             $self->{'want_index'} = 0;
280             $self->{'want_x'} = 0;
281             $self->{'row_widths'} = {};
282             $self->{'drawn_array'} = [];
283             $self->{'pixmap_end_x'} = 0;
284             $self->{'visibility_state'} = 'initial';
285             $self->{'run'} = 1; # default yes
286             $self->{'frame_rate'} = DEFAULT_FRAME_RATE;
287             $self->{'speed'} = DEFAULT_SPEED;
288             $self->{'vertical'} = 0;
289            
290             $self->add_events (['visibility-notify-mask',
291             'button-press-mask',
292             'button-motion-mask',
293             'button-release-mask']);
294             }
295              
296             sub GET_PROPERTY {
297             my ($self, $pspec) = @_;
298             my $pname = $pspec->get_name;
299             if ($pname eq 'orientation') {
300             return ($self->{'vertical'} ? 'vertical' : 'horizontal');
301             } else {
302             return $self->{$pname};
303             }
304             }
305              
306             sub SET_PROPERTY {
307             my ($self, $pspec, $newval) = @_;
308             my $pname = $pspec->get_name;
309             my $oldval = $self->{$pname};
310             ### SET_PROPERTY: $pname, $newval
311              
312             if ($pname eq 'orientation') {
313             $oldval = $self->{'vertical'};
314             $self->{'vertical'} = $newval = ($newval eq 'horizontal' ? 0 : 1);
315              
316             } else {
317             $self->{$pname} = $newval;
318              
319             if ($pname eq 'model') {
320             if (($oldval||0) == ($newval||0)) {
321             # no change, avoid resize, redraw, etc
322             return;
323             }
324             my $model = $newval;
325             $self->{'model_empty'} = ! ($model && $model->get_iter_first);
326             $self->{'model_ids'} = $model && do {
327             Scalar::Util::weaken (my $weak_self = $self);
328             my $ref_weak_self = \$weak_self;
329              
330             require Glib::Ex::SignalIds;
331             Glib::Ex::SignalIds->new
332             ($model,
333             $model->signal_connect (row_changed => \&_do_row_changed,
334             $ref_weak_self),
335             $model->signal_connect (row_inserted => \&_do_row_inserted,
336             $ref_weak_self),
337             $model->signal_connect (row_deleted => \&_do_row_deleted,
338             $ref_weak_self),
339             $model->signal_connect (rows_reordered => \&_do_rows_reordered,
340             $ref_weak_self))
341             };
342             }
343             }
344              
345             # updates ...
346              
347             # 'fixed_height_mode' turned to false provokes a resize, so as to look at
348             # all the rows now, not just the first. But don't resize when turning
349             # fixed_height_mode to true since the size we have is based on all rows
350             # and if the first row is truely representative then its size is the same
351             # as already in use.
352             #
353             if ($pname eq 'model'
354             || ($pname eq 'orientation' && $oldval != $newval)) {
355             ### model or orientation change
356             %{$self->{'row_widths'}} = ();
357             $self->queue_resize;
358             _pixmap_queue_draw ($self); # zap pixmap contents
359             }
360              
361             if ($pname eq 'fixed_height_mode' && $oldval && ! $newval) {
362             $self->queue_resize;
363             }
364              
365             if ($pname eq 'model' || $pname eq 'run' || $pname eq 'frame_rate') {
366             if ($pname eq 'frame_rate') {
367             # Discard old timer ready for new rate. Might like to carry over the
368             # elapsed period in the old one, but it should be short enough not to
369             # matter, and Glib doesn't seem to have much to help such a
370             # calculation.
371             delete $self->{'timer'};
372             }
373             _update_timer ($self);
374             }
375             }
376              
377              
378             #------------------------------------------------------------------------------
379             # size desired and allocated
380              
381             # 'size-request' class closure
382             sub _do_size_request {
383             my ($self, $req) = @_;
384             ### TickerView _do_size_request()
385              
386             $req->width (0);
387             $req->height (0);
388              
389             my $model = $self->{'model'} || return; # no size if no model
390             my @cells = $self->GET_CELLS;
391             @cells || return; # no size if no cells
392              
393             my $sizefield = 3 - $self->{'vertical'}; # width vert, height horiz
394             my $want_size = 0;
395             for (my $iter = $model->get_iter_first;
396             $iter;
397             $iter = $model->iter_next ($iter)) {
398             $self->_set_cell_data ($iter);
399             foreach my $cell (@cells) {
400             $want_size = max ($want_size,
401             ($cell->get_size($self,undef))[$sizefield]);
402             }
403             if ($self->{'fixed_height_mode'}) {
404             ### one row only for fixed-height-mode
405             last;
406             }
407             }
408              
409             if ($sizefield == 3) {
410             $req->height ($want_size);
411             } else {
412             $req->width ($want_size);
413             }
414             ### decide size: $req->width."x".$req->height
415             }
416              
417             # 'size_allocate' class closure
418             #
419             # This is also reached for cell renderer and attribute changes through
420             # $self->queue_resize in CellLayout::Base.
421             #
422             # For a move without a resize the pixmap at its current size could be
423             # retained. Probably moves alone won't occur often enough to make that
424             # worth worrying about.
425             #
426             # Crib: no queue_draw() here, since the default redraw_on_alloc means that's
427             # done automatically (in gtk_widget_size_allocate()).
428             #
429             sub _do_size_allocate {
430             my ($self, $alloc) = @_;
431             ### TickerView _do_size_allocate()
432              
433             $self->signal_chain_from_overridden ($alloc);
434              
435             if (my $pixmap = $self->{'pixmap'}) {
436             my ($want_width, $want_height) = _pixmap_desired_size ($self, $alloc);
437             my ($got_width, $got_height) = $pixmap->get_size;
438             if ($want_width != $got_width || $want_height != $got_height) {
439             ### want new pixmap size
440             $self->{'pixmap'} = undef;
441             _pixmap_queue_draw ($self);
442             return;
443             }
444             }
445             }
446              
447              
448             #-----------------------------------------------------------------------------
449             # expose and pixmap
450              
451             # 'expose-event' class closure, getting Gtk2::Gdk::Event::Expose
452             sub _do_expose_event {
453             my ($self, $event) = @_;
454             if (DEBUG >= 2) {
455             my $expose_size = ($self->{'vertical'}
456             ? $event->area->y + $event->area->height
457             : $event->area->x + $event->area->width);
458             print "TickerView _do_expose_event count=",$event->count,
459             " pixmap_redraw=",($self->{'pixmap_redraw'}?"yes":"no"),
460             " expose_size=$expose_size\n";
461             }
462              
463             my $expose_size = do { my $expose_rect = $event->area;
464             ($self->{'vertical'}
465             ? $expose_rect->y + $expose_rect->height
466             : $expose_rect->x + $expose_rect->width) };
467             my $pixmap = _pixmap($self);
468              
469             my $x;
470             if ($self->{'pixmap_redraw'}
471             || ! defined ($x = _pixmap_find_want ($self, $expose_size))) {
472             # want_index/want_x isn't in the pixmap at all
473             _pixmap_empty ($self);
474             $x = 0;
475              
476             } elsif ($x + $expose_size > $self->{'pixmap_size'}) {
477             # want_index/want_x is in the pixmap, but there's not enough space after
478             # it for the $expose_size
479             if (DEBUG >= 2) { print "expose found $x but +$expose_size =",
480             $x + $expose_size,
481             " is past $self->{'pixmap_size'}, so shift\n"; }
482             _pixmap_shift ($self, $x);
483             $x = 0;
484             }
485             _pixmap_extend ($self, $x + $expose_size);
486              
487             if ($self->get_direction eq 'rtl') {
488             # width when horiz, height when vert
489             my $win_size = ($self->allocation->values)[2 + $self->{'vertical'}];
490             $x = $self->{'pixmap_size'} - 1 - $win_size - $x;
491             }
492             my $win = $self->window;
493             my $gc = $self->get_style->black_gc; # any gc for an XCopyArea
494             $gc->set_clip_region ($event->region);
495             $win->draw_drawable ($gc, $pixmap,
496             ($self->{'vertical'} ? (0,$x) : ($x,0)), # src
497             0,0, # dst
498             $win->get_size);
499             $gc->set_clip_region (undef);
500             return 0; # Gtk2::EVENT_PROPAGATE
501             }
502              
503             sub _pixmap_find_want {
504             my ($self, $expose_size) = @_;
505             if (DEBUG >= 2) { print " _pixmap_find_want",
506             " want_index=$self->{'want_index'}",
507             " want_x=$self->{'want_x'}\n"; }
508              
509             # the usual case here is _normalize() finding want_index still within its
510             # row width and the previously determined drawn_want_at in drawn_array is
511             # still wholely in a drawn portion of the pixmap
512              
513             my ($want_x, $want_index)
514             = _normalize ($self, $self->{'want_x'}, $self->{'want_index'});
515             if (! defined $want_x) {
516             # no model, or model empty, or all rows zero width
517             return ($self->{'want_x'} = 0);
518             }
519             $self->{'want_x'} = $want_x;
520             $self->{'want_index'} = $want_index;
521              
522             $want_x = POSIX::floor ($want_x);
523             my $drawn = $self->{'drawn_array'};
524             my ($i, $x);
525              
526             # see if the cached 'drawn_want_at' is still the wanted index and in range
527             if (defined ($i = $self->{'drawn_want_at'})
528             && defined $drawn->[$i]
529             && $drawn->[$i] == $want_index
530             && ($x = $drawn->[$i+1] - $want_x) >= 0
531             && $x + $expose_size <= $self->{'pixmap_size'}) {
532             if (DEBUG >= 2) { print " drawn_want_at still good\n"; }
533             return $x;
534             }
535              
536             if (DEBUG >= 2) { print " seeking leftmost $want_index\n"; }
537             for ($i = 0; $i < @$drawn; $i+=2) {
538             if ($drawn->[$i] == $want_index
539             && (($x = $drawn->[$i+1] - $want_x) >= 0)) {
540             $self->{'drawn_want_at'} = $i;
541             return $x;
542             }
543             }
544             if (DEBUG >= 2) { local $,=' '; print " not found in",@$drawn,"\n"; }
545             return undef;
546             }
547              
548             sub _pixmap_empty {
549             my ($self) = @_;
550             if (DEBUG) { print "_pixmap_empty to",
551             " want_index=$self->{'want_index'},",
552             "want_x=$self->{'want_x'}\n"; }
553              
554             $self->{'pixmap_redraw'} = 0;
555             my $pixmap = _pixmap($self);
556             my ($pixmap_width, $pixmap_height) = $pixmap->get_size;
557             my $gc = $self->get_style->bg_gc ($self->state);
558             $pixmap->draw_rectangle ($gc, 1, 0,0, $pixmap_width,$pixmap_height);
559              
560             @{$self->{'drawn_array'}} = ();
561             $self->{'drawn_want_at'} = undef;
562             %{$self->{'row_widths'}} = (); # prune
563              
564             $self->{'pixmap_end_x'} = POSIX::floor ($self->{'want_x'});
565             }
566              
567             sub _pixmap_shift {
568             my ($self, $offset) = @_;
569             if (DEBUG >= 2) {
570             print "_pixmap_shift offset=$offset, from",
571             " pixmap_end_x=",(defined $self->{'pixmap_end_x'} ? $self->{'pixmap_end_x'} : 'undef'),
572             "\n";
573             }
574             my $pixmap_size = $self->{'pixmap_size'};
575             my $drawn = $self->{'drawn_array'};
576              
577             # if the rightmost drawn goes past the end of the pixmap then discard it
578             if (@$drawn
579             && $drawn->[-1] + _row_width($self,$drawn->[-2]) > $pixmap_size) {
580             if (DEBUG >= 2) { print " last index=$drawn->[-2] past end, drop it\n"; }
581             $self->{'pixmap_end_x'} = pop @$drawn;
582             pop @$drawn; # index
583             if (! @$drawn) { goto \&_pixmap_empty; }
584             }
585              
586             my %prune_row_widths; # keys are the indexes to be discarded
587             my $last_nonpositive = 0;
588             for (my $i = 0; $i < @$drawn; $i+=2) {
589             if (($drawn->[$i+1] -= $offset) <= 0) {
590             $last_nonpositive = $i;
591             $prune_row_widths{$drawn->[$i]} = 1;
592             } else {
593             delete $prune_row_widths{$drawn->[$i]};
594             }
595             }
596             my $end_x = ($self->{'pixmap_end_x'} -= $offset);
597             splice @$drawn, 0, $last_nonpositive;
598              
599             # row_widths for rows shifted off at the left are dropped, unless they
600             # also occur later in the drawn contents
601             delete $prune_row_widths{$drawn->[0]};
602             delete @{$self->{'row_widths'}} {keys %prune_row_widths}; # hash slice
603             if (DEBUG >= 2) {
604             local $,=' '; print " prune row widths:", keys %prune_row_widths,"\n";
605             }
606             # CHECK-ME: probably not supposed to shift down to nothing ...
607             if (! @$drawn) { goto \&_pixmap_empty; }
608              
609             if (DEBUG >= 2) { local $,=' '; print " now drawn",@$drawn,"\n"; }
610              
611             my $pixmap = $self->{'pixmap'};
612             my ($pixmap_width, $pixmap_height) = $pixmap->get_size;
613             my $gc = $self->get_style->bg_gc ($self->state);
614              
615             # $end_x shouldn't be negative, but apply clamp $copy_size just in case.
616             # Won't have $offset==0, thus won't have $src_x==$dst_x, so no sort circuits.
617             #
618             my $copy_size = max ($end_x, 0);
619             my $clear_size = $pixmap_size - $copy_size;
620             my ($src_x, $dst_x, $clear_x);
621             if ($self->get_direction eq 'ltr') {
622             # $pixmap_size
623             # +------+----------------+----+
624             # | | $copy_size | |
625             # +------+----------------+----+
626             # $offset $end_x --> measuring rightwards
627             # / /
628             # +----------------+-----------+
629             # | $copy_size |$clear_size|
630             # +----------------+-----------+
631             #
632             $dst_x = 0;
633             $src_x = $offset;
634             $clear_x = $copy_size;
635             } else {
636             # $pixmap_size
637             # +----+----------------+------+
638             # | | $copy_size | |
639             # +----+----------------+------+
640             # $end_x $offset <-- measuring leftwards
641             # \ \
642             # +-----------+----------------+
643             # |$clear_size| $copy_size |
644             # +-----------+----------------+
645             #
646             $dst_x = $clear_size;
647             $src_x = $dst_x - $offset;
648             $clear_x = 0;
649             }
650             if (DEBUG >= 2) { print " copy $src_x to $dst_x size=$copy_size\n";
651             print " clear $clear_x size=$clear_size\n"; }
652              
653             if ($self->{'vertical'}) {
654             $pixmap->draw_drawable ($gc, $pixmap,
655             0,$src_x, # src
656             0,$dst_x, # dst
657             $pixmap_width, $copy_size); # width,height
658             # clear the remainder
659             $pixmap->draw_rectangle ($gc, 1,
660             0,$clear_x,
661             $pixmap_width, $clear_size);
662             } else {
663             # horizontal
664             $pixmap->draw_drawable ($gc, $pixmap,
665             $src_x,0, # src
666             $dst_x,0, # dst
667             $copy_size, $pixmap_height); # width,height
668             # clear the remainder
669             $pixmap->draw_rectangle ($gc, 1,
670             $clear_x,0,
671             $clear_size, $pixmap_height);
672             }
673             }
674              
675             # draw more at 'pixmap_end_x' to ensure it's not less than $target_x
676             sub _pixmap_extend {
677             my ($self, $target_x) = @_;
678             if (DEBUG >= 2) {
679             my $last_index = $self->{'drawn'}->[-2];
680             my $pixmap_size = $self->{'pixmap_size'};
681             print "_pixmap_extend target_x=$target_x",
682             " got last index=",(defined $last_index?$last_index:'undef'),
683             ",x=$self->{'pixmap_end_x'}",
684             ", pixmap_size=",(defined $pixmap_size ? $pixmap_size : '[undef]'),
685             "\n";
686             }
687             if (DEBUG) {
688             if ($target_x > $self->{'pixmap_size'}) {
689             die "oops, target_x=$target_x bigger than pixmap ",
690             $self->{'pixmap_size'};
691             }
692             }
693              
694             my $x = $self->{'pixmap_end_x'};
695             if ($x >= $target_x) { return; } # if target already covered
696              
697             my $pixmap = _pixmap($self);
698             my ($pixmap_width, $pixmap_height) = $pixmap->get_size;
699             my $pixmap_size = $self->{'pixmap_size'};
700              
701             my $model = $self->{'model'};
702             if (! $model) {
703             ### no model set
704             EMPTY:
705             $self->{'pixmap_end_x'} = $pixmap_size;
706             return;
707             }
708              
709             # "pack_start"s first then "pack_end"s
710             my @cellinfo_list = ($self->_cellinfo_starts,
711             reverse $self->_cellinfo_ends);
712             if (! @cellinfo_list) {
713             ### no cell renderers to draw with
714             goto EMPTY;
715             }
716              
717             my $all_zeros = _make_all_zeros_proc();
718             my $ltor = ($self->get_direction eq 'ltr');
719             my $row_widths = $self->{'row_widths'};
720             my $drawn = $self->{'drawn_array'};
721             my $vertical = $self->{'vertical'};
722              
723             my $index = $drawn->[-2];
724             if (defined $index) {
725             $index++;
726             } else {
727             $index = $self->{'want_index'};
728             }
729             my $iter = $model->iter_nth_child (undef, $index);
730              
731             for (;;) {
732             if (! $iter) {
733             # initial $index was past the end, or stepped iter_next() past the
734             # end, either way wrap around
735             $index = 0;
736             $iter = $model->get_iter_first;
737             if (! $iter) {
738             ### model has no rows
739             $x = $pixmap_size;
740             last;
741             }
742             }
743              
744             push @$drawn, $index, $x;
745             $self->_set_cell_data ($iter);
746              
747             my $row_size = 0;
748             foreach my $cellinfo (@cellinfo_list) {
749             my $cell = $cellinfo->{'cell'};
750             if (! $cell->get('visible')) { next; }
751              
752             my (undef, undef, $width, $height) = $cell->get_size ($self, undef);
753             my $rect;
754             if ($vertical) {
755             $rect = Gtk2::Gdk::Rectangle->new
756             (0, $ltor ? $x : $pixmap_height - 1 - $x - $height,
757             $pixmap_width, $height);
758             $x += $height;
759             $row_size += $height;
760             } else {
761             $rect = Gtk2::Gdk::Rectangle->new
762             ($ltor ? $x : $pixmap_width - 1 - $x - $width, 0,
763             $width, $pixmap_height);
764             $x += $width;
765             $row_size += $width;
766             }
767             $cell->render ($pixmap, $self, $rect, $rect, $rect, []);
768             }
769              
770             $row_widths->{$index} = $row_size;
771             if (DEBUG >= 2) { print " draw $index at x=",$x-$row_size,
772             " width $row_size\n"; }
773              
774             if ($all_zeros->($index, $row_size)) {
775             ### all cell widths on all rows are zero
776             $self->{'want_x'} = 0;
777             $x = $pixmap_size;
778             last;
779             }
780              
781             $index++;
782             if ($x >= $target_x) { last; } # stop when target covered
783             $iter = $model->iter_next ($iter);
784             }
785              
786             $self->{'pixmap_end_x'} = min ($x, $pixmap_size);
787             ### extended to pixmap_end_x: $self->{'pixmap_end_x'}
788             }
789              
790             # _pixmap() returns 'pixmap', creating it if it doesn't already exist.
791             # The height is the same as the window height.
792             # The width is 1.5 * the window width, or half the screen width, whichever
793             # is bigger.
794             #
795             # The pixmap is designed to avoid drawing the same row repeatedly as it
796             # scrolls across. The wider the pixmap the less often a full redraw will be
797             # needed. The width used is therefore a compromise between the memory taken
798             # by a wide pixmap, versus redraws caused by a narrow pixmap.
799             #
800             # Twice the window width gives a reasonable amount of hidden pixmap
801             # buffering off the window ends. However if the window is unusually narrow
802             # it could be much less than a typical row, so impose a minimum of half the
803             # screen width.
804             #
805             # (Maybe a maximum of say twice the screen width could be imposed too, so
806             # that a hugely wide window doesn't result in a massive pixmap. But for a
807             # pixmap smaller than the window we'd have to notice what portion of the
808             # window is on-screen. Probably that's much more trouble than it's worth.
809             # If you ask for a stupidly wide window then expect to have your pixmap
810             # memory used up. :-)
811             #
812             sub _pixmap {
813             my ($self) = @_;
814             return ($self->{'pixmap'} ||= do {
815             ### _pixmap() create
816             my ($pixmap_width, $pixmap_height)
817             = _pixmap_desired_size ($self, $self->allocation);
818             ### create size: "${pixmap_width}x${pixmap_height}"
819             Gtk2::Gdk::Pixmap->new ($self->window, $pixmap_width, $pixmap_height, -1);
820             });
821             }
822             use constant _PIXMAP_ALLOCATION_FACTOR => 1.5;
823             use constant _SCREEN_SIZE_FACTOR => 0.5;
824             sub _pixmap_desired_size {
825             my ($self, $alloc) = @_;
826             ### _pixmap_desired_size()
827             my @pixmap_dims = ($alloc->width, $alloc->height);
828             my $i = $self->{'vertical'}; # width for horiz, height for vert
829             my $screen = $self->get_screen; # gtk 2.4 for celllayout, so have 2.2 screen
830             my @screen_dims = ($screen->get_width, $screen->get_height);
831              
832             ### max: "alloc*"._PIXMAP_ALLOCATION_FACTOR." = ".($pixmap_dims[$i] * _PIXMAP_ALLOCATION_FACTOR)
833             ### max: "screen*"._SCREEN_SIZE_FACTOR." = ".($screen_dims[$i] * _SCREEN_SIZE_FACTOR)
834             $pixmap_dims[$i] = $self->{'pixmap_size'}
835             = int (max ($pixmap_dims[$i] * _PIXMAP_ALLOCATION_FACTOR,
836             $screen_dims[$i] * _SCREEN_SIZE_FACTOR));
837             ### desire: "$pixmap_dims[0]x$pixmap_dims[1]"
838             return @pixmap_dims;
839             }
840              
841             sub _pixmap_queue_draw {
842             my ($self) = @_;
843             ### _pixmap_queue_draw()
844             # zap 'drawn_array' to save work in _apply_remap
845             $self->{'pixmap_redraw'} = 1;
846             @{$self->{'drawn_array'}} = ();
847             $self->queue_draw;
848             }
849              
850             #-----------------------------------------------------------------------------
851             # row index widths and normalization
852              
853             # Normalize $x,$index so that $x<=0 and $x+$row_width >= 0.
854             # The return is two new values ($x,$index).
855             # If $x==0 then $x,$index are returned unchanged.
856             # If there's no model, or the model is empty, the return is empty ().
857             # If all rows are zero width the return is $x==undef and $index unchanged.
858             #
859             sub _normalize {
860             my ($self, $x, $index) = @_;
861              
862             my $model = $self->{'model'} || return;
863             my $all_zeros = _make_all_zeros_proc();
864             my $len = $model->iter_n_children(undef) || return; # if model empty
865              
866             if ($x < 0) {
867             # Here we're looking to see if the want_index row is entirely
868             # off-screen, ie. if want_x + row_width (of that row) would still be
869             # want_x <= 0.
870             #
871             # If _row_width() gives us a $iter, because it used it to get a row
872             # width, then we keep it going for further rows. If _row_width()
873             # operates out of its cache then there's no iter.
874             #
875             ### forward from: "$index,$x"
876             my $iter;
877              
878             for (;;) {
879             my $row_width = _row_width ($self, $index, $iter);
880             if ($x + $row_width > 0) {
881             last;
882             }
883             if ($all_zeros->($index, $row_width)) {
884             ### all cell widths on all rows are zero
885             return (undef, $_[2]); # with original $index
886             }
887             $x += $row_width;
888             $index++;
889             if ($index >= $len) {
890             $index = 0;
891             $iter = undef;
892             } else {
893             if ($iter) {
894             $iter = $model->iter_next ($iter);
895             }
896             }
897             }
898              
899             } else {
900             # Here we're trying to bring $x back to <= 0, usually because a backward
901             # scroll has pushed our want_x position to the right and we have to see
902             # what the preceding row is and want position to draw it.
903             #
904             # Because there's no "iter_prev" there's no use of iters here, it ends
905             # up a new iter_nth_child in _row_width() for every row not already
906             # cached. For a user scroll back just a short distance the previous row
907             # is probably already cached (and even probably in the pixmap).
908             #
909             ### backward from: "$x,$index"
910              
911             while ($x > 0) {
912             $index--;
913             if ($index < 0) {
914             $index = max (0, $len-1);
915             }
916             my $row_width = _row_width ($self, $index);
917             if ($all_zeros->($index, $row_width)) {
918             ### all cell widths on all rows are zero
919             return (undef, $_[2]); # with original $index
920             }
921             $x -= $row_width;
922             }
923             }
924             #### now at "$index,$x"
925             return ($x, $index);
926             }
927              
928             # Return the width in pixels of row $index, or height when vertical.
929             # $iter is an iterator for $index, or undef to make one here if necessary.
930             # If an iterator is made then it's stored back to $_[2], as call-by-reference.
931             #
932             sub _row_width {
933             my ($self, $index, $iter) = @_;
934              
935             my $row_widths = $self->{'row_widths'};
936             my $row_width = $row_widths->{$index};
937             if (defined $row_width) { return $row_width; }
938              
939             if (DEBUG) { print " _row_width on $index, iter=",
940             (defined $iter ? $iter : 'undef'), "\n"; }
941             if (! defined $iter) {
942             my $model = $self->{'model'};
943             $iter = $_[2] = $model && $model->iter_nth_child (undef, $index);
944             if (! defined $iter) {
945             if (DEBUG) { print " _row_width index $index out of range\n"; }
946             return 0;
947             }
948             }
949             $self->_set_cell_data ($iter);
950              
951             my $sizefield = 2 + $self->{'vertical'}; # width horiz, height vert
952             $row_width = 0;
953             foreach my $cellinfo (@{$self->{'cellinfo_list'}}) {
954             my $cell = $cellinfo->{'cell'};
955             if (! $cell->get('visible')) { next; }
956             $row_width += ($cell->get_size ($self,undef)) [$sizefield];
957             }
958             ### _row_width() calc: "$index is $row_width"
959             return ($row_widths->{$index} = $row_width);
960             }
961              
962             #------------------------------------------------------------------------------
963             # programmatic scrolling
964              
965             sub scroll_to_start {
966             my ($self) = @_;
967             _scroll_to_pos ($self, 0, 0);
968             }
969              
970             sub scroll_pixels {
971             my ($self, $pixels) = @_;
972             _scroll_to_pos ($self, $self->{'want_x'} - $pixels, $self->{'want_index'});
973             }
974              
975             sub _scroll_to_pos {
976             my ($self, $x, $index) = @_;
977             #### _scroll_to_pos(): "x=$x index=$index"
978              
979             $self->{'want_index'} = $index;
980             $self->{'want_x'} = $x;
981              
982             # If drawable(), ie. GTK_WIDGET_DRAWABLE(), is true, meaning widget
983             # show()ed and window mapped, then draw after a server sync. If unmapped
984             # then programmatic scrolls just move the position around and the eventual
985             # map will get an expose to draw.
986             #
987             if ($self->drawable) {
988             $self->{'sync_call'} ||= do {
989             my $weak_self = $self;
990             Scalar::Util::weaken ($weak_self);
991             Gtk2::Ex::SyncCall->sync ($self, \&_sync_call_handler, \$weak_self);
992             };
993             }
994             }
995             sub _sync_call_handler {
996             my ($ref_weak_self) = @_;
997             my $self = $$ref_weak_self || return;
998             #### TickerView _sync_call_handler()
999              
1000             $self->{'sync_call'} = undef;
1001              
1002             # Recheck drawable(), GTK_WIDGET_DRAWABLE(), in case unmapped in between
1003             # the last scroll and the server sync reply. If still mapped then do an
1004             # immediate draw via queue_draw and forced update.
1005             #
1006             # process_updates() normally runs under an idle at GDK_PRIORITY_REDRAW,
1007             # but don't wait to get down there. Currently at GDK_PRIORITY_EVENTS from
1008             # the sync and the sync means now is a good time for a forced draw.
1009             #
1010             if ($self->drawable) {
1011             $self->queue_draw;
1012             $self->window->process_updates (1);
1013             }
1014             }
1015              
1016              
1017             #------------------------------------------------------------------------------
1018             # drawing style changes
1019              
1020             # 'direction_changed' class closure
1021             sub _do_direction_changed {
1022             my ($self, $prev_dir) = @_;
1023             _pixmap_queue_draw ($self);
1024              
1025             # As of Gtk 2.18 the GtkWidget code in gtk_widget_real_direction_changed()
1026             # (previously called gtk_widget_direction_changed()) does a queue_resize(),
1027             # which is neither needed or wanted here. But a direction change should
1028             # be infrequent and better make sure anything GtkWidget does in the future
1029             # gets run.
1030             $self->signal_chain_from_overridden ($prev_dir);
1031             }
1032              
1033             # 'notify' class closure
1034             # SET_PROPERTY() is called only for own class properties, this default
1035             # handler sees changes to those defined by the GtkWidget superclass (and all
1036             # other sub and super classes)
1037             sub _do_notify {
1038             my ($self, $pspec) = @_;
1039             my $pname = $pspec->get_name;
1040             ### TickerView _do_notify(): $pname
1041              
1042             if ($pname eq 'sensitive') {
1043             # gtk_widget_set_sensitive does $self->queue_draw, so just need to
1044             # invalidate pixmap contents here for a redraw
1045             ### redraw
1046             _pixmap_queue_draw ($self);
1047             }
1048             $self->signal_chain_from_overridden ($pspec);
1049             }
1050              
1051             # 'state_changed' class closure ($self, $state)
1052             # 'style_set' class closure ($self, $prev_style)
1053             sub _do_state_or_style_changed {
1054             my ($self, $arg) = @_;
1055             if (DEBUG) { print "TickerView state-changed or style-set '",
1056             (defined $arg ? $arg : 'undef'),"'\n"; }
1057             _pixmap_queue_draw ($self);
1058             $self->signal_chain_from_overridden ($arg);
1059             }
1060              
1061              
1062             #------------------------------------------------------------------------------
1063             # scroll timer
1064             #
1065             # _update_timer() starts or stops the timer according to the numerous
1066             # conditions set out in that func. In general the idea is not to run the
1067             # timer when there's nothing to see and/or move: eg. the window is not
1068             # visible, or there's nothing in the model+renderers. Care must be taken to
1069             # call _update_timer() when any of the conditions may have changed.
1070             #
1071             # The timer action itself is pretty simple, it just calls the public
1072             # $self->scroll_pixels() to make the move, by an amount based on elapsed
1073             # real time, per "OTHER NOTES" in the pod below. The hairy stuff in
1074             # scroll_pixels() collapsing multiple motions and drawing works just as well
1075             # from the timer as it does from application uses of that func. In fact the
1076             # collapsing exists mainly to help if the timer runs a touch too fast for
1077             # the server's drawing.
1078             #
1079              
1080             # _TIMER_PRIORITY is below the drawing priority GDK_PRIORITY_EVENTS in
1081             # _sync_call_handler(), so drawing of the current position goes out before
1082             # making a scroll to a new position.
1083             #
1084             # Try _TIMER_PRIORITY below GDK_PRIORITY_REDRAW too, to hopefully cooperate
1085             # with redrawing of other widgets, letting their drawing go out before
1086             # scrolling the ticker.
1087             #
1088             use constant _TIMER_PRIORITY => (GDK_PRIORITY_REDRAW + 10);
1089              
1090             # _gettime() returns a floating point count of seconds since some fixed but
1091             # unspecified origin time.
1092             #
1093             # clock_gettime(CLOCK_REALTIME) is preferred. clock_gettime() always
1094             # exists, but it croaks if there's no such C library func. In that case
1095             # fall back on the hires time(), which is whatever best thing Time::HiRes
1096             # can do, probably gettimeofday() normally.
1097             #
1098             # Maybe it'd be worth checking clock_getres() to see it's a decent
1099             # resolution. It's conceivable some old implementations might do
1100             # CLOCK_REALTIME just from the CLK_TCK times() counter, giving only 10
1101             # millisecond resolution. That's enough for a modest 10 or 20 frames/sec,
1102             # but if attempting say 100 frames on a fast computer for ultra smoothness
1103             # then higher resolution would be needed.
1104             #
1105             sub _gettime {
1106             return Time::HiRes::clock_gettime (Time::HiRes::CLOCK_REALTIME());
1107             }
1108             unless (eval { _gettime(); 1 }) {
1109             ### TickerView fallback to Time HiRes time() due to clock_gettime() error: $@
1110             no warnings;
1111             *_gettime = \&Time::HiRes::time;
1112             }
1113              
1114             # start or stop the scroll timer according to the various settings
1115             sub _update_timer {
1116             my ($self) = @_;
1117              
1118             my $want_timer = $self->{'run'}
1119             && ! $self->{'paused_count'}
1120             && $self->mapped
1121             && $self->{'visibility_state'} ne 'fully-obscured'
1122             && $self->{'cellinfo_list'}
1123             && @{$self->{'cellinfo_list'}} # renderer list not empty
1124             && $self->{'frame_rate'} > 0
1125             && ! defined $self->{'drag_xy'} # not in a drag
1126             && $self->{'model'}
1127             && ! $self->{'model_empty'};
1128              
1129             if (DEBUG) {
1130             print " _update_timer run=", $self->{'run'},
1131             " paused=", ($self->{'paused_count'}||'no'),
1132             " mapped=", $self->mapped ? 1 : 0,
1133             " visibility=", $self->{'visibility_state'},
1134             " model=", ($self->{'model'} ? 'yes' : 'none'),
1135             " model_empty=", $self->{'model_empty'} || '0',
1136             " (iter_first=", (! defined $self->{'model'} ? 'n/a)'
1137             : $self->{'model'}->get_iter_first ? 'yes)' : 'no)'),
1138             " --> want ", ($want_timer ? 'yes' : 'no'), "\n";
1139             }
1140              
1141             $self->{'timer'} = $want_timer &&
1142             ($self->{'timer'} # existing timer if already running
1143             || do { # otherwise a new one
1144             my $period = POSIX::ceil (1000.0 / $self->{'frame_rate'});
1145             if (DEBUG) { print "TickerView start timer, $period ms\n"; }
1146            
1147             my $weak_self = $self;
1148             Scalar::Util::weaken ($weak_self);
1149             $self->{'prev_time'} = _gettime();
1150             require Glib::Ex::SourceIds;
1151             Glib::Ex::SourceIds->new
1152             (Glib::Timeout->add ($period, \&_do_timer, \$weak_self,
1153             _TIMER_PRIORITY));
1154             });
1155             }
1156              
1157             sub _do_timer {
1158             my ($ref_weak_self) = @_;
1159             # shouldn't see an undef in $$ref_weak_self because the timer should be
1160             # stopped already by _do_unrealize in the course of widget destruction,
1161             # but if for some reason that hasn't happened then stop it now
1162             my $self = $$ref_weak_self || return 0; # Glib::SOURCE_REMOVE
1163              
1164             my $t = _gettime();
1165             my $delta = $t - $self->{'prev_time'};
1166             $self->{'prev_time'} = $t;
1167              
1168             # Watch out for the clock going backwards, don't want to scroll back.
1169             # Watch out for jumping wildly forwards too due to the process blocked for
1170             # a while, don't want to churn through some massive pixel count forwards.
1171             $delta = min (5, max (0, $delta));
1172              
1173             my $step = $self->{'speed'} * $delta;
1174             $self->scroll_pixels ($step);
1175             if (DEBUG >= 2) { print "_do_timer scroll $delta seconds, $step pixels,",
1176             " to $self->{'want_x'}\n"; }
1177             return 1; # Glib::SOURCE_CONTINUE
1178             }
1179              
1180             # 'map' class closure ($self)
1181             # 'unmap' class closure ($self)
1182             #
1183             # This is asking the widget to map or unmap itself, not map-event or
1184             # unmap-event back from the server.
1185             #
1186             sub _do_map_or_unmap {
1187             my ($self) = @_;
1188             ### TickerView _do_map_or_unmap()
1189              
1190             # chain before _update_timer(), so the GtkWidget code sets or unsets the
1191             # mapped flag which _update_timer() will look at
1192             $self->signal_chain_from_overridden;
1193             _update_timer ($self);
1194             }
1195              
1196             # 'unrealize' class closure
1197             # (asking the widget to unrealize itself)
1198             #
1199             # When a ticker is removed from a container only unrealize is called, not
1200             # unmap then unrealize, hence an _update_timer() check here as well as
1201             # _do_map_or_unmap() above.
1202             #
1203             sub _do_unrealize {
1204             my ($self) = @_;
1205             ### TickerView _do_unrealize()
1206              
1207             # chain before _update_timer(), so the GtkWidget code clears the mapped flag
1208             $self->signal_chain_from_overridden;
1209              
1210             @{$self->{'drawn_array'}} = (); # full redraw if realized again later
1211             $self->{'pixmap'} = undef; # possible different depth if realized again later
1212             _update_timer ($self);
1213             }
1214              
1215             # 'visibility_notify_event' class closure
1216             sub _do_visibility_notify_event {
1217             my ($self, $event) = @_;
1218             ### TickerView _do_visibility_notify_event(): $event->state
1219             $self->{'visibility_state'} = $event->state;
1220             _update_timer ($self);
1221             return $self->signal_chain_from_overridden ($event);
1222             }
1223              
1224              
1225             #------------------------------------------------------------------------------
1226             # dragging
1227             #
1228             # The basic operation here is pretty simple, it's just a matter of calling
1229             # the public $self->scroll_pixels() with each mouse move amount as reported
1230             # by motion-notify. The hairy stuff in scroll_pixels() to collapse moving
1231             # and drawing works just as well for moves here as for application calls.
1232             #
1233             # If someone does a grab_pointer, either within the program or another
1234             # client, then we'll no longer get motion notifies. Should timer based
1235             # scrolling resume immediately, or only on button release? If the new grab
1236             # is some unrelated action taking over then immediately might be best. But
1237             # only on button release may be more consistent, in having the timer
1238             # scrolling resume only on button release. The latter is done for now.
1239             #
1240             # If the window is moved during the drag, either repositioned by application
1241             # code, or repositioned by the window manager etc, then it's possible to
1242             # either
1243             #
1244             # 1. Let the displayed contents stay with the left edge of the window.
1245             # 2. Let the displayed contents stay with the mouse, so it's like the
1246             # window move reveals a different portion.
1247             #
1248             # Neither is too difficult, but 1 is adopted since in 2 there's a bit of
1249             # flashing when the server copies the contents with the move and they then
1250             # have to be redrawn. (Redrawn under size-allocate, since there's only
1251             # configure-notify for a window move, no mouse motion-notify event.)
1252             #
1253             # Perhaps the double-buffering extension could help with the flashing, but
1254             # it'd have to be applied to the parent window, and could only work when the
1255             # move originates client-side, not say from the window manager. For now 1
1256             # is easier, and window moves during a drag should be fairly unusual anyway.
1257             #
1258             # To implement 1 the mouse position for dragging is maintained in root
1259             # window coordinates. This means it's independent of the ticker window
1260             # position. On that basis don't need to pay any attention to the window
1261             # position, simply apply root window based mouse motion to scroll_pixels().
1262             # Both x and y are maintained so that you can actually change the
1263             # "orientation" property in the middle of a drag and still get the right
1264             # result!
1265             #
1266              
1267             # if (0) {
1268             # my $bindings = Gtk2::BindingSet->new ('Gtk2__Ex__TickerView');
1269             # $bindings->entry_add_signal
1270             # (Gtk2::Gdk->keyval_from_name('Pointer_Button1'),[],
1271             # 'start-drag');
1272             # $bindings->entry_add_signal
1273             # (Gtk2::Gdk->keyval_from_name('Pointer_Button1'),['release-mask'],
1274             # 'end-drag');
1275             # # priority level "gtk" treating this as widget level default, for
1276             # # overriding by application or user RC
1277             # $bindings->add_path ('class', 'Gtk2__Ex__TickerView', 'gtk');
1278             # }
1279              
1280             sub is_drag_active {
1281             my ($self) = @_;
1282             return (defined $self->{'drag_xy'});
1283             }
1284              
1285             # 'button_press_event' class closure, getting Gtk2::Gdk::Event::Button
1286             sub _do_button_press_event {
1287             my ($self, $event) = @_;
1288             #### TickerView button_press: $event->button
1289             if ($event->button == 1) {
1290             $self->{'drag_xy'} = [ $event->root_coords ];
1291             _update_timer ($self); # stop timer
1292             }
1293             return $self->signal_chain_from_overridden ($event);
1294             }
1295              
1296             # 'motion_notify_event' class closure, getting Gtk2::Gdk::Event::Motion
1297             #
1298             # Use of is_hint() supports 'pointer-motion-hint-mask' perhaps set by the
1299             # application or some add-on feature. Dragging only runs from a mouse
1300             # button so for now it's enough to use get_pointer() rather than
1301             # $display->get_state().
1302             #
1303             sub _do_motion_notify_event {
1304             my ($self, $event) = @_;
1305             #### TickerView _do_motion_notify_event()
1306             if (defined $self->{'drag_xy'}) { # ignore motion/drags of other buttons
1307             _drag_scroll ($self, $event);
1308             }
1309             return $self->signal_chain_from_overridden ($event);
1310             }
1311              
1312             # 'button_release_event' class closure, getting Gtk2::Gdk::Event::Button
1313             #
1314             sub _do_button_release_event {
1315             my ($self, $event) = @_;
1316             #### TickerView _do_button_release_event(): $event->button
1317              
1318             if (defined $self->{'drag_xy'} && $event->button == 1) {
1319             _drag_scroll ($self, $event); # final dragged position from this event
1320             delete $self->{'drag_xy'};
1321             _update_timer ($self); # restart timer
1322             }
1323             return $self->signal_chain_from_overridden ($event);
1324             }
1325              
1326             # $event is either Gtk2::Gdk::Event::Motion or Gtk2::Gdk::Event::Button
1327             sub _drag_scroll {
1328             my ($self, $event) = @_;
1329             if (DEBUG >= 2) { print " _drag_scroll ",
1330             ($event->can('is_hint') && $event->is_hint
1331             ? 'hint' : 'not-hint'), "\n"; }
1332              
1333             my @xy = ($event->can('is_hint') && $event->is_hint
1334             ? $self->get_root_window->get_pointer
1335             : $event->root_coords);
1336              
1337             # step is simply how much the new position has moved from the old one
1338             my $i = $self->{'vertical'};
1339             my $step = $self->{'drag_xy'}->[$i] - $xy[$i];
1340             @{$self->{'drag_xy'}} = @xy;
1341              
1342             if ($self->get_direction eq 'rtl') { $step = -$step; }
1343             if (DEBUG >= 2) { print " step $step\n"; }
1344             $self->scroll_pixels ($step);
1345             }
1346              
1347             #------------------------------------------------------------------------------
1348             # mouse wheel scroll
1349              
1350             my %direction_sign = (up => -1,
1351             down => 1,
1352             left => -1,
1353             right => 1);
1354             my %direction_is_vertical = (up => 1,
1355             down => 1,
1356             left => 0,
1357             right => 0);
1358             # 'scroll-event' class closure, getting Gtk2::Gdk::Event::Scroll
1359             sub _do_scroll_event {
1360             my ($self, $event) = @_;
1361             #### TickerView scroll-event: $event->direction
1362             my $dir = $event->direction;
1363             my $vertical = $self->{'vertical'};
1364              
1365             # width when horiz, height when vert
1366             my $step = ($self->allocation->values)[2 + $vertical]
1367             * ($event->state & 'control-mask' ? 0.9 : 0.1)
1368             * $direction_sign{$dir};
1369             unless ($direction_is_vertical{$dir} ^ $vertical) {
1370             if ($self->get_direction eq 'rtl') { $step = - $step; }
1371             }
1372             $self->scroll_pixels ($step);
1373             return $self->signal_chain_from_overridden ($event);
1374             }
1375              
1376             #------------------------------------------------------------------------------
1377             # renderer changes
1378              
1379             sub _cellinfo_list_changed {
1380             my ($self) = @_;
1381             ### TickerView _cellinfo_list_changed()
1382             %{$self->{'row_widths'}} = ();
1383             _pixmap_queue_draw ($self);
1384             _update_timer ($self); # possible newly empty or non-empty cellinfo list
1385             $self->SUPER::_cellinfo_list_changed;
1386             }
1387              
1388             sub _cellinfo_attributes_changed {
1389             my ($self) = @_;
1390             ### TickerView _cellinfo_attributes_changed()
1391             %{$self->{'row_widths'}} = ();
1392             _pixmap_queue_draw ($self);
1393             $self->SUPER::_cellinfo_attributes_changed;
1394             }
1395              
1396              
1397             #------------------------------------------------------------------------------
1398             # model changes
1399             #
1400             # The main optimization attempted here is to do nothing when changed rows
1401             # are off-screen, or rather off-pixmap, with the aim of doing no drawing
1402             # when undisplayed parts of the model change.
1403             #
1404             # In practice you have to be in fixed-height-mode for off-screen updates to
1405             # do nothing at all since in the default "all rows sized" mode any change or
1406             # insert or delete has to re-examine all rows. The insert could check only
1407             # for an increase, but doesn't do that currently.
1408             #
1409              
1410             # 'row-changed' on the model
1411             sub _do_row_changed {
1412             my ($model, $path, $iter, $ref_weak_self) = @_;
1413             my $self = $$ref_weak_self || return;
1414             ### _do_row_changed() path: $path->to_string
1415             $path->get_depth == 1 || return; # only top rows of the model
1416             my ($index) = $path->get_indices;
1417              
1418             # recalculate width
1419             delete $self->{'row_widths'}->{$index};
1420              
1421             # fixed-height-mode means every row is the same height so a change to any
1422             # one of them doesn't affect the previously calculated size -- even when
1423             # it's a change to the representative row 0. Believe that's how
1424             # GtkTreeView interprets its fixed-height-mode, and it has the happy
1425             # effect of not provoking repeated rechecks if row 0 changes a lot.
1426             #
1427             # In non fixed-height-mode, however, every row change potentially
1428             # increases or decreases the height. (Or at least a change to the highest
1429             # could decrease, or a change to any of the equal highest could increase.)
1430             #
1431             if (! $self->{'fixed_height_mode'}) {
1432             $self->queue_resize;
1433             }
1434              
1435             # if changed row is in pixmap then redraw
1436             _pixmap_queue_draw_if_index ($self, $index);
1437             }
1438              
1439             # 'row-inserted' on the model
1440             sub _do_row_inserted {
1441             my ($model, $path, $iter, $ref_weak_self) = @_;
1442             my $self = $$ref_weak_self || return;
1443             ### _do_row_inserted() path: $path->to_string
1444             $path->get_depth == 1 || return; # only top rows
1445             my ($index) = $path->get_indices;
1446              
1447             if ($self->{'model_empty'}) {
1448             # empty -> non-empty restarts timer if stopped due to empty
1449             _update_timer ($self);
1450             }
1451             if ($self->{'model_empty'} || ! $self->{'fixed_height_mode'}) {
1452             # empty -> non-empty changes size from zero to something;
1453             # and any new row insertion resizes when non fixed-height-mode
1454             # (could just see if this new row bigger than already calculated)
1455             $self->queue_resize;
1456             }
1457             $self->{'model_empty'} = 0;
1458              
1459             _pixmap_queue_draw_if_index ($self, $index);
1460             _apply_remap ($self,
1461             # called as $new_index = $remap->($old_index)
1462             sub { $_[0] >= $index ? $_[0] + 1 : $_[0] });
1463             }
1464              
1465             # 'row-deleted' on the model
1466             sub _do_row_deleted {
1467             my ($model, $path, $ref_weak_self) = @_;
1468             my $self = $$ref_weak_self || return;
1469             ### _do_row_deleted() path: $path->to_string
1470             $path->get_depth == 1 || return; # only top rows
1471             my ($index) = $path->get_indices;
1472              
1473             delete $self->{'row_widths'}->{$index};
1474              
1475             my $model_empty = $self->{'model_empty'} = ! $model->get_iter_first;
1476             if ($model_empty) {
1477             # becoming empty, stop timer while empty
1478             _update_timer ($self);
1479             }
1480             if ($model_empty || ! $self->{'fixed_height_mode'}) {
1481             # becoming empty will become zero size;
1482             # or if ever row checked then any delete affects height
1483             # (actually only a delete of the highest row affects it, if wanted to
1484             # record which was the biggest)
1485             $self->queue_resize;
1486             }
1487              
1488             # want_index and row_widths move down.
1489             # If want_index itself is deleted then leave it unchanged to show from the
1490             # next following, and if it was the last row then it'll wrap around in the
1491             # draw.
1492             #
1493             _pixmap_queue_draw_if_index ($self, $index);
1494             _apply_remap ($self,
1495             # called as $new_index = $remap->($old_index)
1496             sub { $_[0] > $index ? $_[0] - 1 : $_[0] });
1497             }
1498              
1499             # 'rows-reordered' signal on the model
1500             sub _do_rows_reordered {
1501             my ($model, $reordered_path, $reordered_iter, $aref, $ref_weak_self) = @_;
1502             my $self = $$ref_weak_self || return;
1503             ### _do_rows_reordered()
1504             if (defined $reordered_iter) { return; } # top rows only
1505              
1506             # $oldpos == $aref->[$newpos], ie. aref says where the row used to be.
1507             # $remap{$oldpos} == $newpos, ie. where the old has been sent
1508             # Building a hash might be a bit unnecessary if not much to remap, but
1509             # it's less code than a linear search or similar.
1510             #
1511             my %remap;
1512             @remap{@$aref} = (0 .. $#$aref);
1513             if (DEBUG) { require Data::Dumper;
1514             print " remap ",
1515             Data::Dumper->new([\%remap],['remap'])->Sortkeys(1)->Dump; }
1516             # want_index and row_widths permute
1517             # allow for indexes out of range in case want_index yet unnormalized
1518             # called as $new_index = $remap->($old_index)
1519             my $remap = sub { defined $remap{$_[0]} ? $remap{$_[0]} : $_[0] };
1520              
1521             # Set the pixmap to redraw if the drawn rows are no longer a contiguous
1522             # run, modulo the model length. This is a bit of work to check, but the
1523             # pixmap contents can be retained nicely under rotations or shuffle ups or
1524             # downs that don't affect the displayed portion.
1525             my $len = scalar @$aref;
1526             my $delta_func = sub { ($_[0] - $remap->($_[0]) + $len) % $len };
1527             my $drawn = $self->{'drawn_array'};
1528             if (@$drawn) {
1529             my $want_delta = $delta_func->($drawn->[0]);
1530             for (my $i = 2; $i < @$drawn; $i += 2) {
1531             if ($delta_func->($drawn->[$i]) != $want_delta) {
1532             _pixmap_queue_draw ($self); # shuffled about, must redraw
1533             last;
1534             }
1535             }
1536             }
1537             _apply_remap ($self, $remap);
1538             }
1539              
1540             # set the pixmap to redraw if $index is drawn in it
1541             sub _pixmap_queue_draw_if_index {
1542             my ($self, $index) = @_;
1543             my $drawn = $self->{'drawn_array'};
1544             for (my $i = 0; $i < @$drawn; $i += 2) {
1545             if ($drawn->[$i] == $index) {
1546             _pixmap_queue_draw ($self);
1547             return 1;
1548             }
1549             }
1550             return 0;
1551             }
1552              
1553             # Remap indexes in want_index, drawn_array, and row_widths. drawn_array is
1554             # empty at this point if it's going to be redrawn
1555             sub _apply_remap {
1556             my ($self, $remap) = @_;
1557             ### _apply_remap(): $remap
1558              
1559             if (defined (my $want_index = $self->{'want_index'})) {
1560             if (DEBUG) { print " want_index $want_index to ",
1561             $remap->($want_index),"\n"; }
1562             $self->{'want_index'} = $remap->($want_index);
1563             }
1564             my $drawn = $self->{'drawn_array'};
1565             if (@$drawn) {
1566             for (my $i = 0; $i < @$drawn; $i += 2) {
1567             $drawn->[$i] = $remap->($drawn->[$i]);
1568             }
1569             }
1570             _hash_keys_remap ($self->{'row_widths'}, $remap);
1571             }
1572              
1573             # modify the keys in %$href by $newkey = $func->($oldkey)
1574             # the value associated with $oldkey moves to $newkey
1575             #
1576             sub _hash_keys_remap {
1577             my ($href, $func) = @_;
1578             %$href = map { ($func->($_), $href->{$_}) } keys %$href;
1579             }
1580              
1581              
1582             #------------------------------------------------------------------------------
1583             # generic helpers
1584              
1585             # _make_all_zeros_proc() returns a procedure to be called
1586             # $func->($index,$width) designed to protect against every $index having a
1587             # zero $width.
1588             #
1589             # $func returns true until it sees an $index==0 and then a second $index==0,
1590             # with all calls having $width==0. The idea is that if the drawing,
1591             # scrolling or whatever loop has gone from $index zero all the way up and
1592             # around back to $index zero again, and all the $width's seen are zero, then
1593             # it should bail out.
1594             #
1595             # Any non-zero $width seen makes the returned procedure always return true.
1596             # It might be only a single index position out of thousands, but that's
1597             # enough.
1598             #
1599             sub _make_all_zeros_proc {
1600             my $seen_nonzero = 0;
1601             my $count_index_zero = 0;
1602             return sub {
1603             my ($index, $width) = @_;
1604             if ($width != 0) { $seen_nonzero = 1; }
1605             if ($index == 0) { $count_index_zero++; }
1606             return (! $seen_nonzero) && ($count_index_zero >= 2);
1607             }
1608             }
1609              
1610              
1611             #------------------------------------------------------------------------------
1612             # other method funcs
1613              
1614             sub get_path_at_pos {
1615             my ($self, $x, $y) = @_;
1616             ### get_path_at_pos(): "$x,$y"
1617              
1618             # Go from the want_x/want_index desired position, even if the drawing
1619             # isn't yet actually displaying that. This makes most sense after a
1620             # programmatic scroll, and if it's a user button press then the display
1621             # will only be a moment away from showing that want_x/want_index position.
1622             #
1623             my $index = $self->{'want_index'};
1624             $x -= $self->{'want_x'};
1625             if (DEBUG) { print " adj for want_x=",$self->{'want_x'},", to x=$x\n"; }
1626              
1627             ($x, $index) = _normalize ($self, -$x, $index);
1628             if (DEBUG) { print " got ", (defined $x ? $x : 'undef'),
1629             ",",(defined $index ? $index : 'undef'),"\n"; }
1630             if (defined $x) {
1631             return Gtk2::TreePath->new_from_indices ($index);
1632             } else {
1633             return undef;
1634             }
1635             }
1636              
1637             1;
1638             __END__