File Coverage

blib/lib/Tickit/Widget/Choice.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 either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2014 -- leonerd@leonerd.org.uk
5              
6             package Tickit::Widget::Choice;
7              
8 1     1   801 use strict;
  1         1  
  1         29  
9 1     1   5 use warnings;
  1         2  
  1         38  
10 1     1   12 use base qw( Tickit::Widget );
  1         2  
  1         964  
11             use Tickit::Style;
12              
13             our $VERSION = '0.02';
14              
15             use Carp;
16              
17             use Tickit::RenderBuffer qw( LINE_SINGLE LINE_DOUBLE CAP_START CAP_END );
18             use Tickit::Utils qw( textwidth );
19              
20             use Tickit::Widget::Menu 0.09; # ->highlight_item
21             use Tickit::Widget::Menu::Item;
22              
23             use List::Util qw( max );
24              
25             use constant WIDGET_PEN_FROM_STYLE => 1;
26             use constant CAN_FOCUS => 1;
27              
28             =head1 NAME
29              
30             C - a widget giving a choice from a list
31              
32             =cut
33              
34             style_definition base =>
35             border_fg => "hi-white",
36             border_linestyle => LINE_SINGLE,
37             '' => "first_choice",
38             '' => "prev_choice",
39             '' => "next_choice",
40             '' => "last_choice",
41             '' => "popup";
42              
43             style_definition ':focus' =>
44             border_linestyle => LINE_DOUBLE;
45              
46             style_redraw_keys qw( border_linestyle );
47              
48             use constant KEYPRESSES_FROM_STYLE => 1;
49              
50             =head1 CONSTRUCTOR
51              
52             =cut
53              
54             =head2 new
55              
56             $choice = Tickit::Widget::Choice->new( %args )
57              
58             Constructs a new C object.
59              
60             Takes the following named arguments
61              
62             =over 8
63              
64             =item choices => ARRAY
65              
66             Optional. If supplied, should be an ARRAY reference containing two-element
67             ARRAY references. Each will be added to the list of choices as if by a call to
68             C for each element in the array.
69              
70             =item on_changed => CODE
71              
72             Optional. If supplied, used to set the initial value of the C
73             event handler.
74              
75             =back
76              
77             =cut
78              
79             sub new
80             {
81             my $class = shift;
82             my %params = @_;
83              
84             my $self = $class->SUPER::new( %params );
85              
86             $self->{choices} = [];
87              
88             $self->push_choice( @$_ ) for @{ $params{choices} || [] };
89              
90             $self->set_on_changed( $params{on_changed} ) if $params{on_changed};
91              
92             return $self;
93             }
94              
95             sub lines { 1 }
96              
97             sub cols
98             {
99             my $self = shift;
100             return 4 + max( 1, map { textwidth $_->[1] } @{ $self->{choices} } );
101             }
102              
103             sub window_gained
104             {
105             my $self = shift;
106             my ( $window ) = @_;
107             $self->SUPER::window_gained( $window );
108              
109             $window->cursor_at( 0, 1 );
110             }
111              
112             =head1 ACCESSORS
113              
114             =cut
115              
116             =head2 on_changed
117              
118             $on_changed = $self->on_changed
119              
120             =cut
121              
122             sub on_changed
123             {
124             my $self = shift;
125             return $self->{on_changed};
126             }
127              
128             =head2 set_on_changed
129              
130             $self->set_on_changed( $on_changed )
131              
132             Return or set the CODE reference to be called when the chosen selection is
133             changed.
134              
135             $on_changed->( $choice, $value )
136              
137             =cut
138              
139             sub set_on_changed
140             {
141             my $self = shift;
142             ( $self->{on_changed} ) = @_;
143             }
144              
145             =head1 METHODS
146              
147             =cut
148              
149             =head2 push_choice
150              
151             $choice->push_choice( $value, $caption )
152              
153             Appends another choice to the list of choices, with the given value and
154             display caption.
155              
156             =cut
157              
158             sub push_choice
159             {
160             my $self = shift;
161             my ( $value, $caption ) = @_;
162              
163             push @{ $self->{choices} }, [ $value, $caption ];
164             $self->{chosen} = 0 if !defined $self->{chosen};
165              
166             $self->resized;
167             $self->redraw;
168              
169             return $self;
170             }
171              
172             =head2 chosen_value
173              
174             $value = $choice->chosen_value
175              
176             Returns the value of the currently-chosen choice.
177              
178             =cut
179              
180             sub chosen_value
181             {
182             my $self = shift;
183             return $self->{choices}[ $self->{chosen} ]->[0];
184             }
185              
186             =head2 choose_by_idx
187              
188             $choice->choose_by_idx( $idx )
189              
190             Moves the chosen choice to the one at the given index. If this wasn't the
191             previously-chosen one, invokes the C event.
192              
193             =cut
194              
195             sub choose_by_idx
196             {
197             my $self = shift;
198             my ( $idx ) = @_;
199              
200             return if $self->{chosen} == $idx;
201              
202             $self->{chosen} = $idx;
203             $self->redraw;
204              
205             $self->{on_changed}->( $self, $self->chosen_value ) if $self->{on_changed};
206             }
207              
208             =head2 choose_by_value
209              
210             $choice->choose_by_value( $value )
211              
212             Moves the chosen choise to the one having the given value, if such a choice
213             exists. If this wasn't the previously-chosen one, invokes the C
214             event.
215              
216             =cut
217              
218             sub choose_by_value
219             {
220             my $self = shift;
221             my ( $value ) = @_;
222              
223             my $choices = $self->{choices};
224             $choices->[$_][0] eq $value and return $self->choose_by_idx( $_ )
225             for 0 .. $#$choices;
226              
227             croak "No such choice with value '$value'";
228             }
229              
230             =head2 popup_menu
231              
232             $choice->popup_menu
233              
234             Display the popup menu in a modal float until a choice is made.
235              
236             =cut
237              
238             sub popup_menu
239             {
240             my $self = shift;
241              
242             my $menu = $self->{menu} = Tickit::Widget::Menu->new(
243             items => [ map {
244             my ( $value, $caption ) = @$_;
245             Tickit::Widget::Menu::Item->new(
246             name => $caption,
247             on_activate => sub {
248             undef $self->{menu};
249             $self->choose_by_value( $value );
250             },
251             )
252             } @{ $self->{choices} } ],
253             );
254              
255             my $top = -1;
256             $top = 0 if $self->window->abs_top == 0;
257              
258             $menu->popup( $self->window, $top, 0 );
259              
260             $menu->highlight_item( $self->{chosen} );
261             }
262              
263             sub render_to_rb
264             {
265             my $self = shift;
266             my ( $rb, $rect ) = @_;
267              
268             my $border_pen = $self->get_style_pen( 'border' );
269             my $linestyle = $self->get_style_values( 'border_linestyle' );
270              
271             my $chosen = $self->{choices}[ $self->{chosen} ];
272              
273             my $right = $self->window->cols - 3;
274              
275             $rb->vline_at( 0, 0, 0, $linestyle, $border_pen, CAP_START|CAP_END );
276              
277             $rb->goto( 0, 1 );
278             $rb->text( substr( $chosen->[1], 0, $right - 1 ) );
279             $rb->erase_to( $right );
280              
281             $rb->vline_at( 0, 0, $right, $linestyle, $border_pen, CAP_START|CAP_END );
282             $rb->text_at( 0, $right+1, "-", $border_pen );
283             $rb->vline_at( 0, 0, $right+2, $linestyle, $border_pen, CAP_START|CAP_END );
284             }
285              
286             sub key_first_choice { my $self = shift; $self->choose_by_idx( 0 ); 1 }
287             sub key_last_choice { my $self = shift; $self->choose_by_idx( $#{ $self->{choices} } ); 1 }
288              
289             sub key_next_choice { my $self = shift; $self->choose_by_idx( $self->{chosen}+1 ) if $self->{chosen} < $#{ $self->{choices} }; 1 }
290             sub key_prev_choice { my $self = shift; $self->choose_by_idx( $self->{chosen}-1 ) if $self->{chosen} > 0; 1 }
291              
292             sub key_popup { my $self = shift; $self->popup_menu; 1 }
293              
294             sub on_mouse
295             {
296             my $self = shift;
297             my ( $ev ) = @_;
298              
299             return unless $ev->type eq "press" and $ev->button == 1;
300              
301             my $win = $self->window;
302             my $right = $win->cols - 3;
303              
304             my $col = $ev->col;
305             if( $ev->line == 0 and ( $col > 1 && $col < $right or $col == $right + 1 ) ) {
306             $self->popup_menu;
307             }
308              
309             return 1;
310             }
311              
312             =head1 TODO
313              
314             =over 4
315              
316             =item *
317              
318             Render a full border around the widget if height is at least 3.
319              
320             =back
321              
322             =cut
323              
324             =head1 AUTHOR
325              
326             Paul Evans
327              
328             =cut
329              
330             0x55AA;