File Coverage

blib/lib/Tickit/Widget/Choice.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


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