File Coverage

blib/lib/Term/Graille/Menu.pm
Criterion Covered Total %
statement 15 129 11.6
branch 0 60 0.0
condition 0 8 0.0
subroutine 5 22 22.7
pod 11 17 64.7
total 31 236 13.1


line stmt bran cond sub pod time code
1             =head1 NAME
2             Term::Graille::Menu
3              
4              
5             =head1 SYNOPSIS
6            
7             my $menu=new Term::Graille::Menu(
8             menu=>[["File","New","Save","Load","Quit"],
9             ["Edit","Clear",["Reformat","2x4","4x4"],["Scroll","left","right","up","down"]],
10             "About"],
11             redraw=>\&main::refreshScreen,
12             callback=>\&main::menuActions,
13             );
14              
15              
16             =head1 DESCRIPTION
17              
18             Developed to allow user interaction using a hierarchical menu in command line
19             applications. The menu is activated using a key press, and navigated
20             typically using arrow keys. It does not handle or capture the key presses
21             directly, and in Graille is used in conjunctyion with Term::Graille::IO
22              
23              
24             =begin html
25              
26            
27              
28             =end html
29              
30              
31             =head1 FUNCTIONS
32              
33             =cut
34              
35             package Term::Graille::Menu;
36              
37             our $VERSION=0.09;
38              
39 1     1   781 use strict;use warnings;
  1     1   2  
  1         24  
  1         4  
  1         1  
  1         41  
40 1     1   526 use Storable qw(dclone);
  1         2665  
  1         54  
41 1     1   6 use Term::Graille qw/colour printAt clearScreen/;
  1         2  
  1         36  
42 1     1   5 use utf8;
  1         1  
  1         4  
