File Coverage

blib/lib/Curses/UI/Searchable.pm
Criterion Covered Total %
statement 27 148 18.2
branch 0 30 0.0
condition 0 9 0.0
subroutine 9 22 40.9
pod 1 13 7.6
total 37 222 16.6


line stmt bran cond sub pod time code
1             # ----------------------------------------------------------------------
2             # Curses::UI::Searchable
3             # Curses::UI::SearchEntry
4             #
5             # (c) 2001-2002 by Maurice Makaay. All rights reserved.
6             # This file is part of Curses::UI. Curses::UI is free software.
7             # You can redistribute it and/or modify it under the same terms
8             # as perl itself.
9             #
10             # Currently maintained by Marcus Thiesen
11             # e-mail: marcus@cpan.thiesenweb.de
12             # ----------------------------------------------------------------------
13              
14             # TODO: fix dox
15              
16             # ----------------------------------------------------------------------
17             # SearchEntry package
18             # ----------------------------------------------------------------------
19              
20             package Curses::UI::SearchEntry;
21              
22 2     2   11 use Curses;
  2         4  
  2         6220  
23 2     2   18 use Curses::UI::Widget; # For height_by_windowscrheight()
  2         4  
  2         156  
24 2     2   37 use Curses::UI::Common;
  2         3  
  2         204  
25 2     2   11 use Curses::UI::Container;
  2         5  
  2         95  
26              
27 2         1325 use vars qw(
28             $VERSION
29             @ISA
30 2     2   11 );
  2         7  
31              
32             $VERSION = "1.10";
33              
34             @ISA = qw(
35             Curses::UI::ContainerWidget
36             );
37              
38             sub new()
39             {
40 0     0 1   my $class = shift;
41              
42 0           my %userargs = @_;
43 0           keys_to_lowercase(\%userargs);
44              
45 0           my %args = (
46             -prompt => '/', # The initial search prompt
47              
48             %userargs,
49              
50             -x => 0,
51             -y => -1,
52             -width => undef,
53             -border => 0,
54             -sbborder => 0,
55             -showlines => 0,
56             -focus => 0,
57             );
58            
59             # The windowscr height should be 1.
60 0           $args{-height} = height_by_windowscrheight(1,%args);
61              
62 0           my $this = $class->SUPER::new(%args);
63              
64 0           my $entry = $this->add(
65             'entry', 'TextEntry',
66             -x => 1,
67             -y => 0,
68             -height => 1,
69             -border => 0,
70             -sbborder => 0,
71             -showlines => 0,
72             -width => undef,
73             -intellidraw => 0,
74             );
75            
76 0           $this->add(
77             'prompt', 'Label',
78             -x => 0,
79             -y => 0,
80             -height => 1,
81             -width => 2,
82             -border => 0,
83             -text => $this->{-prompt},
84             -intellidraw => 0,
85             );
86              
87 0           $entry->set_routine('loose-focus', \&entry_loose_focus);
88              
89 0           $this->layout;
90              
91 0           return $this;
92             }
93              
94             sub entry_loose_focus()
95             {
96 0     0 0   my $this = shift;
97 0           $this->parent->loose_focus;
98             }
99              
100             sub event_keypress($;)
101             {
102 0     0 0   my $this = shift;
103 0           my $key = shift;
104              
105 0           my $entry = $this->getobj('entry');
106 0 0         if ($entry->{-focus}) {
107 0           $this->getobj('entry')->event_keypress($key);
108             } else {
109 0           $this->{-key} = $key;
110             }
111              
112 0           return $this;
113             }
114              
115             sub get()
116             {
117 0     0 0   my $this = shift;
118 0           $this->getobj('entry')->get;
119             }
120              
121             sub pos(;$)
122             {
123 0     0 0   my $this = shift;
124 0           my $pos = shift;
125 0           $this->getobj('entry')->pos($pos);
126             }
127              
128             sub text(;$)
129             {
130 0     0 0   my $this = shift;
131 0           my $text = shift;
132 0           $this->getobj('entry')->text($text);
133             }
134              
135             sub prompt(;$)
136             {
137 0     0 0   my $this = shift;
138 0           my $prompt = shift;
139 0 0         if (defined $prompt)
140             {
141 0           $prompt = substr($prompt, 0, 1);
142 0           $this->{-prompt} = $prompt;
143 0           $this->getobj('prompt')->text($prompt);
144 0           $this->intellidraw;
145 0           return $this;
146             } else {
147 0           return $this->{-prompt};
148             }
149             }
150              
151             # Let Curses::UI->usemodule() believe that this module
152             # was already loaded (usemodule() would else try to
153             # require the non-existing file).
154             #
155             $INC{'Curses/UI/SearchEntry.pm'} = $INC{'Curses/UI/Searchable.pm'};
156              
157              
158             # ----------------------------------------------------------------------
159             # Searchable package
160             # ----------------------------------------------------------------------
161              
162             package Curses::UI::Searchable;
163              
164 2     2   14 use strict;
  2         6  
  2         345  
