File Coverage

blib/lib/Tickit/Widget/Tabbed.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the Artistic License (the same terms
2             # as Perl itself)
3             #
4             # (C) Tom Molesworth 2011,
5             # Paul Evans, 2011-2015 -- leonerd@leonerd.org.uk
6              
7             package Tickit::Widget::Tabbed;
8              
9 1     1   581 use strict;
  1         1  
  1         25  
10 1     1   3 use warnings;
  1         1  
  1         31  
11 1     1   440 use parent qw(Tickit::ContainerWidget);
  1         246  
  1         6  
12             BEGIN {
13             Tickit::Widget->VERSION("0.12");
14             Tickit::Window->VERSION("0.23");
15             }
16             use Tickit::Style;
17             use constant KEYPRESSES_FROM_STYLE => 1;
18              
19             use Carp;
20             use Tickit::Pen;
21             use List::Util qw(max);
22              
23             use Tickit::Widget::Tabbed::Ribbon;
24              
25             our $VERSION = '0.021';
26              
27             =head1 NAME
28              
29             Tickit::Widget::Tabbed - provide tabbed window support
30              
31             =head1 SYNOPSIS
32              
33             use Tickit::Widget::Tabbed;
34             my $tabbed = Tickit::Widget::Tabbed->new;
35             $tabbed->add_tab(Tickit::Widget::Static->new(text => 'some text'), label => 'First tab');
36             $tabbed->add_tab(Tickit::Widget::Static->new(text => 'some text'), label => 'Second tab');
37              
38             =head1 DESCRIPTION
39              
40             Provides a container that operates as a tabbed window.
41              
42             Subclass of L.
43              
44             =cut
45              
46             =head1 STYLE
47              
48             The default style pen is used as the widget pen. The following style pen
49             prefixes are also used:
50              
51             =over 4
52              
53             =item ribbon => PEN
54              
55             The pen used for the ribbon
56              
57             =item active => PEN
58              
59             The pen attributes used for the active tab on the ribbon
60              
61             =item more => PEN
62              
63             The pen used for "more" ribbon scroll markers
64              
65             =back
66              
67             The following style keys are used:
68              
69             =over 4
70              
71             =item more_left => STRING
72              
73             =item more_right => STRING
74              
75             The text used to indicate that there is more content scrolled to the left or
76             right, respectively, in the ribbon
77              
78             =back
79              
80             =cut
81              
82             style_definition base =>
83             ribbon_fg => 7,
84             ribbon_bg => 4,
85             active_fg => 14,
86             more_fg => "cyan",
87             more_left => "<..",
88             more_right => "..>",
89              
90             '' => "next_tab",
91             '' => "prev_tab",
92             '' => "next_tab",
93             '' => "prev_tab";
94              
95             use constant WIDGET_PEN_FROM_STYLE => 1;
96              
97             =head1 METHODS
98              
99             =cut
100              
101             sub TAB_CLASS { shift->{tab_class} || "Tickit::Widget::Tabbed::Tab" }
102             sub RIBBON_CLASS { shift->{ribbon_class} || "Tickit::Widget::Tabbed::Ribbon" }
103              
104             =head2 new
105              
106             Instantiate a new tabbed window.
107              
108             Takes the following named parameters:
109              
110             =over 4
111              
112             =item * tab_position - (optional) location of the tabs, should be one of left, top, right, bottom.
113              
114             =back
115              
116             =cut
117              
118             sub new {
119             my $class = shift;
120             my %args = @_;
121             my $self = $class->SUPER::new(%args);
122              
123             $self->{tab_class} = delete($args{tab_class});
124             $self->{ribbon_class} = delete($args{ribbon_class});
125              
126             $self->tab_position(delete($args{tab_position}) || 'top');
127              
128             my $ribbon = $self->{ribbon};
129              
130             $ribbon->set_style( $self->get_style_pen("ribbon")->getattrs );
131              
132             return $self;
133             }
134              
135             # Positions for the four screen edges - these will return appropriate sizes
136             # for the tab and child subwindows
137             sub _window_position_left {
138             my $self = shift;
139             my $ribbon = $self->{ribbon};
140             my $label_width = $ribbon->cols;
141             return 0, 0, $self->window->lines, $label_width,
142             0, $label_width, $self->window->lines, $self->window->cols - $label_width;
143             }
144              
145             sub _window_position_right {
146             my $self = shift;
147             my $ribbon = $self->{ribbon};
148             my $label_width = $ribbon->cols;
149             return 0, $self->window->cols - $label_width, $self->window->lines, $label_width,
150             0, 0, $self->window->lines, $self->window->cols - $label_width;
151             }
152              
153             sub _window_position_top {
154             my $self = shift;
155             my $ribbon = $self->{ribbon};
156             my $label_height = $ribbon->lines;
157             $label_height = 1 unless $self->window->lines > $label_height;
158             return 0, 0, $label_height, $self->window->cols,
159             $label_height, 0, max(1, $self->window->lines - $label_height), $self->window->cols;
160             }
161              
162             sub _window_position_bottom {
163             my $self = shift;
164             my $ribbon = $self->{ribbon};
165             my $label_height = $ribbon->lines;
166             $label_height = 1 unless $self->window->lines > $label_height;
167             return $self->window->lines - $label_height, 0, $label_height, $self->window->cols,
168             0, 0, max(1, $self->window->lines - $label_height), $self->window->cols;
169             }
170              
171             sub on_style_changed_values {
172             my $self = shift;
173             my %values = @_;
174              
175             if( grep { $_ =~ m/^ribbon_/ } keys %values ) {
176             $self->{ribbon}->set_style( $self->get_style_pen("ribbon")->getattrs );
177             }
178             }
179              
180             sub reshape {
181             my $self = shift;
182             my $window = $self->window or return;
183             my $tab_position = $self->tab_position;
184             my @positions = $self->${\"_window_position_$tab_position"}();
185             if( my $ribbon_window = $self->{ribbon}->window ) {
186             $ribbon_window->change_geometry( @positions[0..3] );
187             }
188             else {
189             my $ribbon_window = $window->make_sub( @positions[0..3] );
190             $self->{ribbon}->set_window( $ribbon_window );
191             }
192             $self->{child_window_geometry} = [ @positions[4..7] ];
193             foreach my $tab ( $self->{ribbon}->tabs ) {
194             my $child = $tab->widget;
195             if( my $child_window = $child->window ) {
196             $child_window->change_geometry( @positions[4..7] );
197             }
198             else {
199             $child_window = $self->_new_child_window( $child == $self->active_tab->widget );
200             $child->set_window($child_window);
201             }
202             }
203             }
204              
205             sub _max_child_lines
206             {
207             my $self = shift;
208             my $ribbon = $self->{ribbon};
209             return max( 1, map { $_->widget->requested_lines } $ribbon->tabs );
210             }
211              
212             sub _max_child_cols
213             {
214             my $self = shift;
215             my $ribbon = $self->{ribbon};
216             return max( 1, map { $_->widget->requested_cols } $ribbon->tabs );
217             }
218              
219             sub lines
220             {
221             my $self = shift;
222             my $ribbon = $self->{ribbon};
223              
224             if( $ribbon->orientation eq "horizontal" ) {
225             return $ribbon->lines + $self->_max_child_lines;
226             }
227             else {
228             return max( $ribbon->lines, $self->_max_child_lines );
229             }
230             }
231              
232             sub cols
233             {
234             my $self = shift;
235             my $ribbon = $self->{ribbon};
236              
237             if( $ribbon->orientation eq "horizontal" ) {
238             return max( $ribbon->cols, $self->_max_child_cols );
239             }
240             else {
241             return $ribbon->cols + $self->_max_child_cols;
242             }
243             }
244              
245             # All the child widgets
246             sub children {
247             my $self = shift;
248             my $ribbon = $self->{ribbon};
249             return $ribbon, map { $_->widget } $ribbon->tabs;
250             }
251              
252             # The only focusable child widget is the active one
253             sub children_for_focus {
254             my $self = shift;
255             return $self->active_tab_widget;
256             }
257              
258             sub _new_child_window
259             {
260             my $self = shift;
261             my ( $visible ) = @_;
262              
263             my $window = $self->window or return undef;
264              
265             my $child_window = $window->make_hidden_sub( @{ $self->{child_window_geometry} } );
266             $child_window->show if $visible;
267              
268             return $child_window;
269             }
270              
271             sub window_lost {
272             my $self = shift;
273             $self->SUPER::window_lost(@_);
274             $_->widget->set_window(undef) for $self->{ribbon}->tabs;
275              
276             undef $self->{child_window_geometry};
277              
278             $self->{ribbon}->set_window(undef);
279             }
280              
281             =head2 tab_position
282              
283             Accessor for the tab position (top, left, right, bottom).
284              
285             =cut
286              
287             sub tab_position {
288             my $self = shift;
289             if(@_) {
290             my $pos = shift;
291             my $orientation = ( $pos eq "top" or $pos eq "bottom" ) ? "horizontal" :
292             ( $pos eq "left" or $pos eq "right" ) ? "vertical" :
293             croak "Unrecognised value for ->tab_position: $pos";
294              
295             if( !$self->{ribbon} or $self->{ribbon}->orientation ne $orientation ) {
296             my %args = (
297             tabbed => $self,
298             tab_position => $pos,
299             );
300             if( my $old_ribbon = $self->{ribbon} ) {
301             $old_ribbon->window->close;
302             $old_ribbon->set_window( undef );
303             $args{tabs} = [ $old_ribbon->tabs ];
304             $args{active_tab_index} = $old_ribbon->active_tab_index;
305             $args{pen} = $old_ribbon->pen;
306             $args{active_pen} = $old_ribbon->active_pen;
307             undef $self->{ribbon};
308             }
309             $self->{ribbon} = $self->RIBBON_CLASS->new_for_orientation(
310             $orientation, %args
311             );
312             $self->{ribbon}->set_style( $args{pen}->getattrs ) if $args{pen};
313             }
314              
315             $self->{tab_position} = $pos;
316             undef $self->{child_window_geometry};
317              
318             $self->reshape if $self->window;
319             $self->redraw;
320             }
321             return $self->{tab_position};
322             }
323              
324             sub _tabs_changed {
325             my $self = shift;
326             $self->reshape if $self->window;
327             $self->{ribbon}->redraw if $self->{ribbon}->window;
328             }
329              
330             =head2 active_tab_index
331              
332             Returns the 0-based index of the currently-active tab.
333              
334             =cut
335              
336             sub active_tab_index { shift->{ribbon}->active_tab_index }
337              
338             =head2 active_tab
339              
340             Returns the currently-active tab as a tab object. See below.
341              
342             =cut
343              
344             sub active_tab { shift->{ribbon}->active_tab }
345              
346             =head2 active_tab_widget
347              
348             Returns the widget in the currently active tab.
349              
350             =cut
351              
352             # Old name
353             *tab = \&active_tab_widget;
354             sub active_tab_widget {
355             my $self = shift; $self->active_tab && $self->active_tab->widget
356             }
357              
358             =head2 add_tab
359              
360             Add a new tab to this tabbed widget. Returns an object representing the tab;
361             see L below.
362              
363             First parameter is the widget to use.
364              
365             Remaining form a hash:
366              
367             =over 4
368              
369             =item label - label to show on the new tab
370              
371             =back
372              
373             =cut
374              
375             sub add_tab {
376             my $self = shift;
377             my ($child, %opts) = @_;
378              
379             my $ribbon = $self->{ribbon};
380              
381             my $tab = $self->TAB_CLASS->new( $self, widget => $child, %opts );
382              
383             $ribbon->append_tab( $tab );
384              
385             return $tab;
386             }
387              
388             =head2 remove_tab
389              
390             Remove tab given by 0-based index or tab object.
391              
392             =cut
393              
394             sub remove_tab { shift->{ribbon}->remove_tab( @_ ) }
395              
396             =head2 move_tab
397              
398             Move tab given by 0-based index or tab object forward the given number of
399             positions.
400              
401             =cut
402              
403             sub move_tab { shift->{ribbon}->move_tab( @_ ) }
404              
405             =head2 activate_tab
406              
407             Switch to the given tab; by 0-based index, or object.
408              
409             =cut
410              
411             sub activate_tab { shift->{ribbon}->activate_tab( @_ ) }
412              
413             =head2 next_tab
414              
415             Switch to the next tab. This may be bound as a key action.
416              
417             =cut
418              
419             *key_next_tab = \&next_tab;
420             sub next_tab { shift->{ribbon}->next_tab }
421              
422             =head2 prev_tab
423              
424             Switch to the previous tab. This may be bound as a key action.
425              
426             =cut
427              
428             *key_prev_tab = \&prev_tab;
429             sub prev_tab { shift->{ribbon}->prev_tab }
430              
431             sub child_resized {
432             my $self = shift;
433             $self->reshape;
434             }
435              
436             sub on_key {
437             my $self = shift;
438             my ($ev) = @_;
439              
440             return 1 if $self->{ribbon}->on_key(@_);
441              
442             return 0 unless $ev->type eq "key";
443              
444             my $str = $ev->str;
445             if($str =~ m/^M-(\d)$/ ) {
446             my $index = $1 - 1;
447             $self->activate_tab( $index ) if $index < $self->{ribbon}->tabs;
448             return 1;
449             }
450             return 0;
451             }
452              
453             sub render_to_rb {
454             my $self = shift;
455             my ( $rb, $rect ) = @_;
456              
457             # Just clear the child area if we have nothing better to do
458             $rb->eraserect( $rect );
459             }
460              
461             package Tickit::Widget::Tabbed::Tab;
462              
463             use 5.010; # for //= operator
464             use Scalar::Util qw( weaken );
465             use Tickit::Utils qw( textwidth );
466              
467             =head1 METHODS ON TAB OBJECTS
468              
469             The following methods may be called on the objects returned by C or
470             C.
471              
472             =cut
473              
474             sub new {
475             my $class = shift;
476             my ( $tabbed, %args ) = @_;
477             my $self = bless {
478             tabbed => $tabbed,
479             widget => $args{widget},
480             label => $args{label},
481             active => 0,
482             }, $class;
483             weaken( $self->{tabbed} );
484             return $self;
485             }
486              
487             =head2 index
488              
489             Returns the 0-based index of this tab
490              
491             =cut
492              
493             sub index {
494             my $self = shift;
495             return $self->{tabbed}->{ribbon}->_tab2index( $self );
496             }
497              
498             =head2 widget
499              
500             Returns the C contained by this tab
501              
502             =cut
503              
504             sub widget { shift->{widget} }
505              
506             =head2 label
507              
508             Returns the current label text
509              
510             =cut
511              
512             sub label_width {
513             my $self = shift;
514             return $self->{label_width} //= textwidth( $self->{label} );
515             }
516              
517             sub label { shift->{label} }
518              
519             =head2 set_label
520              
521             Set new label text for the tab
522              
523             =cut
524              
525             sub set_label {
526             my $self = shift;
527             ( $self->{label} ) = @_;
528             undef $self->{label_width};
529             $self->{tabbed}->_tabs_changed if $self->{tabbed};
530             }
531              
532             =head2 is_active
533              
534             Returns true if this tab is the currently active one
535              
536             =cut
537              
538             sub is_active {
539             my $self = shift;
540             return $self->{tabbed}->active_tab == $self;
541             }
542              
543             =head2 activate
544              
545             Activate this tab
546              
547             =cut
548              
549             sub activate {
550             my $self = shift;
551             $self->{tabbed}->activate_tab( $self );
552             }
553              
554             sub _activate {
555             my $self = shift;
556             $self->widget->window->show if $self->widget->window;
557             $self->${\$self->{on_activated}}() if $self->{on_activated};
558             }
559              
560             sub _deactivate {
561             my $self = shift;
562             $self->${\$self->{on_deactivated}}() if $self->{on_deactivated};
563             $self->widget->window->hide if $self->widget->window;
564             }
565              
566             =head2 set_on_activated
567              
568             Set a callback or method name to invoke when the tab is activated
569              
570             =cut
571              
572             sub set_on_activated
573             {
574             my $self = shift;
575             ( $self->{on_activated} ) = @_;
576             }
577              
578             =head2 set_on_deactivated
579              
580             Set a callback or method name to invoke when the tab is deactivated
581              
582             =cut
583              
584             sub set_on_deactivated
585             {
586             my $self = shift;
587             ( $self->{on_deactivated} ) = @_;
588             }
589              
590             =head2 pen
591              
592             Returns the C used to draw the label.
593              
594             Pen observers are no longer registered on the return value; to set a different
595             pen on the tab, use the C method instead.
596              
597             =cut
598              
599             sub _has_pen { defined shift->{pen} }
600              
601             sub pen {
602             my $self = shift;
603             return $self->{pen};
604             }
605              
606             sub set_pen {
607             my $self = shift;
608             ( $self->{pen} ) = @_;
609             $self->{tabbed}->_tabs_changed if $self->{tabbed};
610             }
611              
612             sub on_mouse {
613             my $self = shift;
614             my ( $type, $button, $line, $col ) = @_;
615              
616             return 0 unless $type eq "press" && $button == 1;
617             $self->{tabbed}->activate_tab( $self );
618             return 1;
619             }
620              
621             1;
622              
623             __END__