43              
44              
45             =head3 Cnew(%params)>
46              
47             Creates a new $menu; params are
48             C The menu tree as an Array ref containing strings and arrayrefs.
49             Branches are Array refs, and end nodes are strings. See above example to
50             visuialise structure.
51             C This is a function to redraws the application screen.
52             The menu will overwrite parts of the application screen, and this allows
53             function needs to be provided to restire the screen.
54             C The menu does not call any functions, instead returns the
55             leaf string selected. It is upto the main application to use this string to
56             in a dispatch routine (the callback function supplied)
57              
58             =cut
59              
60              
61             sub new{
62 0     0 1   my ($class,%params) = @_;
63 0           my $self={};
64 0           bless $self,$class;
65 0   0       $self->{menu}=$params{menu}//[];
66 0 0         $self->{redraw}=$params{redraw} if (exists $params{redraw}); # function to redraw application
67 0 0         $self->{callback}=$params{callback} if (exists $params{callback}); # function to call after menu item selected
68 0           $self->{breadCrumbs}=[0];
69 0   0       $self->{pos}=$params{pos}//[2,2];
70 0   0       $self->{highlightColour}=$params{highlightColour}//"black on_white";
71 0   0       $self->{normalColour}=$params{normalColour}//"white on_black";
72 0           return $self;
73             }
74              
75              
76             =head3 C<$menu-EsetMenu($menu,$reset)>
77              
78             Changes the menu. if reset is set then the menu "pointer" is set at the first item
79             in menmu tree.
80              
81             =cut
82              
83             sub setMenu{
84 0     0 1   my ($self,$menu,$reset)=@_;
85 0           $self->{menu}=$menu;
86 0 0         $self->{breadCrumbs} if $reset;
87             }
88              
89              
90             =head3 Credraw()>
91              
92             Calls the applications redraw function. This is required for the menu
93             to be overwritten with application screen.
94              
95             =cut
96              
97              
98             sub redraw{
99 0     0 1   my $self=shift;
100 0 0         $self->{redraw}->() if (exists $self->{redraw});
101            
102             }
103              
104              
105              
106             =head3 CnextItem()>, CprevItem()>,
107             CcloseItem()>, CopenItem()>
108              
109             Navigate the menu, select items.
110              
111             =cut
112            
113             sub nextItem{
114 0     0 1   my $self=shift;
115 0           $self->{breadCrumbs}->[-1]++ ;
116 0 0         $self->{breadCrumbs}->[-1]-- if ($self->drillDown() == 0);
117 0           $self->drawMenu();
118             }
119              
120             sub prevItem{
121 0     0 1   my $self=shift;
122 0 0         $self->{breadCrumbs}->[-1]-- unless $self->{breadCrumbs}->[-1]==0;
123 0           $self->drawMenu();
124             }
125              
126             sub closeItem{
127 0     0 1   my $self=shift;
128 0 0         if ($self->depth()>1){
129 0           pop @{$self->{breadCrumbs}};
  0            
130 0           $self->drawMenu();
131             }
132             else{ # if at top level close menu;
133 0           $self->closeMenu();
134             }
135             }
136              
137              
138             sub openItem{# enter submemnu if one exists, or "open" the item;
139 0     0 1   my $self=shift;
140 0           my ($label,$submenu)=@{$self->drillDown()};
  0            
141 0 0         if ($submenu) {
142 0           $self->{breadCrumbs}=[@{$self->{breadCrumbs}},0];
  0            
143 0           $self->drawMenu();
144             }
145             else{
146 0 0         $self->{callback}->($label) if $self->{callback};
147             }
148             }
149              
150             =head3 CcloseMenu()>
151              
152             Closes the menu, redraws the screen (using C<$menu->redraw();>) and sends
153             C to C<$menu->{callback}>;
154              
155             =cut
156              
157              
158             sub closeMenu{
159 0     0 1   my $self=shift;
160 0 0         $self->redraw() if defined $self->redraw() ;
161 0 0         $self->{callback}->(undef) if $self->{callback};
162             }
163              
164              
165             =head3 CupArrow()>, CdownArrow()>,
166             CleftArraow()>, CrightArrow()>
167              
168             The menu does not acpture key presses, but provides methods to allow
169             key presses to trigger navigation.
170              
171              
172             =cut
173              
174             sub upArrow{
175 0     0 1   my $self=shift;
176 0 0         if ($self->depth()==1){
177 0           $self->closeItem();
178             }
179             else{
180 0           $self->prevItem
181             }
182             }
183              
184             sub downArrow{
185 0     0 1   my $self=shift;
186 0 0         if ($self->depth()==1){
187 0           $self->openItem();
188             }
189             else{
190 0           $self->nextItem();
191             }
192             }
193              
194             sub leftArrow{
195 0     0 0   my $self=shift;
196 0 0         if ($self->depth()==1){
197 0           $self->nextItem();
198             }
199             else{
200 0           $self->openItem();
201             }
202             }
203              
204             sub rightArrow{
205 0     0 1   my $self=shift;
206 0 0         if ($self->depth()==1){
207 0           $self->prevItem();
208             }
209             else{
210 0           $self->closeItem();
211 0           $self->{redraw}->();
212 0           $self->drawMenu();
213             }
214             }
215              
216              
217             sub drillDown{ # return curent item, and whether it has children;
218 0     0 0   my $self=shift;
219 0           my $tmp=dclone($self->{menu});
220 0           foreach my $level (0..$#{$self->{breadCrumbs}}){
  0            
221 0 0         return 0 unless $tmp->[$self->{breadCrumbs}->[$level]];
222 0 0         shift @{$tmp} unless ($level==0);
  0            
223 0           $tmp=$tmp->[$self->{breadCrumbs}->[$level]];
224             }
225 0 0         return ref $tmp?[$tmp->[0],1]:[$tmp,0];
226             }
227              
228             sub drawMenu{
229 0     0 0   my $self=shift;
230 0           my $pos=[@{$self->{pos}}]; # get a copy of contents of $self->{pos}
  0            
231 0           foreach my $level (0..$#{$self->{breadCrumbs}}){
  0            
232 0           $pos = $self->drawLevel($level,$self->{breadCrumbs}->[$level],$pos)
233             }
234             }
235              
236             sub drawLevel{
237 0     0 0   my ($self,$level,$ai,$pos)=@_;
238 0           my $nextPos=$pos;
239 0           my $tmp=dclone($self->{menu});
240 0 0         if (!$level){
241 0           foreach my $mi (0..$#$tmp){
242 0 0         my $label=((ref $tmp->[$mi])?$tmp->[$mi]->[0]:$tmp->[$mi]);
243 0 0         my $active=($ai == $mi?1:0);
244 0 0         if ($active){$nextPos=[$nextPos->[0]+1,$pos->[1]]}
  0            
245 0           printAt(@$pos,$self->highlight($label,$active). " ");
246 0           $pos->[1]+=(2+length $label);
247             }
248 0           print "\n";
249             }
250             else{
251 0           my $l=0;
252 0           while ($l<$level){ # walk down the tree until level to be printed
253 0           $tmp=$tmp->[$self->{breadCrumbs}->[$l]];
254 0           shift @{$tmp} ;
  0            
255 0           $l++
256             }
257 0           my $longest=-1;
258 0           foreach(@$tmp){
259 0 0         my $il=length(ref $_?$_->[0]:$_);
260 0 0         $longest=$il if ($longest<$il);
261             };
262 0           printAt(@$pos,"┌". ("─"x$longest)."┐");
263 0           $pos->[0]+=1;
264 0           foreach my $mi (0..$#{$tmp}){ # skip first item which is label for list
  0            
265 0 0         my $label=((ref $tmp->[$mi])?$tmp->[$mi]->[0]:$tmp->[$mi]);
266 0 0         my $active=(($ai) == $mi?1:0);
267 0 0         if ($active){$nextPos=[$pos->[0],$pos->[1]+$longest+2]}
  0            
268 0           printAt(@$pos,$self->highlight($label,$active,$longest));
269 0           $pos->[0]+=1;
270             }
271 0           printAt(@$pos,"└". ("─"x$longest)."┘");
272             }
273 0           return $nextPos;
274             }
275              
276             sub depth{
277 0     0 0   my $self=shift;
278 0           return scalar @{$self->{breadCrumbs}};
  0            
279             }
280              
281             sub highlight{
282 0     0 0   my ($self,$str,$hl,$padding)=@_;
283 0 0         my $space=$padding?(" "x($padding-length $str)):" ";
284 0 0         my $b=$padding?"│":"";
285 0 0         return $b.colour($hl?$self->{highlightColour}:$self->{normalColour}).$str.$space.colour("reset").$b;;
286             }
287              
288             1;