File Coverage

lib/Curses/UI.pm
Criterion Covered Total %
statement 167 443 37.7
branch 47 174 27.0
condition 6 46 13.0
subroutine 27 63 42.8
pod 28 52 53.8
total 275 778 35.3


line stmt bran cond sub pod time code
1             package Curses::UI;
2 11     11   360180 use base qw(Curses::UI::Common Curses::UI::Container);
  11         28  
  11         8352  
3              
4             # If we do not know a terminal type, then imply VT100.
5 8 50   8   243 BEGIN { $ENV{TERM} = 'vt100' unless defined $ENV{TERM} }
6              
7 8     8   66 use strict;
  8         13  
  8         267  
8 8     8   40 use warnings;
  8         17  
  8         496  
9              
10 8     8   40 use Curses;
  8         45  
  8         23398  
11 8     8   13771 use Curses::UI::Language;
  8         25  
  8         263  
12 8     8   4975 use Curses::UI::Color;
  8         23  
  8         489  
13 8     8   7671 use FileHandle;
  8         115692  
  8         63  
14 8     8   3589 use Term::ReadKey;
  8         17  
  8         725  
15              
16             =head1 NAME
17              
18             Curses::UI - A curses based OO user interface framework
19              
20             =head1 VERSION
21              
22             Version 0.9609
23              
24             =cut
25              
26 8     8   46 use vars qw( $VERSION );
  8         19  
  8         45315  
