File Coverage

blib/lib/Tickit/ContainerWidget.pm
Criterion Covered Total %
statement 14 14 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 19 19 100.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2009-2017 -- leonerd@leonerd.org.uk
5              
6             package Tickit::ContainerWidget;
7              
8 1     1   4 use strict;
  1         1  
  1         26  
9 1     1   2 use warnings;
  1         1  
  1         18  
10 1     1   22 use 5.010; # //
  1         2  
11 1     1   3 use feature qw( switch );
  1         1  
  1         79  
12 1     1   4 use base qw( Tickit::Widget );
  1         1  
  1         417  
13             no if $] >= 5.017011, warnings => 'experimental::smartmatch';
14              
15             our $VERSION = '0.52';
16              
17             use Carp;
18              
19             use Scalar::Util qw( refaddr );
20              
21             =head1 NAME
22              
23             C - abstract base class for widgets that contain
24             other widgets
25              
26             =head1 SYNOPSIS
27              
28             TODO
29              
30             =head1 DESCRIPTION
31              
32             This class acts as an abstract base class for widgets that contain at leaast
33             one other widget object. It provides storage for a hash of "options"
34             associated with each child widget.
35              
36             =head1 STYLE
37              
38             The following style tags are used:
39              
40             =over 4
41              
42             =item :focus-child
43              
44             Set whenever a child widget within the container has the input focus.
45              
46             =back
47              
48             =cut
49              
50             sub new
51             {
52             my $class = shift;
53              
54             foreach my $method (qw( children )) {
55             $class->can( $method ) or
56             croak "$class cannot ->$method - do you subclass and implement it?";
57             }
58              
59             my $self = $class->SUPER::new( @_ );
60              
61             $self->{child_opts} = {};
62              
63             return $self;
64             }
65              
66             =head2 $widget->add( $child, %opts )
67              
68             Sets the child widget's parent, stores the options for the child, and calls
69             the C method. The concrete implementation will have to
70             implement storage of this child widget.
71              
72             Returns the container C<$widget> itself, for easy chaining.
73              
74             =cut
75              
76             sub add
77             {
78             my $self = shift;
79             my ( $child, %opts ) = @_;
80              
81             $child->set_parent( $self );
82              
83             $self->{child_opts}{refaddr $child} = \%opts;
84              
85             $self->children_changed;
86              
87             return $self;
88             }
89              
90             =head2 $widget->remove( $child_or_index )
91              
92             Removes the child widget's parent, and calls the C method.
93             The concrete implementation will have to remove this child from its storage.
94              
95             Returns the container C<$widget> itself, for easy chaining.
96              
97             =cut
98              
99             sub remove
100             {
101             my $self = shift;
102             my ( $child ) = @_;
103              
104             $child->set_parent( undef );
105             $child->window->close if $child->window;
106             $child->set_window( undef );
107              
108             delete $self->{child_opts}{refaddr $child};
109              
110             $self->children_changed;
111              
112             return $self;
113             }
114              
115             =head2 %opts = $widget->child_opts( $child )
116              
117             =head2 $opts = $widget->child_opts( $child )
118              
119             Returns the options currently set for the given child as a key/value list in
120             list context, or as a HASH reference in scalar context. The HASH reference in
121             scalar context is the actual hash used to store the options - modifications to
122             it will be preserved.
123              
124             =cut
125              
126             sub child_opts
127             {
128             my $self = shift;
129             my ( $child ) = @_;
130              
131             my $opts = $self->{child_opts}{refaddr $child};
132             return $opts if !wantarray;
133             return %$opts;
134             }
135              
136             =head2 $widget->set_child_opts( $child, %newopts )
137              
138             Sets new options on the given child. Any options whose value is given as
139             C are deleted.
140              
141             =cut
142              
143             sub set_child_opts
144             {
145             my $self = shift;
146             my ( $child, %newopts ) = @_;
147              
148             my $opts = $self->{child_opts}{refaddr $child};
149              
150             foreach ( keys %newopts ) {
151             defined $newopts{$_} ? ( $opts->{$_} = $newopts{$_} ) : ( delete $opts->{$_} );
152             }
153              
154             $self->children_changed;
155             }
156              
157             sub child_resized
158             {
159             my $self = shift;
160             $self->reshape if $self->window;
161             $self->resized;
162             }
163              
164             sub children_changed
165             {
166             my $self = shift;
167              
168             $self->reshape if $self->window;
169             $self->resized;
170             }
171              
172             sub window_gained
173             {
174             my $self = shift;
175             $self->SUPER::window_gained( @_ );
176              
177             $self->window->set_focus_child_notify( 1 );
178             }
179              
180             sub window_lost
181             {
182             my $self = shift;
183              
184             foreach my $child ( $self->children ) {
185             my $childwin = $child->window;
186             $childwin and $childwin->close;
187              
188             $child->set_window( undef );
189             }
190              
191             $self->SUPER::window_lost( @_ );
192             }
193              
194             sub _on_win_focus
195             {
196             my $self = shift;
197             $self->SUPER::_on_win_focus( @_ );
198              
199             $self->set_style_tag( "focus-child" => $_[1] ) if $_[2];
200             }
201              
202             =head2 $child = $widget->find_child( $how, $other, %args )
203              
204             Returns a child widget. The C<$how> argument determines how this is done,
205             relative to the child widget given by C<$other>:
206              
207             =over 4
208              
209             =item first
210              
211             The first child returned by C (C<$other> is ignored)
212              
213             =item last
214              
215             The last child returned by C (C<$other> is ignored)
216              
217             =item before
218              
219             The child widget just before C<$other> in the order given by C
220              
221             =item after
222              
223             The child widget just after C<$other> in the order given by C
224              
225             =back
226              
227             Takes the following named arguments:
228              
229             =over 8
230              
231             =item where => CODE
232              
233             Optional. If defined, gives a filter function to filter the list of children
234             before searching for the required one. Will be invoked once per child, with
235             the child widget set as C<$_>; it should return a boolean value to indicate if
236             that child should be included in the search.
237              
238             =back
239              
240             =cut
241              
242             sub find_child
243             {
244             my $self = shift;
245             my ( $how, $other, %args ) = @_;
246              
247             my $children = $args{children} // "children";
248             my @children = $self->$children;
249             if( my $where = $args{where} ) {
250             @children = grep { defined $other and $_ == $other or $where->() } @children;
251             }
252              
253             for( $how ) {
254             when( "first" ) {
255             return $children[0];
256             }
257             when( "last" ) {
258             return $children[-1];
259             }
260             when( "before" ) {
261             $children[$_] == $other and return $children[$_-1] for 1 .. $#children;
262             return undef;
263             }
264             when( "after" ) {
265             $children[$_] == $other and return $children[$_+1] for 0 .. $#children-1;
266             return undef;
267             }
268             default {
269             croak "Unrecognised ->find_child mode '$how'";
270             }
271             }
272             }
273              
274             use constant CONTAINER_OR_FOCUSABLE => sub {
275             $_->isa( "Tickit::ContainerWidget" ) or
276             $_->window && $_->window->is_visible && $_->CAN_FOCUS
277             };
278              
279             =head2 $widget->focus_next( $how, $other )
280              
281             Moves the input focus to the next widget in the widget tree, by searching in
282             the direction given by C<$how> relative to the widget given by C<$other>
283             (which must be an immediate child of C<$widget>).
284              
285             The direction C<$how> must be one of the following four values:
286              
287             =over 4
288              
289             =item first
290              
291             =item last
292              
293             Moves focus to the first or last child widget that can take focus. Recurses
294             into child widgets that are themselves containers. C<$other> is ignored.
295              
296             =item after
297              
298             =item before
299              
300             Moves focus to the next or previous child widget in tree order from the one
301             given by C<$other>. Recurses into child widgets that are themselves
302             containers, and out into parent containers.
303              
304             These searches will wrap around the widget tree; moving C the last node
305             in the widget tree will move to the first, and vice versa.
306              
307             =back
308              
309             This differs from C in that it performs a full tree search through
310             the widget tree, considering parents and children. If a C or C
311             search falls off the end of one node, it will recurse up to its parent and
312             search within the next child, and so on.
313              
314             Usually this would be used via the widget itself:
315              
316             $self->parent->focus_next( $how => $self );
317              
318             =cut
319              
320             sub focus_next
321             {
322             my $self = shift;
323             my ( $how, $other ) = @_;
324              
325             # This tree search has the potential to loop infinitely, if there are no
326             # focusable widgets at all. It would only do this if it cycles via the root
327             # widget twice in a row. Technically we could detect it earlier, but that
328             # is more difficult to arrange for
329             my $done_root;
330              
331             my $next;
332              
333             my $children = $self->can( "children_for_focus" ) || "children";
334              
335             while(1) {
336             $next = $self->find_child( $how, $other,
337             where => CONTAINER_OR_FOCUSABLE,
338             children => $children,
339             );
340             last if $next and $next->CAN_FOCUS;
341              
342             # Either we found a container (recurse into it),
343             if( $next ) {
344             my $childhow = $how;
345             if( $how eq "after" ) { $childhow = "first" }
346             elsif( $how eq "before" ) { $childhow = "last" }
347              
348             # See if child has it
349             return 1 if $next->focus_next( $childhow => undef );
350              
351             $other = $next;
352             redo;
353             }
354             # or we'll have to recurse up to my parent
355             elsif( my $parent = $self->parent ) {
356             if( $how eq "after" or $how eq "before" ) {
357             $other = $self;
358             $self = $parent;
359             redo;
360             }
361             else {
362             return undef;
363             }
364             }
365             # or we'll have to cycle around the root
366             else {
367             die "Cycled through the entire widget tree and did not find a focusable widget" if $done_root;
368             $done_root++;
369              
370             if( $how eq "after" ) { $how = "first" }
371             elsif( $how eq "before" ) { $how = "last" }
372             else { die "Cannot cycle how=$how around root widget"; }
373              
374             $other = undef;
375             redo;
376             }
377             }
378              
379             $next->take_focus;
380             return 1;
381             }
382              
383             =head1 SUBCLASS METHODS
384              
385             =head2 @children = $widget->children
386              
387             Required. Should return a list of all the contained child widgets. The order
388             is not specified, but should be in some stable order that makes sense given
389             the layout of the widget's children.
390              
391             This method is used by C to remove the windows from all the child
392             widgets automatically, and by C to obtain a child relative to
393             another given one.
394              
395             =head2 @children = $widget->children_for_focus
396              
397             Optional. If implemented, this method is called to obtain a list of child
398             widgets to perform a child search on when changing focus using the
399             C method. If it is not implemented, the regular C method
400             is called instead.
401              
402             Normally this method shouldn't be used, but it may be useful on container
403             widgets that also display "helper" widgets that should not be considered as
404             part of the main focus set. This method can then exclude them.
405              
406             =head2 $widget->children_changed
407              
408             Optional. If implemented, this method will be called after any change of the
409             contained child widgets or their options. Typically this will be used to set
410             windows on them by sub-dividing the window of the parent.
411              
412             If not overridden, the base implementation will call C.
413              
414             =head2 $widget->child_resized( $child )
415              
416             Optional. If implemented, this method will be called after a child widget
417             changes or may have changed its size requirements. Typically this will be used
418             to adjusts the windows allocated to children.
419              
420             If not overridden, the base implementation will call C.
421              
422             =head1 AUTHOR
423              
424             Paul Evans
425              
426             =cut
427              
428             0x55AA;