File Coverage

blib/lib/Term/Graille/Menu.pm
Criterion Covered Total %
statement 15 139 10.7
branch 0 58 0.0
condition 0 8 0.0
subroutine 5 28 17.8
pod 11 17 64.7
total 31 250 12.4


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