165 2     2   12 use Curses;
  2         9  
  2         5777  
166 2     2   18 use Curses::UI::Common;
  2         4  
  2         254  
167             require Exporter;
168              
169 2         1970 use vars qw(
170             $VERSION
171             @ISA
172             @EXPORT
173 2     2   13 );
  2         4  
174              
175             $VERSION = '1.10';
176              
177             @ISA = qw(
178             Exporter
179             );
180              
181             @EXPORT = qw(
182             search_forward
183             search_backward
184             search
185             search_next
186             );
187              
188             sub search_forward()
189             {
190 0     0 0   my $this = shift;
191 0           $this->search("/", +1);
192             }
193              
194             sub search_backward()
195             {
196 0     0 0   my $this = shift;
197 0           $this->search("?", -1);
198             }
199              
200             sub search()
201             {
202 0     0 0   my $this = shift;
203 0   0       my $prompt = shift || ':';
204 0   0       my $direction = shift || +1;
205              
206 0           $this->change_canvasheight(-1);
207 0           $this->draw;
208              
209 0           my $querybox = new Curses::UI::SearchEntry(
210             -parent => $this,
211             -prompt => $prompt,
212             );
213              
214 0           my $old_cursor_mode = $this->root->cursor_mode;
215 0           $this->root->cursor_mode(1);
216 0           $querybox->getobj('entry')->{-focus} = 1;
217 0           $querybox->draw;
218 0           $querybox->modalfocus();
219 0           $querybox->getobj('entry')->{-focus} = 0;
220              
221 0           my $query = $querybox->get;
222 0           $querybox->prompt(':');
223 0           $querybox->draw;
224            
225 0           my $key;
226 0 0         if ($query ne '')
227             {
228 0           my ($newidx, $wrapped) =
229             $this->search_next($query, $direction);
230              
231 0           KEY: for (;;)
232             {
233 0 0         unless (defined $newidx) {
234 0           $querybox->text('Not found');
235             } else {
236 0 0         $querybox->text($wrapped ? 'Wrapped' : '');
237             }
238 0           $querybox->pos(0);
239 0           $querybox->draw;
240              
241 0           $querybox->{-key} = '-1';
242 0           while ($querybox->{-key} eq '-1') {
243 0           $this->root->do_one_event($querybox);
244             }
245              
246 0 0         if ($querybox->{-key} eq 'n') {
    0          
247 0           ($newidx, $wrapped) =
248             $this->search_next($query, $direction);
249             } elsif ($querybox->{-key} eq 'N') {
250 0           ($newidx, $wrapped) =
251             $this->search_next($query, -$direction);
252             } else {
253 0           last KEY;
254             }
255             }
256             }
257              
258             # Restore the screen.
259 0           $this->root->cursor_mode($old_cursor_mode);
260 0           $this->change_canvasheight(+1);
261 0           $this->draw;
262              
263 0           $this->root->feedkey($querybox->{-key});
264 0           return $this;
265             }
266              
267             sub search_next($$;)
268             {
269 0     0 0   my $this = shift;
270 0           my $query = shift;
271 0           my $direction = shift;
272 0 0         $direction = ($direction > 0 ? +1 : -1);
273 0           $this->search_get($query, $direction);
274             }
275              
276             sub change_canvasheight($;)
277             {
278 0     0 0   my $this = shift;
279 0           my $change = shift;
280              
281 0 0         if ($change < 0)
    0          
282             {
283             # Change the canvasheight, so we can fit in the searchline.
284 0           $this->{-sh}--;
285 0 0         $this->{-yscrpos}++
286             if ($this->{-ypos}-$this->{-yscrpos} == $this->canvasheight);
287             }
288             elsif ($change > 0)
289             {
290             # Restore the canvasheight.
291 0           $this->{-sh}++;
292 0           my $inscreen = ($this->canvasheight
293             - ($this->number_of_lines
294             - $this->{-yscrpos}));
295 0   0       while ($this->{-yscrpos} > 0 and
296             $inscreen < $this->canvasheight)
297             {
298 0           $this->{-yscrpos}--;
299 0           $inscreen = ($this->canvasheight
300             - ($this->number_of_lines
301             - $this->{-yscrpos}));
302             }
303             }
304              
305 0           $this->{-search_highlight} = undef;
306 0           $this->layout_content();
307             }
308              
309             sub search_get($$;)
310             {
311 0     0 0   my $this = shift;
312 0           my $query = shift;
313 0   0       my $direction = shift || +1;
314              
315 0           my $startpos = $this->{-ypos};
316 0           my $offset = 0;
317 0           my $wrapped = 0;
318 0           for (;;)
319             {
320             # Find the line position to match.
321 0           $offset += $direction;
322 0           my $newpos = $this->{-ypos} + $offset;
323              
324 0           my $last_idx = $this->number_of_lines - 1;
325              
326             # Beyond limits?
327 0 0         if ($newpos < 0)
328             {
329 0           $newpos = $last_idx;
330 0           $offset = $newpos - $this->{-ypos};
331 0           $wrapped = 1;
332             }
333            
334 0 0         if ($newpos > $last_idx)
335             {
336 0           $newpos = 0;
337 0           $offset = $newpos - $this->{-ypos};
338 0           $wrapped = 1;
339             }
340              
341             # Nothing found?
342 0 0         return (undef,undef) if $newpos == $startpos;
343              
344 0 0         if ($this->getline_at_ypos($newpos) =~ /\Q$query/i)
345             {
346 0           $this->{-ypos} = $newpos;
347 0           $this->{-search_highlight} = $newpos;
348 0           $startpos = $newpos;
349 0           $this->layout_content;
350 0           $this->draw(1);
351 0           return $newpos, $wrapped;
352 0           $wrapped = 0;
353             }
354             }
355             }
356              
357              
358              
359             1;
360              
361              
362             =pod
363              
364             =head1 NAME
365              
366             Curses::UI::Searchable - Add 'less'-like search abilities to a widget
367              
368             =head1 CLASS HIERARCHY
369              
370             Curses::UI::Searchable - base class
371              
372              
373             =head1 SYNOPSIS
374              
375             package MyWidget;
376              
377             use Curses::UI::Searchable;
378             use vars qw(@ISA);
379             @ISA = qw(Curses::UI::Searchable);
380              
381             ....
382              
383             sub new () {
384             # Create class instance $this.
385             ....
386              
387             $this->set_routine('search-forward', \&search_forward);
388             $this->set_binding('search-forward', '/');
389             $this->set_routine('search-backward', \&search_backward);
390             $this->set_binding('search-backward', '?');
391             }
392              
393             sub layout_content() {
394             my $this = shift;
395              
396             # Layout your widget's content.
397             ....
398              
399             return $this;
400             }
401              
402             sub number_of_lines() {
403             my $this = shift;
404              
405             # Return the number of lines in
406             # the widget's content.
407             return ....
408             }
409              
410             sub getline_at_ypos($;) {
411             my $this = shift;
412             my $ypos = shift;
413              
414             # Return the content on the line
415             # where ypos = $ypos
416             return ....
417             }
418              
419              
420             =head1 DESCRIPTION
421              
422             Using Curses::UI::Searchable, you can add 'less'-like
423             search capabilities to your widget.
424              
425             To make your widget searchable using this class,
426             your widget should meet the following requirements:
427              
428             =over 4
429              
430             =item * B
431              
432             All methods for searching are in Curses::UI::Searchable.
433             By making your class a descendant of this class, these
434             methods are automatically inherited.
435              
436             =item * B<-ypos data member>
437              
438             The current vertical position in the widget should be
439             identified by $this->{-ypos}. This y-position is the
440             index of the line of content. Here's an example for
441             a Listbox widget.
442              
443             -ypos
444             |
445             v
446             +------+
447             0 |One |
448             1 |Two |
449             2 |Three |
450             +------+
451              
452             =item * B
453              
454             Your widget class should have a method B,
455             which returns the total number of lines in the widget's
456             content. So in the example above, this method would
457             return the value 3.
458              
459             =item * B
460              
461             Your widget class should have a method B,
462             which returns the line of content at -ypos YPOS.
463             So in the example above, this method would return
464             the value "Two" for YPOS = 1.
465              
466             =item * B
467              
468             The search routines will set the -ypos of your widget if a
469             match is found for the given search string. Your B
470             routine should make sure that the line of content at -ypos
471             will be made visible if the B method is called.
472              
473             =item * B
474              
475             If the search routines find a match, $this->{-search_highlight}
476             will be set to the -ypos for the line on which the match
477             was found. If no match was found $this->{-search_highlight}
478             will be undefined. If you want a matching line to be highlighted,
479             in your widget, you can use this data member to do so
480             (an example of a widget that uses this option is the
481             L widget).
482              
483             =item * B
484              
485             There are two search routines. These are B and
486             B. These have to be called in order to
487             display the search prompt. The best way to do this is by
488             creating bindings for them. Here's an example which will
489             make '/' a forward search and '?' a backward search:
490              
491             $this->set_routine('search-forward' , \&search_forward);
492             $this->set_binding('search-forward' , '/');
493             $this->set_routine('search-backward' , \&search_backward);
494             $this->set_binding('search-backward' , '?');
495              
496             =back
497              
498              
499              
500             =head1 SEE ALSO
501              
502             L,
503              
504              
505              
506              
507             =head1 AUTHOR
508              
509             Copyright (c) 2001-2002 Maurice Makaay. All rights reserved.
510              
511             Maintained by Marcus Thiesen (marcus@cpan.thiesenweb.de)
512              
513              
514             This package is free software and is provided "as is" without express
515             or implied warranty. It may be used, redistributed and/or modified
516             under the same terms as perl itself.
517