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