File Coverage

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


line stmt bran cond sub pod time code
1             #
2             # This file is part of Curses-Toolkit
3             #
4             # This software is copyright (c) 2011 by Damien "dams" Krotkine.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9 1     1   1050 use warnings;
  1         2  
  1         44  
10 1     1   5 use strict;
  1         3  
  1         71  
11              
12             package Curses::Toolkit;
13             {
14             $Curses::Toolkit::VERSION = '0.211';
15             }
16              
17             # ABSTRACT: a modern Curses toolkit
18              
19 1     1   5 use Params::Validate qw(SCALAR ARRAYREF HASHREF CODEREF GLOB GLOBREF SCALARREF HANDLE BOOLEAN UNDEF validate validate_pos);
  1         2  
  1         114  
20              
21 1     1   624 use Curses::Toolkit::Theme;
  0            
  0            
22              
23              
24             sub init_root_window {
25             my $class = shift;
26              
27             my %params = validate(
28             @_,
29             { theme_name => {
30             type => SCALAR,
31             optional => 1,
32             },
33             mainloop => { optional => 1 },
34             quit_key => {
35             type => SCALAR,
36             default => 'q',
37             },
38             switch_key => {
39             type => SCALAR,
40             default => 'r',
41             },
42             test_environment => {
43             type => HASHREF,
44             optional => 1,
45             },
46             }
47             );
48              
49             # get the Curses handler
50             use Curses;
51             my $curses_handler = Curses->new();
52              
53             # already done ?
54             # raw();
55             # cbreak();
56             # noecho();
57             # $curses_handler->keypad(1);
58              
59             if (has_colors) {
60             start_color();
61             }
62              
63             eval { Curses->can('NCURSES_MOUSE_VERSION') && ( NCURSES_MOUSE_VERSION() >= 1 ) };
64              
65             my $old_mouse_mask;
66             my $mouse_mask = mousemask( ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, $old_mouse_mask );
67              
68             use Curses::Toolkit::Theme::Default;
69             use Curses::Toolkit::Theme::Default::Color::Yellow;
70             use Curses::Toolkit::Theme::Default::Color::Pink;
71             use Curses::Toolkit::Theme::Default::Color::BlueWhite;
72              
73             use Tie::Array::Iterable;
74             $params{theme_name} ||= Curses::Toolkit->get_default_theme_name();
75             my @windows = ();
76             my $self = bless {
77             initialized => 1,
78             curses_handler => $curses_handler,
79             windows => Tie::Array::Iterable->new(@windows),
80             theme_name => $params{theme_name},
81             mainloop => $params{mainloop},
82             last_stack => 0,
83             event_listeners => [],
84             window_iterator => undef,
85             test_environment => $params{test_environment},
86             }, $class;
87             $self->_recompute_shape();
88              
89             use Curses::Toolkit::EventListener;
90              
91             # add a default listener that listen to any Shape event
92             $self->add_event_listener(
93             Curses::Toolkit::EventListener->new(
94             accepted_events => {
95             'Curses::Toolkit::Event::Shape' => sub { 1; },
96             },
97             code => sub {
98             my ( $screen_h, $screen_w );
99             $self->_recompute_shape();
100              
101             # for now we rebuild all coordinates
102             foreach my $window ( $self->get_windows() ) {
103             $window->rebuild_all_coordinates();
104             }
105             },
106             )
107             );
108             if ( defined $params{quit_key} ) {
109             $self->add_event_listener(
110             Curses::Toolkit::EventListener->new(
111             accepted_events => {
112             'Curses::Toolkit::Event::Key' => sub {
113             my ($event) = @_;
114             $event->{type} eq 'stroke' or return 0;
115             lc $event->{params}{key} eq $params{quit_key} or return 0;
116             },
117             },
118             code => sub {
119             exit;
120             },
121             )
122             );
123             }
124             if ( defined $params{switch_key} ) {
125             $self->add_event_listener(
126             Curses::Toolkit::EventListener->new(
127             accepted_events => {
128             'Curses::Toolkit::Event::Key' => sub {
129             my ($event) = @_;
130             $event->{type} eq 'stroke' or return 0;
131             lc $event->{params}{key} eq $params{switch_key} or return 0;
132             },
133             },
134             code => sub {
135             my ( $event, $widget ) = @_;
136             defined $self->{window_iterator}
137             or return;
138             my $window = $widget->{window_iterator}->next();
139             if ( !defined $window ) {
140             $widget->{window_iterator}->to_start();
141             $window = $widget->{window_iterator}->value();
142             }
143              
144             # get the currently focused widget, unfocus it
145             my $current_focused_widget = $self->get_focused_widget();
146             if ( defined $current_focused_widget && $current_focused_widget->can('set_focus') ) {
147             $current_focused_widget->set_focus(0);
148             }
149             $window->bring_to_front();
150              
151             # focus the window or one of its component
152             my $next_focused_widget =
153             $window->get_next_focused_widget(1); # 1 means "consider if $window is focusable"
154             defined $next_focused_widget
155             and $next_focused_widget->set_focus(1);
156             },
157             )
158             );
159             }
160              
161             # key listener for TAB
162             $self->add_event_listener(
163             Curses::Toolkit::EventListener->new(
164             accepted_events => {
165             'Curses::Toolkit::Event::Key' => sub {
166             my ($event) = @_;
167             $event->{type} eq 'stroke' or return 0;
168             $event->{params}{key} eq '<^I>' or return 0;
169             },
170             },
171             code => sub {
172             my $focused_widget = $self->get_focused_widget();
173             if ( defined $focused_widget ) {
174             my $next_focused_widget = $focused_widget->get_next_focused_widget();
175             defined $next_focused_widget
176             and $next_focused_widget->set_focus(1);
177             } else {
178             my $focused_window = $self->get_focused_window();
179             my $next_focused_widget = $focused_window->get_next_focused_widget();
180             defined $next_focused_widget
181             and $next_focused_widget->set_focus(1);
182             }
183             },
184             )
185             );
186              
187             # key listener for BACK TAB
188             $self->add_event_listener(
189             Curses::Toolkit::EventListener->new(
190             accepted_events => {
191             'Curses::Toolkit::Event::Key' => sub {
192             my ($event) = @_;
193             $event->{type} eq 'stroke' or return 0;
194             $event->{params}{key} eq 'KEY_BTAB' or return 0;
195             },
196             },
197             code => sub {
198              
199             # my $focused_widget = $self->get_focused_widget();
200             # if (defined $focused_widget) {
201             # my $prev_focused_widget = $focused_widget->get_prev_focused_widget();
202             # defined $prev_focused_widget and
203             # $prev_focused_widget->set_focus(1);
204             # } else {
205             # my $focused_window = $self->get_focused_window();
206             # my $prev_focused_widget = $focused_window->get_prev_focused_widget();
207             # defined $prev_focused_widget and
208             # $prev_focused_widget->set_focus(1);
209             # }
210             },
211             )
212             );
213              
214             return $self;
215             }
216              
217             sub get_default_theme_name {
218             my ($class) = @_;
219             return (
220             has_colors()
221             ? 'Curses::Toolkit::Theme::Default::Color::BlueWhite'
222             : 'Curses::Toolkit::Theme::Default'
223             );
224              
225             # 'Curses::Toolkit::Theme::Default::Color::Yellow'
226             # 'Curses::Toolkit::Theme::Default::Color::Pink'
227             }
228              
229              
230             # destroyer
231             DESTROY {
232             my ($obj) = @_;
233              
234             # ending Curses
235             ref($obj) eq 'Curses::Toolkit'
236             and Curses::endwin;
237             }
238              
239              
240              
241             sub get_theme_name {
242             my ($self) = @_;
243             return $self->{theme_name};
244             }
245              
246              
247              
248             sub add_event_listener {
249             my $self = shift;
250             my ($listener) = validate_pos( @_, { isa => 'Curses::Toolkit::EventListener' } );
251             push @{ $self->{event_listeners} }, $listener;
252             return $self;
253             }
254              
255              
256             sub get_event_listeners {
257             my ($self) = @_;
258             return @{ $self->{event_listeners} };
259             }
260              
261              
262             sub get_focused_widget {
263             my ($self) = @_;
264             my $window = $self->get_focused_window();
265             defined $window or return;
266             return $window->get_focused_widget();
267             }
268              
269              
270             sub get_focused_window {
271             my ($self) = @_;
272             my @windows = $self->get_windows();
273             @windows or return;
274             my $window =
275             ( sort { $b->get_property( window => 'stack' ) <=> $a->get_property( window => 'stack' ) } @windows )[0];
276             return $window;
277             }
278              
279              
280             # sub get_next_window {
281             # my ($self) = @_;
282             # my $iterator = $window->{window_iterator}
283             # or return;
284             # $iterator->next();
285             # my $sister_window = $iterator->value(); # might be undef
286             # $iterator->prev();
287             # defined $sister_window and return $sister_window;
288             # return;
289             # }
290              
291              
292             sub set_mainloop {
293             my $self = shift;
294             my ($mainloop) = validate_pos( @_, { optional => 0 } );
295             $self->{mainloop} = $mainloop;
296             return $self;
297             }
298              
299              
300             sub get_mainloop {
301             my ($self) = @_;
302             return $self->{mainloop};
303             }
304              
305              
306             sub get_shape {
307             my ($self) = @_;
308             return $self->{shape};
309             }
310              
311              
312             sub add_window {
313             my $self = shift;
314             my ($window) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget::Window' } );
315             $window->_set_curses_handler( $self->{curses_handler} );
316             $window->set_theme_name( $self->{theme_name} );
317             $window->set_root_window($self);
318             $self->bring_window_to_front($window);
319              
320             # in case the window has proportional coordinates depending on the root window
321             # TODO : do that only if window has proportional coordinates, not always
322             $window->rebuild_all_coordinates();
323             push @{ $self->{windows} }, $window;
324             $self->{window_iterator} ||= $self->{windows}->forward_from();
325             $self->needs_redraw();
326             return $self;
327             }
328              
329              
330             sub bring_window_to_front {
331             my $self = shift;
332             my ($window) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget::Window' } );
333             $self->{last_stack}++;
334             $window->set_property( window => 'stack', $self->{last_stack} );
335             my $last_stack = $self->{last_stack};
336             $last_stack % 5 == 0
337             and $self->{last_stack} = $self->_cleanup_windows_stacks();
338              
339             $self->needs_redraw();
340             return $self;
341             }
342              
343             sub _cleanup_windows_stacks {
344             my ($self) = @_;
345              
346             my @sorted_windows =
347             sort { $a->get_property( window => 'stack' ) <=> $b->get_property( window => 'stack' ) } $self->get_windows();
348              
349             foreach my $idx ( 0 .. @sorted_windows - 1 ) {
350             $sorted_windows[$idx]->set_property( window => 'stack', $idx );
351             }
352             return @sorted_windows - 1;
353             }
354              
355              
356             sub needs_redraw {
357             my ($self) = @_;
358             my $mainloop = $self->get_mainloop();
359             defined $mainloop or return $self;
360             $mainloop->needs_redraw();
361             return $self;
362             }
363              
364              
365             sub get_windows {
366             my ($self) = @_;
367             return @{ $self->{windows} };
368             }
369              
370              
371             sub set_modal_widget {
372             my $self = shift;
373             my ($widget) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget' } );
374             $self->{_modal_widget} = $widget;
375             return $self;
376             }
377              
378              
379             sub unset_modal_widget {
380             my $self = shift;
381             $self->{_modal_widget} = undef;
382             return;
383             }
384              
385              
386             sub get_modal_widget {
387             my ($self) = @_;
388              
389             my $modal_widget = $self->{_modal_widget};
390             defined $modal_widget or return;
391             return $modal_widget;
392             }
393              
394              
395             sub show_all {
396             my ($self) = @_;
397             foreach my $window ( $self->get_windows() ) {
398             $window->show_all();
399             }
400             return $self;
401             }
402              
403              
404              
405              
406             sub render {
407             my ($self) = @_;
408              
409             $self->{test_environment}
410             or $self->{curses_handler}->erase();
411              
412             if (!defined $self->{_root_theme}) {
413             $self->{_root_theme} = $self->get_theme_name->new(Curses::Toolkit::Widget::Window->new());
414             $self->{_root_theme}->_set_colors($self->{_root_theme}->ROOT_COLOR, $self->{_root_theme}->ROOT_COLOR);
415             }
416             my $root_theme = $self->{_root_theme};
417              
418             my $c = $self->{shape};
419             my $str = ' ' x ($c->get_x2() - $c->get_x1());
420             $self->{curses_handler}->attron($root_theme->_get_color_pair);
421             foreach my $y ( $c->get_y1() .. $c->get_y2() - 1 ) {
422             $self->{curses_handler}->addstr( $y, $c->get_x1(), $str );
423             }
424            
425             foreach my $window ( sort { $a->get_property( window => 'stack' ) <=> $b->get_property( window => 'stack' ) }
426             $self->get_windows() )
427             {
428             $window->render();
429             }
430             return $self;
431             }
432              
433              
434             sub display {
435             my ($self) = @_;
436             $self->{curses_handler}->refresh();
437             return $self;
438             }
439              
440              
441             sub dispatch_event {
442             my $self = shift;
443             my ( $event, $widget ) = validate_pos(
444             @_, { isa => 'Curses::Toolkit::Event' },
445             { isa => 'Curses::Toolkit::Widget', optional => 1 },
446             );
447              
448             if ( !defined $widget ) {
449             $widget = $self->get_modal_widget();
450             defined $widget and $self->unset_modal_widget();
451             }
452             $widget ||= $event->get_matching_widget();
453             defined $widget or return;
454              
455             while (1) {
456             foreach my $listener ( grep { $_->is_enabled() } $widget->get_event_listeners() ) {
457             if ( $listener->can_handle($event) ) {
458             $listener->send_event( $event, $widget );
459             $event->can_propagate()
460             or return 1;
461             }
462             }
463             $event->restricted_to_widget()
464             and return;
465             if ( $widget->isa('Curses::Toolkit::Widget::Window') ) {
466             $widget = $widget->get_root_window();
467             } elsif ( $widget->isa('Curses::Toolkit::Widget') ) {
468             $widget = $widget->get_parent();
469             } else {
470             return;
471             }
472             defined $widget or return;
473             }
474             return;
475             }
476              
477              
478             sub fire_event {
479             my $self = shift;
480             my ( $event, $widget ) = validate_pos(
481             @_, { isa => 'Curses::Toolkit::Event' },
482             { isa => 'Curses::Toolkit::Widget', optional => 1 },
483             );
484             my $mainloop = $self->get_mainloop();
485             defined $mainloop or return $self;
486             $mainloop->stack_event( $event, $widget );
487             return $self;
488             }
489              
490              
491             sub add_delay {
492             my $self = shift;
493             my $mainloop = $self->get_mainloop();
494             defined $mainloop or return;
495             $mainloop->add_delay(@_);
496             return;
497             }
498              
499             # ## Private methods ##
500              
501             # # event_handling
502              
503             # my @supported_events = (qw(Curses::Toolkit::Event::Shape));
504             # sub _handle_event {
505             # my ($self, $event) = @_;
506             # use List::MoreUtils qw(any);
507             # if ( any { $event->isa($_) } @supported_events ) {
508             # my $method_name = '_event_' . lc( (split('::|_', ref($event)))[-1] ) . '_' . $event->get_type();
509             # if ($self->can($method_name)) {
510             # return $self->$method_name();
511             # }
512             # }
513             # # event failed being applied
514             # return 0;
515             # }
516              
517             # core event handling for Curses::Toolkit::Event::Shape event of type 'change'
518             sub _event_shape_change {
519             my ($self) = @_;
520              
521             my ( $screen_h, $screen_w );
522             $self->_recompute_shape();
523              
524             # for now we rebuild all coordinates
525             foreach my $window ( $self->get_windows() ) {
526             $window->rebuild_all_coordinates();
527             }
528              
529             # for now rebuild everything
530             # my $mainloop = $self->get_mainloop();
531             # if (defined $mainloop) {
532             # $mainloop->needs_redraw();
533             # }
534              
535             # event suceeded
536             return 1;
537              
538             }
539              
540             sub _recompute_shape {
541             my ($self) = @_;
542             use Curses::Toolkit::Object::Coordinates;
543             my ( $screen_h, $screen_w );
544             use Curses;
545             if ($self->{test_environment}) {
546             $screen_h = $self->{test_environment}->{screen_h};
547             $screen_w = $self->{test_environment}->{screen_w};
548             } else {
549             endwin;
550             $self->{curses_handler}->getmaxyx( $screen_h, $screen_w );
551             }
552             use Curses::Toolkit::Object::Shape;
553             $self->{shape} ||= Curses::Toolkit::Object::Shape->new_zero();
554             $self->{shape}->_set(
555             x2 => $screen_w,
556             y2 => $screen_h,
557             );
558             return $self;
559             }
560              
561             sub _rebuild_all {
562             my ($self) = @_;
563             foreach my $window ( $self->get_windows() ) {
564             $window->rebuild_all_coordinates();
565             }
566             return $self;
567             }
568              
569             1;
570              
571             __END__