27             $VERSION = 0.9609;
28              
29             =head1 SYNOPSIS
30              
31             use Curses::UI;
32              
33             # create a new C::UI object
34             my $cui = Curses::UI->new( -clear_on_exit => 1,
35             -debug => $debug, );
36              
37             # this is where we gloss over setting up all the widgets and data
38             # structures :)
39              
40             # start the event loop
41             $cui->mainloop;
42              
43              
44             =head1 DESCRIPTION
45              
46             Curses::UI is an object-oriented user interface framework for Perl.
47              
48             It contains basic widgets (like buttons and text areas), more
49             "advanced" widgets (like UI tabs and a fully-functional basic text
50             editor), and some higher-level classes like pre-fab error dialogues.
51              
52             See L and the C directory of the
53             source distribution for more introductory material.
54              
55             =cut
56              
57             $Curses::UI::debug = 0;
58             $Curses::UI::screen_too_small = 0;
59             $Curses::UI::initialized = 0;
60             $Curses::UI::color_support = 0;
61             $Curses::UI::color_object = 0;
62             $Curses::UI::ncurses_mouse = 0;
63             $Curses::UI::gpm_mouse = 0;
64              
65             # Detect if we should use the new moushandler
66             if ($ENV{"TERM"} ne "xterm") {
67             eval { require Curses::UI::Mousehandler::GPM;
68             import Curses::UI::Mousehandler::GPM; };
69             if (!$@) {
70             $Curses::UI::gpm_mouse = gpm_enable();
71             print STDERR "DEBUG: gpm_mouse: " . $Curses::UI::gpm_mouse . "\n"
72             if $Curses::UI::debug;
73             }
74             } else {
75             # Detect ncurses functionality. Magic for Solaris 8
76             eval { $Curses::UI::ncurses_mouse = (Curses->can('NCURSES_MOUSE_VERSION')
77             &&
78             (NCURSES_MOUSE_VERSION() >= 1 ) ) };
79             print STDERR "DEBUG: Detected mouse support $Curses::UI::ncurses_mouse\n"
80             if $Curses::UI::debug;
81             }
82              
83              
84              
85             =head1 CONSTRUCTOR
86              
87             Create a new Curses::UI object:
88              
89             my $cui = Curses::UI->new( OPTIONS );
90              
91             where C is one or more of the following.
92              
93             =head2 -clear_on_exit
94              
95             If true, Curses::UI will call C on exit. Defaults to false.
96              
97             =head2 -color_support
98              
99             If true, Curses::UI tries to enable color for the
100             application. Defaults to false.
101              
102             =head2 -compat
103              
104             If true, Curses::UI will run in compatibility mode, meaning that only
105             very simple characters will be used for creating the widgets. Defaults
106             to false.
107              
108             =head2 -keydelay
109              
110             If set to a positive integer, Curses::UI will track elapsed seconds
111             since the user's last keystroke, preventing timer events from
112             occurring for the specified number of seconds afterwards. By default
113             this option is set to '0' (disabled).
114              
115             =head2 -mouse_support
116              
117             Curses::UI attempts to auto-discover if mouse support should be
118             enabled or not. This option allows a hard override. Expects a boolean
119             value.
120              
121             =head2 -userdata
122              
123             Takes a scalar (frequently a hashref) as its argument, and stows that
124             scalar inside the Curses::UI object where it can be retrieved with the
125             L<#userdata> method. Handy inside callbacks and the like.
126              
127             =head2 -default_colors
128              
129             Directs the underlying Curses library to allow use of default color
130             pairs on terminals. Is preset to true and you almost certainly don't
131             want to twiddle it. See C if you think you do.
132              
133             =cut
134              
135             sub new {
136 9     9 1 1369 my ($class,%userargs) = @_;
137              
138 9 50       41 fatalerror("Curses::UI->new can only be called once!")
139             if $Curses::UI::initialized;
140              
141 9         59 &Curses::UI::Common::keys_to_lowercase(\%userargs);
142              
143 9         187 my %args = (
144             -compat => 0, # Use compatibility mode?
145             -clear_on_exit => 0, # Clear screen if program exits?
146             -cursor_mode => 0, # What is the current cursor_mode?
147             -debug => undef, # Turn on debugging mode?
148             -keydelay => 0, # Track seconds since last keystroke?
149             -language => undef, # Which language to use?
150             -mouse_support => 1, # Do we want mouse support
151             -overlapping => 1, # Whether overlapping widgets are supported
152             -color_support => 0,
153             -default_colors=> 1,
154             #user data
155             -userdata => undef, #user internal data
156             %userargs,
157             -read_timeout => -1, # full blocking read by default
158             -scheduled_code => [],
159             -added_code => {},
160             -lastkey => 0, # Last keypress time (set in mainloop)
161             );
162              
163 9 100       50 $Curses::UI::debug = $args{-debug}
164             if defined $args{-debug};
165              
166 9 50       62 $Curses::UI::ncurses_mouse = $args{-mouse_support}
167             if defined $args{-mouse_support};
168              
169 9 100 66     50 if ($Curses::UI::gpm_mouse && $args{-mouse_support}) {
170 1         2 $Curses::UI::ncurses_mouse = 1;
171 1         3 $args{-read_timeout} = 0.25;
172             } else {
173 8         18 $Curses::UI::gpm_mouse = 0;
174             }
175              
176 9         85 my $self = bless { %args }, $class;
177              
178 9         241 my $lang = new Curses::UI::Language($self->{-language});
179 9         52 $self->lang($lang);
180 9 50       41 print STDERR "DEBUG: Loaded language: $lang->{-lang}\n"
181             if $Curses::UI::debug;
182              
183             # Color support
184 9 50       50 $Curses::UI::color_support = $args{-color_support} if
185             defined $args{-color_support};
186              
187 9         55 $self->layout();
188              
189 9         64 return $self;
190             }
191              
192             DESTROY {
193 3     3   1049 my $self = shift;
194 3         16 my $scr = $self->{-canvasscr};
195 3 50       23 $scr->delwin() if (defined($scr));
196 3         409 endwin();
197 3         201 $Curses::UI::initialized = 0;
198              
199 3 50       279 if ($self->{-clear_on_exit})
200 0         0 { Curses::erase(); Curses::clear() }
  0         0  
201             }
202              
203              
204              
205             =head1 EVENT HANDLING METHODS
206              
207             =head2 mainloop
208              
209             The Curses::UI event handling loop. Call once setup is finished to
210             "start" a C::UI program.
211              
212             =cut
213              
214             sub mainloop {
215 0     0 1 0 my ($self) = @_;
216              
217             # Draw the initial screen.
218 0         0 $self->focus(undef, 1); # 1 = forced focus
219 0         0 $self->draw;
220 0         0 doupdate();
221              
222 0         0 $self->{mainloop}=1;
223              
224             # Inifinite event loop.
225 0         0 while ($self->{mainloop}) { $self->do_one_event }
  0         0  
226             }
227              
228             =head2 mainloopExit
229              
230             This exits the main loop.
231              
232             =cut
233              
234             sub mainloopExit{
235 0     0 1 0 my $self=$_[0];
236              
237 0         0 $self->{mainloop}=undef;
238             }
239              
240             =head2 schedule_event
241              
242             Pushes its argument (a coderef) onto the scheduled event stack
243              
244             =cut
245              
246             sub schedule_event {
247 0     0 1 0 my ($self, $code) = @_;
248              
249 0 0 0     0 $self->fatalerror("schedule_event(): callback is no CODE reference")
250             unless defined $code and ref $code eq 'CODE';
251              
252 0         0 push @{$self->{-scheduled_code}}, $code;
  0         0  
253             }
254              
255              
256              
257             =head1 WINDOW/LAYOUT METHODS
258              
259             =head2 layout
260              
261             The layout method of Curses::UI tries to find the size of the screen
262             then calls the C method of every contained object (i.e. window
263             or widget). It is not normally necessary to call this method directly.
264              
265             =cut
266              
267             sub layout {
268 9     9 1 29 my ($self) = @_;
269              
270 9         17 $Curses::UI::screen_too_small = 0;
271              
272             # find the terminal size.
273 9         54 my ($cols,$lines) = GetTerminalSize;
274 9         471 $ENV{COLS} = $cols;
275 9         39 $ENV{LINES} = $lines;
276              
277 9 50       31 if ($Curses::UI::initialized)
278             {
279 0         0 my $scr = $self->{-canvasscr};
280 0 0       0 $scr->delwin() if (defined($scr));
281 0         0 endwin();
282             }
283             # Initialize the curses screen.
284 9         90 initscr();
285 9         1694 noecho();
286 9         1503 raw();
287              
288             # Colors
289 9 50       1528 if ($Curses::UI::color_support) {
290 0 0       0 if ( has_colors() ) {
291 0         0 $Curses::UI::color_object = new Curses::UI::Color(-default_colors => $self->{-default_colors});
292             } else {
293 0         0 $Curses::UI::color_support = 0;
294             }
295             }
296              
297             # Mouse events if possible
298 9         20 my $old = 0;
299 9         15 my $mmreturn;
300 9 50       40 if ( $Curses::UI::ncurses_mouse )
301             {
302 9 50       30 print STDERR "DEBUG: ncurses mouse events are enabled\n"
303             if $Curses::UI::debug;
304             # In case of gpm, mousemask fails. (MT: Not for me, maybe GPM changed?)
305 9         15 eval { $mmreturn = mousemask( ALL_MOUSE_EVENTS(), $old ) };
  9         55  
306 9 50       2505 if ($Curses::UI::debug) {
307 0         0 print STDERR "DEBUG: mousemask returned $mmreturn\n";
308 0         0 print STDERR "DEBUG: Old is now $old\n";
309 0 0       0 print STDERR "DEBUG: mousemask() failed: $@\n" if $@;
310             }
311             }
312              
313             # Create root window.
314 9         48 my $root = newwin($lines, $cols, 0, 0);
315 9 50       82 die "newwin($lines, $cols, 0, 0) failed\n"
316             unless defined $root;
317              
318             # Let this object present itself as a standard
319             # Curses::UI widget, regarding size, location and
320             # drawing area. This will make it possible for
321             # child windows / widgets to layout and draw themselves.
322 9         51 $self->{-width} = $self->{-w} = $self->{-bw} = $cols;
323 9         48 $self->{-height} = $self->{-h} = $self->{-bh} = $lines;
324 9         36 $self->{-x} = $self->{-y} = 0;
325 9         21 $self->{-canvasscr} = $root;
326              
327             # Walk through all contained objects and let them
328             # layout themselves.
329 9         123 $self->layout_contained_objects;
330 9         44 $self->draw();
331              
332 9         1484 $Curses::UI::initialized = 1;
333 9         25 return $self;
334             }
335              
336             sub layout_new()
337             {
338 0     0 0 0 my $self = shift;
339              
340 0         0 $Curses::UI::screen_too_small = 0;
341              
342             # find the terminal size.
343 0         0 my ($cols,$lines) = GetTerminalSize;
344 0         0 $ENV{COLS} = $cols;
345 0         0 $ENV{LINES} = $lines;
346              
347             # Let this object present itself as a standard
348             # Curses::UI widget, regarding size, location and
349             # drawing area. This will make it possible for
350             # child windows / widgets to layout and draw themselves.
351             #
352 0         0 $self->{-width} = $self->{-w} = $self->{-bw} = $cols;
353 0         0 $self->{-height} = $self->{-h} = $self->{-bh} = $lines;
354 0         0 $self->{-x} = $self->{-y} = 0;
355             # $self->{-canvasscr} = $root;
356              
357             # Walk through all contained objects and let them
358             # layout themselves.
359 0         0 $self->layout_contained_objects;
360              
361 0         0 $Curses::UI::initialized = 1;
362 0         0 $self->draw();
363 0         0 return $self;
364             }
365              
366              
367             # ----------------------------------------------------------------------
368             # Event handling
369             # ----------------------------------------------------------------------
370              
371              
372             # TODO: document
373             sub do_one_event(;$)
374             {
375 2     2 0 1278 my $self = shift;
376 2         3 my $object = shift;
377 2 50       7 $object = $self unless defined $object;
378              
379 2         3 eval {curs_set($self->{-cursor_mode})};
  2         7  
380              
381             # gpm mouse?
382 2 100       8 if ($Curses::UI::gpm_mouse) {
383 1         5 $self->handle_gpm_mouse_event($object);
384 0         0 doupdate();
385             }
386              
387             # Read a key or use the feeded key.
388 1         3 my $key = $self->{-feedkey};
389 1 50       2 unless (defined $key) {
390 1         11 $key = $self->get_key($self->{-read_timeout});
391             }
392 1         2 $self->{-feedkey} = undef;
393              
394             # If there was a keypress, set -lastkey
395 1 50       6 $self->{-lastkey} = time() unless ($key eq '-1');
396              
397             # ncurses sends KEY_RESIZE() key on resize. Ignore this key.
398             # TODO: Try to redraw and layout everything anew
399             # KEY_RESIZE doesn't seem to work right;
400 1 50       15 if (Curses->can("KEY_RESIZE")) {
401 0 0       0 eval { $key = '-1' if $key eq KEY_RESIZE(); };
  0         0  
402             }
403 1         5 my ($cols,$lines) = GetTerminalSize;
404 1 50 33     41 if ( ($ENV{COLS} != $cols) || ( $ENV{LINES} != $lines )) {
405 0         0 $self->layout();
406 0         0 $self->draw;
407             }
408              
409             # ncurses sends KEY_MOUSE()
410 1 50       4 if ($Curses::UI::ncurses_mouse) {
411 1 50       7 if ($key eq KEY_MOUSE()) {
412 0 0       0 print STDERR "DEBUG: Got a KEY_MOUSE(), handeling it\n"
413             if $Curses::UI::debug;
414 0         0 $self->handle_mouse_event($object);
415 0         0 doupdate();
416 0         0 return $self;
417             }
418             }
419              
420             # If the screen is too small, then will exit.
421             # Else the next event loop will be started.
422 1 50       207 if ($Curses::UI::screen_too_small) {
423 0 0       0 exit(1) if $key eq "\cC";
424 0         0 return $self;
425             }
426              
427             # Delegate the keypress. This is not done to $self,
428             # but to $object, so all events will go to the
429             # object that called do_one_event(). This is used to
430             # enable modal focusing.
431 1 50       4 $object->event_keypress($key) unless $key eq '-1';
432              
433             # Execute timer code
434 1         4 $self->do_timer;
435              
436             # Execute one scheduled event;
437 1 50       1 if (@{$self->{-scheduled_code}}) {
  1         5  
438 0         0 my $code = shift @{$self->{-scheduled_code}};
  0         0  
439 0         0 $code->($self);
440             }
441              
442             # Execute added code
443 1         7 foreach my $key (keys %{$self->{-added_code}}) {
  1         4  
444 0         0 my $code = $self->{-added_code}->{$key};
445 0 0       0 $self->fatalerror("Method $key is not a coderef")
446             if (ref $code ne 'CODE');
447 0         0 $code->($self);
448             }
449              
450              
451             # Update the screen.
452 1         3 doupdate();
453              
454 1         3 return $self;
455             }
456              
457             # TODO: document
458              
459             # TODO: document
460             sub add_callback()
461             {
462 0     0 1 0 my $self = shift;
463 0         0 my $id = shift;
464 0         0 my $code = shift;
465              
466 0 0       0 $self->fatalerror(
467             "add_callback(): is is not set"
468             ) unless defined $id;
469              
470 0 0 0     0 $self->fatalerror(
471             "add_callback(): callback is no CODE reference"
472             ) unless defined $code and ref $code eq 'CODE';
473              
474 0         0 $self->{-added_code}->{$id} = $code;
475             }
476              
477             # TODO: document
478             sub delete_callback()
479             {
480 0     0 1 0 my $self = shift;
481 0         0 my $id = shift;
482              
483 0 0       0 $self->fatalerror(
484             "delete_callback(): id is not set"
485             ) unless defined $id;
486              
487 0 0       0 delete $self->{-added_code}->{$id} if
488             defined $self->{-added_code}->{$id};
489             }
490              
491             sub draw()
492             {
493 10     10 1 19 my $self = shift;
494 10   100     57 my $no_doupdate = shift || 0;
495              
496 10 50       34 if ($Curses::UI::screen_too_small)
497             {
498 0         0 my $s = $self->{-canvasscr};
499 0         0 $s->clear;
500 0         0 $s->addstr(0, 0, $self->lang->get('screen_too_small'));
501 0         0 $s->move(4,0);
502 0         0 $s->noutrefresh();
503 0         0 doupdate();
504             } else {
505 10         129 $self->SUPER::draw(1);
506 10 100       89 doupdate() unless $no_doupdate;
507             }
508             }
509              
510             # TODO: document
511             sub feedkey()
512             {
513 0     0 0 0 my $self = shift;
514 0         0 my $key = shift;
515 0         0 $self->{-feedkey} = $key;
516 0         0 return $self;
517             }
518              
519             # TODO: document
520             sub flushkeys()
521             {
522 0     0 0 0 my $self = shift;
523              
524 0         0 my $key = '';
525 0         0 my @k = ();
526 0         0 until ( $key eq "-1" ) {
527 0         0 $key = $self->get_key(0);
528             }
529             }
530              
531             # Returns 0 if less than -keydelay seconds have elapsed since the last
532             # user action. Returns the number of elapsed seconds otherwise.
533             sub keydelay()
534             {
535 0     0 1 0 my $self = shift;
536              
537 0         0 my $time = time();
538 0         0 my $elapsed = $time - $self->{-lastkey};
539              
540 0 0       0 return 0 if ($elapsed < $self->{-keydelay});
541 0         0 return $elapsed;
542             }
543              
544             # ----------------------------------------------------------------------
545             # Timed event handling
546             # ----------------------------------------------------------------------
547              
548             sub set_read_timeout()
549             {
550 0     0 0 0 my $self = shift;
551              
552 0         0 my $new_timeout = -1;
553 0         0 TIMER: while (my ($id, $config) = each %{$self->{-timers}})
  0         0  
554             {
555             # Skip timer if it is disabled.
556 0 0       0 next TIMER unless $config->{-enabled};
557              
558 0 0 0     0 $new_timeout = $config->{-time}
559             unless $new_timeout != -1 and
560             $new_timeout < $config->{-time};
561             }
562 0 0 0     0 $new_timeout = 1 if $new_timeout < 0 and $new_timeout != -1;
563              
564 0         0 $self->{-read_timeout} = $new_timeout;
565 0         0 return $self;
566             }
567              
568             sub set_timer($$;)
569             {
570 0     0 0 0 my $self = shift;
571 0         0 my $id = shift;
572 0         0 my $callback = shift;
573 0   0     0 my $time = shift || 1;
574              
575 0 0 0     0 $self->fatalerror(
576             "add_timer(): callback is no CODE reference"
577             ) unless defined $callback and ref $callback eq 'CODE';
578              
579 0 0       0 $self->fatalerror(
580             "add_timer(): id is not set"
581             ) unless defined $id;
582              
583 0         0 my $config = {
584             -time => $time,
585             -callback => $callback,
586             -enabled => 1,
587             -lastrun => time(),
588             };
589 0         0 $self->{-timers}->{$id} = $config;
590              
591 0         0 $self->set_read_timeout;
592              
593 0         0 return $self;
594             }
595              
596             sub disable_timer($;)
597             {
598 0     0 0 0 my ($self,$id) = @_;
599 0 0       0 if (defined $self->{-timers}->{$id}) {
600 0         0 $self->{-timers}->{$id}->{-enabled} = 0;
601             }
602 0         0 $self->set_read_timeout;
603 0         0 return $self;
604             }
605              
606             sub enable_timer($;)
607             {
608 0     0 0 0 my ($self,$id) = @_;
609 0 0       0 if (defined $self->{-timers}->{$id}) {
610 0         0 $self->{-timers}->{$id}->{-enabled} = 1;
611             }
612 0         0 $self->set_read_timeout;
613 0         0 return $self;
614             }
615              
616             sub delete_timer($;)
617             {
618 0     0 0 0 my ($self,$id) = @_;
619 0 0       0 if (defined $self->{-timers}->{$id}) {
620 0         0 delete $self->{-timers}->{$id};
621             }
622 0         0 $self->set_read_timeout;
623 0         0 return $self;
624             }
625              
626             sub do_timer()
627             {
628 1     1 0 2 my $self = shift;
629              
630 1         7 my $now = time();
631 1         2 my $timers_done = 0;
632              
633             # Short-circuit timers if the keydelay hasn't elapsed
634 1 50       5 if ($self->{-keydelay}) {
635 0 0       0 return $self unless $self->keydelay;
636             }
637              
638 1         2 TIMER: while (my ($id, $config) = each %{$self->{-timers}})
  1         13  
639             {
640             # Skip timer if it is disabled.
641 0 0       0 next TIMER unless $config->{-enabled};
642              
643             # No -lastrun set? Then do it now.
644 0 0       0 unless (defined $config->{-lastrun})
645             {
646 0         0 $config->{-lastrun} = $now;
647 0         0 next TIMER;
648             }
649              
650 0 0       0 if ($config->{-lastrun} <= ($now - $config->{-time}))
651             {
652 0         0 $config->{-callback}->($self);
653 0         0 $config->{-lastrun} = $now;
654 0         0 $timers_done++;
655             }
656             }
657              
658             # Bring the cursor back to the focused object by
659             # redrawing it. Due to drawing other objects, it might
660             # have moved to another widget or screen location.
661             #
662 1 50       2 $self->focus_path(-1)->draw if $timers_done;
663              
664 1         2 return $self;
665             }
666              
667             # ----------------------------------------------------------------------
668             # Mouse events
669             # ----------------------------------------------------------------------
670              
671             sub handle_mouse_event()
672             {
673 0     0 0 0 my $self = shift;
674 0         0 my $object = shift;
675 0 0       0 $object = $self unless defined $object;
676              
677 0         0 my $MEVENT = 0;
678 0         0 getmouse($MEVENT);
679              
680             # $MEVENT is a struct. From curses.h (note: this might change!):
681             #
682             # typedef struct
683             # {
684             # short id; /* ID to distinguish multiple devices */
685             # int x, y, z; /* event coordinates (character-cell) */
686             # mmask_t bstate; /* button state bits */
687             # } MEVENT;
688             #
689             # ---------------
690             # s signed short
691             # x null byte
692             # x null byte
693             # ---------------
694             # i integer
695             # ---------------
696             # i integer
697             # ---------------
698             # i integer
699             # ---------------
700             # l long
701             # ---------------
702              
703 0         0 my ($id, $x, $y, $z, $bstate) = unpack("sx2i3l", $MEVENT);
704 0         0 my %MEVENT = (
705             -id => $id,
706             -x => $x,
707             -y => $y,
708             -bstate => $bstate,
709             );
710              
711             # Get the objects at the mouse event position.
712 0         0 my $tree = $self->object_at_xy($object, $MEVENT{-x}, $MEVENT{-y});
713              
714             # Walk through the object tree, top object first.
715 0         0 foreach my $object (reverse @$tree)
716             {
717             # Send the mouse-event to the object.
718             # Leave the loop if the object handled the event.
719 0 0       0 print STDERR "Asking $object to handle $MEVENT{-bstate} ...\n" if
720             $Curses::UI::debug;
721 0         0 my $return = $object->event_mouse(\%MEVENT);
722 0 0 0     0 last if defined $return and $return ne 'DELEGATE';
723             }
724             }
725              
726             sub handle_gpm_mouse_event()
727             {
728 1     1 0 2 my $self = shift;
729 1         1 my $object = shift;
730 1 50       4 $object = $self unless defined $object;
731              
732 1 50       3 return unless $Curses::UI::gpm_mouse;
733              
734 1         26 my $MEVENT = gpm_get_mouse_event();
735             # $MEVENT from C:UI:MH:GPM is identical.
736              
737 0 0       0 return unless $MEVENT;
738              
739 0         0 my ($id, $x, $y, $z, $bstate) = unpack("sx2i3l", $MEVENT);
740 0         0 my %MEVENT = (
741             -id => $id,
742             -x => $x,
743             -y => $y,
744             -bstate => $bstate,
745             );
746              
747             # Get the objects at the mouse event position.
748 0         0 my $tree = $self->object_at_xy($object, $MEVENT{-x}, $MEVENT{-y});
749              
750             # Walk through the object tree, top object first.
751 0         0 foreach my $object (reverse @$tree)
752             {
753             # Send the mouse-event to the object.
754             # Leave the loop if the object handled the event.
755              
756 0         0 my $return = $object->event_mouse(\%MEVENT);
757 0 0 0     0 last if defined $return and $return ne 'DELEGATE';
758             }
759             }
760              
761              
762             sub object_at_xy($$;$)
763             {
764 0     0 0 0 my $self = shift;
765 0         0 my $object = shift;
766 0         0 my $x = shift;
767 0         0 my $y = shift;
768 0         0 my $tree = shift;
769 0 0       0 $tree = [] unless defined $tree;
770              
771 0         0 push @$tree, $object;
772              
773 0         0 my $idx = -1;
774 0         0 while (defined $object->{-draworder}->[$idx])
775             {
776 0         0 my $testobj = $object->getobj($object->{-draworder}->[$idx]);
777 0         0 $idx--;
778              
779             # Find the window parameters for the $testobj.
780 0 0       0 my $scr = defined $testobj->{-borderscr} ? '-borderscr' : '-canvasscr';
781 0         0 my $winp = $testobj->windowparameters($scr);
782              
783             # Does the click fall inside this object?
784 0 0 0     0 if ( $x >= $winp->{-x} and
      0        
      0        
785             $x < ($winp->{-x}+$winp->{-w}) and
786             $y >= $winp->{-y} and
787             $y < ($winp->{-y}+$winp->{-h}) ) {
788              
789 0 0 0     0 if ( $testobj->isa('Curses::UI::Container') and
790             not $testobj->isa('Curses::UI::ContainerWidget')) {
791 0         0 $self->object_at_xy($testobj, $x, $y, $tree)
792             } else {
793 0         0 push @$tree, $testobj;
794             }
795 0         0 return $tree;
796             }
797             }
798              
799 0         0 return $tree;
800             }
801              
802              
803             # ----------------------------------------------------------------------
804             # Other subroutines
805             # ----------------------------------------------------------------------
806              
807             # TODO: document
808             sub fatalerror($$;$)
809             {
810 0     0 0 0 my $self = shift;
811 0         0 my $error = shift;
812 0         0 my $exit = shift;
813              
814 0 0       0 $exit = 1 unless defined $exit;
815 0         0 chomp $error;
816 0         0 $error .= "\n";
817              
818 0         0 my $s = $self->{-canvasscr};
819 0         0 $s->clear;
820 0         0 $s->addstr(0,0,"Fatal program error:\n"
821             . "-"x($ENV{COLS}-1) . "\n"
822             . $error
823             . "-"x($ENV{COLS}-1) . "\n"
824             . "Press any key to exit...");
825 0         0 $s->noutrefresh();
826 0         0 doupdate();
827              
828 0         0 $self->flushkeys();
829 0         0 for (;;)
830             {
831 0         0 my $key = $self->get_key();
832 0 0       0 last if $key ne "-1";
833             }
834              
835 0         0 exit($exit);
836             }
837              
838             sub usemodule($;)
839             {
840 25     25 1 40 my $self = shift;
841 25         40 my $class = shift;
842              
843             # Create class filename.
844 25         32 my $file = $class;
845 25         95 $file =~ s|::|/|g;
846 25         36 $file .= '.pm';
847              
848             # Automatically load the required class.
849 25 100       72 if (not defined $INC{$file})
850             {
851             eval
852 9         14 {
853 9         6967 require $file;
854 9         286 $class->import;
855             };
856              
857             # Fatal error if the class could not be loaded.
858 9 50       46 $self->fatalerror("Could not load $class from $file:\n$@")
859             if $@;
860             }
861              
862 25         64 return $self;
863             }
864              
865             sub focus_path()
866             {
867 3     3 0 12 my $self = shift;
868 3         7 my $index = shift;
869              
870 3         5 my $p_obj = $self;
871 3         7 my @path = ($p_obj);
872 3         5 for(;;)
873             {
874 9         32 my $p_el = $p_obj->{-draworder}->[-1];
875 9 100       23 last unless defined $p_el;
876 6         16 $p_obj = $p_obj->{-id2object}->{$p_el};
877 6         9 push @path, $p_obj;
878 6 50       38 last if $p_obj->isa('Curses::UI::ContainerWidget');
879             }
880              
881 3 50       20 return (defined $index ? $path[$index] : @path);
882             }
883              
884             # add() is overridden, because we only want to be able
885             # to add Curses::UI:Window objects to the Curses::UI
886             # rootlevel.
887             #
888             sub add()
889             {
890 6     6 1 4985 my $self = shift;
891 6         25 my $id = shift;
892 6         13 my $class = shift;
893 6         22 my %args = @_;
894              
895             # Make it possible to specify WidgetType instead of
896             # Curses::UI::WidgetType.
897 6 50 33     43 $class = "Curses::UI::$class"
898             if $class !~ /\:\:/ or
899             $class =~ /^Dialog\:\:[^\:]+$/;
900              
901 6         31 $self->usemodule($class);
902              
903 6 50       96 $self->fatalerror(
904             "You may only add Curses::UI::Window objects to "
905             . "Curses::UI and no $class objects"
906             ) unless $class->isa('Curses::UI::Window');
907              
908 6         75 $self->SUPER::add($id, $class, %args);
909             }
910              
911             # Sets/Get the user data
912             sub userdata
913             {
914 2     2 1 1111 my $self = shift;
915 2 100       7 if (defined $_[0])
916             {
917 1         4 $self->{-userdata} = $_[0];
918             }
919 2         9 return $self->{-userdata};
920             }
921              
922             # ----------------------------------------------------------------------
923             # Focusable dialog windows
924             # ----------------------------------------------------------------------
925              
926             sub tempdialog()
927             {
928 0     0 0 0 my $self = shift;
929 0         0 my $class = shift;
930 0         0 my %args = @_;
931              
932 0         0 my $id = "__window_$class";
933              
934 0         0 my $dialog = $self->add($id, $class, %args);
935 0         0 $dialog->modalfocus;
936 0         0 my $return = $dialog->get;
937 0         0 $self->delete($id);
938 0         0 $self->root->focus(undef, 1);
939 0         0 return $return;
940             }
941              
942             # The argument list will be returned unchanged, unless it
943             # contains only one item. In that case ($ifone, $_[0]) will
944             # be returned. This enables constructions like:
945             #
946             # $cui->dialog("Some dialog message");
947             #
948             # instead of:
949             #
950             # $cui->dialog(-message => "Some dialog message");
951             #
952             sub process_args()
953             {
954 0     0 0 0 my $self = shift;
955 0         0 my $ifone = shift;
956 0 0       0 if (@_ == 1) { @_ = ($ifone => $_[0]) }
  0         0  
957 0         0 return @_;
958             }
959              
960             sub error()
961             {
962 0     0 1 0 my $self = shift;
963 0         0 my %args = $self->process_args('-message', @_);
964 0         0 $self->tempdialog('Dialog::Error', %args);
965             }
966              
967             sub dialog()
968             {
969 0     0 1 0 my $self = shift;
970 0         0 my %args = $self->process_args('-message', @_);
971 0         0 $self->tempdialog('Dialog::Basic', %args);
972             }
973              
974             sub question()
975             {
976 0     0 0 0 my $self = shift;
977 0         0 my %args = $self->process_args('-question', @_);
978 0         0 $self->tempdialog('Dialog::Question', %args);
979             }
980              
981             sub calendardialog()
982             {
983 0     0 0 0 my $self = shift;
984 0         0 my %args = $self->process_args('-title', @_);
985 0         0 $self->tempdialog('Dialog::Calendar', %args);
986             }
987              
988             sub filebrowser()
989             {
990 0     0 1 0 my $self = shift;
991 0         0 my %args = $self->process_args('-title', @_);
992              
993             # Create title
994 0 0       0 unless (defined $args{-title}) {
995 0         0 my $l = $self->root->lang;
996 0         0 $args{-title} = $l->get('file_title');
997             }
998              
999             # Select a file to load from.
1000 0         0 $self->tempdialog('Dialog::Filebrowser', %args);
1001             }
1002              
1003             sub dirbrowser()
1004             {
1005 0     0 0 0 my $self = shift;
1006 0         0 my %args = $self->process_args('-title', @_);
1007              
1008             # Create title
1009 0 0       0 unless (defined $args{-title}) {
1010 0         0 my $l = $self->root->lang;
1011 0         0 $args{-title} = $l->get('dir_title');
1012             }
1013              
1014             # Select a file to load from.
1015 0         0 $self->tempdialog('Dialog::Dirbrowser', %args);
1016             }
1017              
1018             sub savefilebrowser()
1019             {
1020 0     0 1 0 my $self = shift;
1021 0         0 my %args = $self->process_args('-title', @_);
1022              
1023 0         0 my $l = $self->root->lang;
1024              
1025             # Create title.
1026 0 0       0 $args{-title} = $l->get('file_savetitle')
1027             unless defined $args{-title};
1028              
1029             # Select a file to save to.
1030 0         0 my $file = $self->filebrowser(-editfilename => 1, %args);
1031 0 0       0 return unless defined $file;
1032              
1033             # Check if the file exists. Ask for overwrite
1034             # permission if it does.
1035 0 0       0 if (-e $file)
1036             {
1037             # Get language specific data.
1038 0         0 my $pre = $l->get('file_overwrite_question_pre');
1039 0         0 my $post = $l->get('file_overwrite_question_post');
1040 0         0 my $title = $l->get('file_overwrite_title');
1041              
1042 0         0 my $overwrite = $self->dialog(
1043             -title => $title,
1044             -buttons => [ 'yes', 'no' ],
1045             -message => $pre . $file . $post,
1046             );
1047 0 0       0 return unless $overwrite;
1048             }
1049              
1050 0         0 return $file;
1051             }
1052              
1053             sub loadfilebrowser()
1054             {
1055 0     0 1 0 my $self = shift;
1056 0         0 my %args = $self->process_args('-title', @_);
1057              
1058             # Create title
1059 0 0       0 unless (defined $args{-title}) {
1060 0         0 my $l = $self->root->lang;
1061 0         0 $args{-title} = $l->get('file_loadtitle');
1062             }
1063              
1064 0         0 $self->filebrowser(-editfilename => 0, %args);
1065             }
1066              
1067             # ----------------------------------------------------------------------
1068             # Non-focusable dialogs
1069             # ----------------------------------------------------------------------
1070              
1071             my $status_id = "__status_dialog";
1072             sub status($;)
1073             {
1074 0     0 1 0 my $self = shift;
1075 0         0 my %args = $self->process_args('-message', @_);
1076              
1077 0         0 $self->delete($status_id);
1078 0         0 $self->add($status_id, 'Dialog::Status', %args)->draw;
1079              
1080 0         0 return $self;
1081             }
1082              
1083             sub nostatus()
1084             {
1085 0     0 1 0 my $self = shift;
1086 0         0 $self->delete($status_id);
1087 0         0 $self->flushkeys();
1088 0         0 $self->draw;
1089 0         0 return $self;
1090             }
1091              
1092             sub progress()
1093             {
1094 0     0 1 0 my $self = shift;
1095 0         0 my %args = @_;
1096              
1097 0         0 $self->add(
1098             "__progress_$self",
1099             'Dialog::Progress',
1100             %args
1101             );
1102 0         0 $self->draw;
1103              
1104 0         0 return $self;
1105             }
1106              
1107             sub setprogress($;$)
1108             {
1109 0     0 1 0 my $self = shift;
1110 0         0 my $pos = shift;
1111 0         0 my $message = shift;
1112              
1113             # If I do not do this, the progress bar seems frozen
1114             # if a key is pressed on my Solaris machine. Flushing
1115             # the input keys solves this. And this is not a bad
1116             # thing to do during a progress dialog (input is ignored
1117             # this way).
1118 0         0 $self->flushkeys;
1119              
1120 0         0 my $p = $self->getobj("__progress_$self");
1121 0 0       0 return unless defined $p;
1122 0 0       0 $p->pos($pos) if defined $pos;
1123 0 0       0 $p->message($message) if defined $message;
1124 0         0 $p->draw;
1125              
1126 0         0 return $self;
1127             }
1128              
1129             sub noprogress()
1130             {
1131 0     0 1 0 my $self = shift;
1132 0         0 $self->delete("__progress_$self");
1133 0         0 $self->flushkeys;
1134 0         0 $self->draw;
1135 0         0 return $self;
1136             }
1137              
1138             sub leave_curses()
1139             {
1140 7     7 1 45 my $self = shift;
1141 7         44 def_prog_mode();
1142 7         1145 endwin();
1143             }
1144              
1145             sub reset_curses()
1146             {
1147 0     0 1 0 my $self = shift;
1148 0         0 reset_prog_mode();
1149 0         0 $self->layout(); # In case the terminal has been resized
1150             }
1151              
1152             ### Color support
1153              
1154             sub color() {
1155 1     1 1 6 my $self = shift;
1156 1         6 return $Curses::UI::color_object;
1157             }
1158              
1159             sub set_color {
1160 1     1 1 544 my $self = shift;
1161 1         2 my $co = shift;
1162              
1163 1         3 $Curses::UI::color_object = $co;
1164             }
1165              
1166              
1167              
1168             # ----------------------------------------------------------------------
1169             # Accessor functions
1170             # ----------------------------------------------------------------------
1171              
1172 0     0 1 0 sub compat(;$) { shift()->accessor('-compat', shift()) }
1173 4     4 1 15 sub clear_on_exit(;$) { shift()->accessor('-clear_on_exit', shift()) }
1174 0     0 0 0 sub cursor_mode(;$) { shift()->accessor('-cursor_mode', shift()) }
1175 9     9 0 93 sub lang(;$) { shift()->accessor('-language_object', shift()) }
1176 3     3 0 13 sub overlapping(;$) { shift()->accessor('-overlapping', shift()) }
1177              
1178             # TODO: document
1179             sub debug(;$)
1180             {
1181 0     0 0   my $self = shift;
1182 0           my $value = shift;
1183 0           $Curses::UI::debug = $self->accessor('-debug', $value);
1184             }
1185              
1186              
1187              
1188              
1189              
1190              
1191             =head1 CONVENIENCE DIALOG METHODS
1192              
1193             =head2 dialog
1194              
1195             Use the C method to show a dialog window. If you only provide
1196             a single argument, this argument will be used as the message to
1197             show. Example:
1198              
1199             $cui->dialog("Hello, world!");
1200              
1201             If you want to have some more control over the dialog window, you will
1202             have to provide more arguments (for an explanation of the arguments
1203             that can be used, see L. Example:
1204              
1205             my $yes = $cui->dialog(
1206             -message => "Hello, world?",
1207             -buttons =3D> ['yes','no'],
1208             -values => [1,0],
1209             -title => 'Question',
1210             );
1211              
1212             if ($yes) {
1213             # whatever
1214             }
1215              
1216              
1217             =head2 error
1218              
1219             The C method will create an error dialog. This is basically a
1220             Curses::UI::Dialog::Basic, but it has an ASCII-art exclamation sign
1221             drawn left to the message. For the rest it's just like
1222             C. Example:
1223              
1224             $cui->error("It's the end of the\n"
1225             ."world as we know it!");
1226              
1227             =head2 filebrowser
1228              
1229             Creates a file browser dialog. For an explanation of the arguments
1230             that can be used, see L. Example:
1231              
1232             my $file = $cui->filebrowser(
1233             -path => "/tmp",
1234             -show_hidden => 1,
1235             );
1236              
1237             # Filebrowser will return undef
1238             # if no file was selected.
1239             if (defined $file) {
1240             unless (open F, ">$file") {
1241             print F "Hello, world!\n";
1242             close F;
1243             } else {
1244             $cui->error(qq(Error on writing to "$file":\n$!));
1245             }
1246              
1247             =head2 loadfilebrowser, savefilebrowser
1248              
1249             These two methods will create file browser dialogs as well. The
1250             difference is that these will have the dialogs set up correctly for
1251             loading and saving files. Moreover, the save dialog will check if the
1252             selected file exists or not. If it does exist, it will show an
1253             overwrite confirmation to check if the user really wants to overwrite
1254             the selected file.
1255              
1256             =head2 status, nostatus
1257              
1258             Using these methods it's easy to provide status information for the
1259             user of your program. The status dialog is a dialog with only a label
1260             on it. The status dialog doesn't really get the focus. It's only used
1261             to display some information. If you need more than one status, you can
1262             call C subsequently. Any existing status dialog will be
1263             cleaned up and a new one will be created.
1264              
1265             If you are finished, you can delete the status dialog by calling the
1266             C method. Example:
1267              
1268             $cui->status("Saying hello to the world...");
1269             # code for saying "Hello, world!"
1270              
1271             $cui->status("Saying goodbye to the world...");
1272             # code for saying "Goodbye, world!"
1273              
1274             $cui->nostatus;
1275              
1276             =head2 progress, setprogress, noprogress
1277              
1278             Using these methods it's easy to provide progress information to the
1279             user. The progress dialog is a dialog with an optional label on it and
1280             a progress bar. Similar to the status dialog, this dialog does not get
1281             the focus.
1282              
1283             Using the C method, a new progress dialog can be created.
1284             This method takes the same arguments as the
1285             L class.
1286              
1287             After that the progress can be set using C. This method
1288             takes one or two arguments. The first argument is the current position
1289             of the progressbar. The second argument is the message to show in the
1290             label. If one of these arguments is undefined, the current value will
1291             be kept.
1292              
1293             If you are finished, you can delete the progress dialog by calling the
1294             C method.
1295              
1296             $cui->progress(
1297             -max => 10,
1298             -message => "Counting 10 seconds...",
1299             );
1300              
1301             for my $second (0..10) {
1302             $cui->setprogress($second)
1303             sleep 1;
1304             }
1305              
1306             $cui->noprogress;
1307              
1308             =cut
1309              
1310              
1311              
1312             =head1 OTHER METHODS
1313              
1314             =over 4
1315              
1316             =item B ( )
1317              
1318             Temporarily leaves curses mode and recovers normal terminal mode.
1319              
1320             =item B ( )
1321              
1322             Return to curses mode after B.
1323              
1324             =item B ( ID, CLASS, OPTIONS )
1325              
1326             The B method of Curses::UI is almost the same as the B
1327             method of Curses::UI::Container. The difference is that Curses::UI
1328             will only accept classes that are (descendants) of the
1329             Curses::UI::Window class. For the rest of the information see
1330             L.
1331              
1332             =item B ( ID, CODE)
1333              
1334             This method lets you add a callback into the mainloop permanently.
1335             The code is executed after the input handler has run.
1336              
1337             =item B ( ID )
1338              
1339             This method deletes the CODE specified by ID from the mainloop.
1340              
1341             =item B ( CLASSNAME )
1342              
1343             Loads the with CLASSNAME given module.
1344              
1345             =item B ( [ SCALAR ] )
1346              
1347             This method will return the user internal data stored in the UI
1348             object. If a SCALAR parameter is specified it will also set the
1349             current user data to it.
1350              
1351             =item B ( )
1352              
1353             This method is used internally to control timer events when the
1354             B<-keydelay> option is set, but it can be called directly it to find
1355             out if the required amount of time has passed since the user's last
1356             action. B() will return 0 if insufficent time has passed,
1357             and will return the number of elapsed seconds otherwise.
1358              
1359             =item B ( [BOOLEAN] )
1360              
1361             The B<-compat> option will be set to the BOOLEAN value, unless BOOLEAN
1362             is omitted. The method returns the current value for B<-compat>.
1363              
1364             =item B ( [BOOLEAN] )
1365              
1366             The B<-clear_on_exit> option will be set to the BOOLEAN value, unless
1367             BOOLEAN is omitted. The method returns the current value for
1368             B<-clear_on_exit>.
1369              
1370             =item B ( )
1371              
1372             Returns the currently used Curses::UI::Color object
1373              
1374             =item B ( OBJECT )
1375              
1376             Replaces the currently used Color object with another. This can be
1377             used to fast change all colors in a Curses::UI application.
1378              
1379             =back
1380              
1381              
1382              
1383             =head1 SEE ALSO
1384              
1385             =over
1386              
1387             =item L
1388              
1389             =item L (a POE eventsystem and mainloop for Curses::UI)
1390              
1391             =item L (SVN repo, info, and links)
1392              
1393             =back
1394              
1395              
1396             =head1 BUGS
1397              
1398             Please report any bugs or feature requests to
1399             C, or through the web interface at
1400             L. I will be
1401             notified, and then you'll automatically be notified of progress on
1402             your bug as I make changes.
1403              
1404              
1405             =head1 AUTHOR
1406              
1407             Shawn Boyette C<< >>
1408              
1409             See the CREDITS file for additional information.
1410              
1411             =head1 COPYRIGHT & LICENSE
1412              
1413             Copyright 2001-2002 Maurice Makaay; 2003-2006 Marcus Thiesen; 2007,
1414             2008 Shawn Boyette. All Rights Reserved.
1415              
1416             This program is free software; you can redistribute it and/or modify
1417             it under the same terms as Perl itself.
1418              
1419             This package is free software and is provided "as is" without express
1420             or implied warranty. It may be used, redistributed and/or modified
1421             under the same terms as perl itself.
1422              
1423             =cut
1424              
1425              
1426             =head1 CLASS LISTING
1427              
1428             =head2 Widgets
1429              
1430             =over
1431              
1432             =item L
1433              
1434             =item L
1435              
1436             =item L
1437              
1438             =item L
1439              
1440             =item L
1441              
1442             =item L
1443              
1444             =item L (used by Curses::UI::Menubar)
1445              
1446             =item L
1447              
1448             =item L
1449              
1450             =item L
1451              
1452             =item L
1453              
1454             =item L
1455              
1456             =item L (used by Curses::UI::Searchable)
1457              
1458             =item L
1459              
1460             =item L
1461              
1462             =item L
1463              
1464             =item L
1465              
1466             =back
1467              
1468             =head2 Dialogs
1469              
1470             =over
1471              
1472             =item L
1473              
1474             =item L
1475              
1476             =item L
1477              
1478             =item L
1479              
1480             =back
1481              
1482             =head2 Base and Support Classes
1483              
1484             =over
1485              
1486             =item L
1487              
1488             =item L
1489              
1490             =item L
1491              
1492             =item L
1493              
1494             =item L
1495              
1496             =back
1497              
1498             =cut
1499              
1500             1; # end of Curses::UI