#
# This file is part of Curses-Toolkit
#
# This software is copyright (c) 2011 by Damien "dams" Krotkine.
#
# This is free software; you can redistribute it and/or modify it under
# the same terms as the Perl 5 programming language system itself.
#
use warnings;
use strict;
package Curses::Toolkit;
{
$Curses::Toolkit::VERSION = '0.208';
}
# ABSTRACT: a modern Curses toolkit
use Params::Validate qw(SCALAR ARRAYREF HASHREF CODEREF GLOB GLOBREF SCALARREF HANDLE BOOLEAN UNDEF validate validate_pos);
use Curses::Toolkit::Theme;
sub init_root_window {
my $class = shift;
my %params = validate(
@_,
{ theme_name => {
type => SCALAR,
optional => 1,
},
mainloop => { optional => 1 },
quit_key => {
type => SCALAR,
default => 'q',
},
switch_key => {
type => SCALAR,
default => 'r',
},
test_environment => {
type => HASHREF,
optional => 1,
},
}
);
# get the Curses handler
use Curses;
my $curses_handler = Curses->new();
# already done ?
# raw();
# cbreak();
# noecho();
# $curses_handler->keypad(1);
if (has_colors) {
start_color();
}
eval { Curses->can('NCURSES_MOUSE_VERSION') && ( NCURSES_MOUSE_VERSION() >= 1 ) };
my $old_mouse_mask;
my $mouse_mask = mousemask( ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, $old_mouse_mask );
use Curses::Toolkit::Theme::Default;
use Curses::Toolkit::Theme::Default::Color::Yellow;
use Curses::Toolkit::Theme::Default::Color::Pink;
use Curses::Toolkit::Theme::Default::Color::BlueWhite;
use Tie::Array::Iterable;
$params{theme_name} ||= Curses::Toolkit->get_default_theme_name();
my @windows = ();
my $self = bless {
initialized => 1,
curses_handler => $curses_handler,
windows => Tie::Array::Iterable->new(@windows),
theme_name => $params{theme_name},
mainloop => $params{mainloop},
last_stack => 0,
event_listeners => [],
window_iterator => undef,
test_environment => $params{test_environment},
}, $class;
$self->_recompute_shape();
use Curses::Toolkit::EventListener;
# add a default listener that listen to any Shape event
$self->add_event_listener(
Curses::Toolkit::EventListener->new(
accepted_events => {
'Curses::Toolkit::Event::Shape' => sub { 1; },
},
code => sub {
my ( $screen_h, $screen_w );
$self->_recompute_shape();
# for now we rebuild all coordinates
foreach my $window ( $self->get_windows() ) {
$window->rebuild_all_coordinates();
}
},
)
);
if ( defined $params{quit_key} ) {
$self->add_event_listener(
Curses::Toolkit::EventListener->new(
accepted_events => {
'Curses::Toolkit::Event::Key' => sub {
my ($event) = @_;
$event->{type} eq 'stroke' or return 0;
lc $event->{params}{key} eq $params{quit_key} or return 0;
},
},
code => sub {
exit;
},
)
);
}
if ( defined $params{switch_key} ) {
$self->add_event_listener(
Curses::Toolkit::EventListener->new(
accepted_events => {
'Curses::Toolkit::Event::Key' => sub {
my ($event) = @_;
$event->{type} eq 'stroke' or return 0;
lc $event->{params}{key} eq $params{switch_key} or return 0;
},
},
code => sub {
my ( $event, $widget ) = @_;
defined $self->{window_iterator}
or return;
my $window = $widget->{window_iterator}->next();
if ( !defined $window ) {
$widget->{window_iterator}->to_start();
$window = $widget->{window_iterator}->value();
}
# get the currently focused widget, unfocus it
my $current_focused_widget = $self->get_focused_widget();
if ( defined $current_focused_widget && $current_focused_widget->can('set_focus') ) {
$current_focused_widget->set_focus(0);
}
$window->bring_to_front();
# focus the window or one of its component
my $next_focused_widget =
$window->get_next_focused_widget(1); # 1 means "consider if $window is focusable"
defined $next_focused_widget
and $next_focused_widget->set_focus(1);
},
)
);
}
# key listener for TAB
$self->add_event_listener(
Curses::Toolkit::EventListener->new(
accepted_events => {
'Curses::Toolkit::Event::Key' => sub {
my ($event) = @_;
$event->{type} eq 'stroke' or return 0;
$event->{params}{key} eq '<^I>' or return 0;
},
},
code => sub {
my $focused_widget = $self->get_focused_widget();
if ( defined $focused_widget ) {
my $next_focused_widget = $focused_widget->get_next_focused_widget();
defined $next_focused_widget
and $next_focused_widget->set_focus(1);
} else {
my $focused_window = $self->get_focused_window();
my $next_focused_widget = $focused_window->get_next_focused_widget();
defined $next_focused_widget
and $next_focused_widget->set_focus(1);
}
},
)
);
# key listener for BACK TAB
$self->add_event_listener(
Curses::Toolkit::EventListener->new(
accepted_events => {
'Curses::Toolkit::Event::Key' => sub {
my ($event) = @_;
$event->{type} eq 'stroke' or return 0;
$event->{params}{key} eq 'KEY_BTAB' or return 0;
},
},
code => sub {
# my $focused_widget = $self->get_focused_widget();
# if (defined $focused_widget) {
# my $prev_focused_widget = $focused_widget->get_prev_focused_widget();
# defined $prev_focused_widget and
# $prev_focused_widget->set_focus(1);
# } else {
# my $focused_window = $self->get_focused_window();
# my $prev_focused_widget = $focused_window->get_prev_focused_widget();
# defined $prev_focused_widget and
# $prev_focused_widget->set_focus(1);
# }
},
)
);
return $self;
}
sub get_default_theme_name {
my ($class) = @_;
return (
has_colors()
? 'Curses::Toolkit::Theme::Default::Color::BlueWhite'
: 'Curses::Toolkit::Theme::Default'
);
# 'Curses::Toolkit::Theme::Default::Color::Yellow'
# 'Curses::Toolkit::Theme::Default::Color::Pink'
}
# destroyer
DESTROY {
my ($obj) = @_;
# ending Curses
ref($obj) eq 'Curses::Toolkit'
and Curses::endwin;
}
sub get_theme_name {
my ($self) = @_;
return $self->{theme_name};
}
sub add_event_listener {
my $self = shift;
my ($listener) = validate_pos( @_, { isa => 'Curses::Toolkit::EventListener' } );
push @{ $self->{event_listeners} }, $listener;
return $self;
}
sub get_event_listeners {
my ($self) = @_;
return @{ $self->{event_listeners} };
}
sub get_focused_widget {
my ($self) = @_;
my $window = $self->get_focused_window();
defined $window or return;
return $window->get_focused_widget();
}
sub get_focused_window {
my ($self) = @_;
my @windows = $self->get_windows();
@windows or return;
my $window =
( sort { $b->get_property( window => 'stack' ) <=> $a->get_property( window => 'stack' ) } @windows )[0];
return $window;
}
# sub get_next_window {
# my ($self) = @_;
# my $iterator = $window->{window_iterator}
# or return;
# $iterator->next();
# my $sister_window = $iterator->value(); # might be undef
# $iterator->prev();
# defined $sister_window and return $sister_window;
# return;
# }
sub set_mainloop {
my $self = shift;
my ($mainloop) = validate_pos( @_, { optional => 0 } );
$self->{mainloop} = $mainloop;
return $self;
}
sub get_mainloop {
my ($self) = @_;
return $self->{mainloop};
}
sub get_shape {
my ($self) = @_;
return $self->{shape};
}
sub add_window {
my $self = shift;
my ($window) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget::Window' } );
$window->_set_curses_handler( $self->{curses_handler} );
$window->set_theme_name( $self->{theme_name} );
$window->set_root_window($self);
$self->bring_window_to_front($window);
# in case the window has proportional coordinates depending on the root window
# TODO : do that only if window has proportional coordinates, not always
$window->rebuild_all_coordinates();
push @{ $self->{windows} }, $window;
$self->{window_iterator} ||= $self->{windows}->forward_from();
$self->needs_redraw();
return $self;
}
sub bring_window_to_front {
my $self = shift;
my ($window) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget::Window' } );
$self->{last_stack}++;
$window->set_property( window => 'stack', $self->{last_stack} );
my $last_stack = $self->{last_stack};
$last_stack % 5 == 0
and $self->{last_stack} = $self->_cleanup_windows_stacks();
$self->needs_redraw();
return $self;
}
sub _cleanup_windows_stacks {
my ($self) = @_;
my @sorted_windows =
sort { $a->get_property( window => 'stack' ) <=> $b->get_property( window => 'stack' ) } $self->get_windows();
foreach my $idx ( 0 .. @sorted_windows - 1 ) {
$sorted_windows[$idx]->set_property( window => 'stack', $idx );
}
return @sorted_windows - 1;
}
sub needs_redraw {
my ($self) = @_;
my $mainloop = $self->get_mainloop();
defined $mainloop or return $self;
$mainloop->needs_redraw();
return $self;
}
sub get_windows {
my ($self) = @_;
return @{ $self->{windows} };
}
sub set_modal_widget {
my $self = shift;
my ($widget) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget' } );
$self->{_modal_widget} = $widget;
return $self;
}
sub unset_modal_widget {
my $self = shift;
$self->{_modal_widget} = undef;
return;
}
sub get_modal_widget {
my ($self) = @_;
my $modal_widget = $self->{_modal_widget};
defined $modal_widget or return;
return $modal_widget;
}
sub show_all {
my ($self) = @_;
foreach my $window ( $self->get_windows() ) {
$window->show_all();
}
return $self;
}
sub render {
my ($self) = @_;
$self->{test_environment}
or $self->{curses_handler}->erase();
if (!defined $self->{_root_theme}) {
$self->{_root_theme} = $self->get_theme_name->new(Curses::Toolkit::Widget::Window->new());
$self->{_root_theme}->_set_colors($self->{_root_theme}->ROOT_COLOR, $self->{_root_theme}->ROOT_COLOR);
}
my $root_theme = $self->{_root_theme};
my $c = $self->{shape};
my $str = ' ' x ($c->get_x2() - $c->get_x1());
$self->{curses_handler}->attron($root_theme->_get_color_pair);
foreach my $y ( $c->get_y1() .. $c->get_y2() - 1 ) {
$self->{curses_handler}->addstr( $y, $c->get_x1(), $str );
}
foreach my $window ( sort { $a->get_property( window => 'stack' ) <=> $b->get_property( window => 'stack' ) }
$self->get_windows() )
{
$window->render();
}
return $self;
}
sub display {
my ($self) = @_;
$self->{curses_handler}->refresh();
return $self;
}
sub dispatch_event {
my $self = shift;
my ( $event, $widget ) = validate_pos(
@_, { isa => 'Curses::Toolkit::Event' },
{ isa => 'Curses::Toolkit::Widget', optional => 1 },
);
if ( !defined $widget ) {
$widget = $self->get_modal_widget();
defined $widget and $self->unset_modal_widget();
}
$widget ||= $event->get_matching_widget();
defined $widget or return;
while (1) {
foreach my $listener ( grep { $_->is_enabled() } $widget->get_event_listeners() ) {
if ( $listener->can_handle($event) ) {
$listener->send_event( $event, $widget );
$event->can_propagate()
or return 1;
}
}
$event->restricted_to_widget()
and return;
if ( $widget->isa('Curses::Toolkit::Widget::Window') ) {
$widget = $widget->get_root_window();
} elsif ( $widget->isa('Curses::Toolkit::Widget') ) {
$widget = $widget->get_parent();
} else {
return;
}
defined $widget or return;
}
return;
}
sub fire_event {
my $self = shift;
my ( $event, $widget ) = validate_pos(
@_, { isa => 'Curses::Toolkit::Event' },
{ isa => 'Curses::Toolkit::Widget', optional => 1 },
);
my $mainloop = $self->get_mainloop();
defined $mainloop or return $self;
$mainloop->stack_event( $event, $widget );
return $self;
}
sub add_delay {
my $self = shift;
my $mainloop = $self->get_mainloop();
defined $mainloop or return;
$mainloop->add_delay(@_);
return;
}
# ## Private methods ##
# # event_handling
# my @supported_events = (qw(Curses::Toolkit::Event::Shape));
# sub _handle_event {
# my ($self, $event) = @_;
# use List::MoreUtils qw(any);
# if ( any { $event->isa($_) } @supported_events ) {
# my $method_name = '_event_' . lc( (split('::|_', ref($event)))[-1] ) . '_' . $event->get_type();
# if ($self->can($method_name)) {
# return $self->$method_name();
# }
# }
# # event failed being applied
# return 0;
# }
# core event handling for Curses::Toolkit::Event::Shape event of type 'change'
sub _event_shape_change {
my ($self) = @_;
my ( $screen_h, $screen_w );
$self->_recompute_shape();
# for now we rebuild all coordinates
foreach my $window ( $self->get_windows() ) {
$window->rebuild_all_coordinates();
}
# for now rebuild everything
# my $mainloop = $self->get_mainloop();
# if (defined $mainloop) {
# $mainloop->needs_redraw();
# }
# event suceeded
return 1;
}
sub _recompute_shape {
my ($self) = @_;
use Curses::Toolkit::Object::Coordinates;
my ( $screen_h, $screen_w );
use Curses;
if ($self->{test_environment}) {
$screen_h = $self->{test_environment}->{screen_h};
$screen_w = $self->{test_environment}->{screen_w};
} else {
endwin;
$self->{curses_handler}->getmaxyx( $screen_h, $screen_w );
}
use Curses::Toolkit::Object::Shape;
$self->{shape} ||= Curses::Toolkit::Object::Shape->new_zero();
$self->{shape}->_set(
x2 => $screen_w,
y2 => $screen_h,
);
return $self;
}
sub _rebuild_all {
my ($self) = @_;
foreach my $window ( $self->get_windows() ) {
$window->rebuild_all_coordinates();
}
return $self;
}
1;
__END__
=pod
=head1 NAME
Curses::Toolkit - a modern Curses toolkit
=head1 VERSION
version 0.208
=head1 SYNOPSIS
use POE::Component::Curses;
use Curses::Toolkit::Widget::Window;
use Curses::Toolkit::Widget::Button;
# spawn a root window
my $root = POE::Component::Curses->spawn();
# adds some widget
$root->add_window(
my $window = Curses::Toolkit::Widget::Window
->new()
->set_name('main_window')
->add_widget(
my $button = Curses::Toolkit::Widget::Button
->new_with_label('Click Me to quit')
->set_name('my_button')
->signal_connect(clicked => sub { exit(0); })
)
->set_coordinates( x1 => '20%', y1 => '20%',
x2 => '80%', y2 => '80%', )
);
POE::Kernel->run();
=head1 DESCRIPTION
This module tries to be a modern curses toolkit, based on the Curses module, to
build "semi-graphical" user interfaces easily.
L<Curses::Toolkit> is meant to be used with a mainloop, which is not part of this
module. I recommend you L<POE::Component::Curses>, which is probably what you
want. L<POE::Component::Curses> uses Curses::Toolkit, but provides a mainloop
and handles keyboard, mouse, timer and other events, whereas Curses::Toolkit is
just the drawing library. See the example above. the C<spawn> method returns a
L<Curses::Toolkit> object, which you can call methods on.
If you already have a mainloop or if you don't need it, you might want
to use Curses::Toolkit directly. But again, it's probably not what you want to
use. In this case you would do something like :
use Curses::Toolkit;
# using Curses::Toolkit without any event loop
my $root = Curses::Toolkit->init_root_window();
my $window = Curses::Toolkit::Widget::Window->new();
$root->add($window);
...
$root->render
=head1 TUTORIAL
If you are new with C<Curses::Toolkit>, I suggest you go through the tutorial. You can find it here:
L<Curses::Toolkit::Tutorial> (not yet done!)
=head1 WIDGETS
Curses::Toolkit is based on a widget model, inspired by Gtk. I suggest you read
the POD of the following widgets :
=over
=item L<Curses::Toolkit::Widget::Window>
Use this widget to create a window. It's the first thing to do once you have a root_window
=item L<Curses::Toolkit::Widget>
Useful to read, it contains the common methods of all the widgets
=item L<Curses::Toolkit::Widget::Label>
To display simple text, with text colors and attributes
=item L<Curses::Toolkit::Widget::Button>
Simple text button widget to interact with the user
=item L<Curses::Toolkit::Widget::GenericButton>
A button widget that can contain anything, not just a label
=item L<Curses::Toolkit::Widget::Entry>
To input text from the user
=item L<Curses::Toolkit::Widget::VBox>
To pack widgets vertically, thus building complex layouts
=item L<Curses::Toolkit::Widget::HBox>
To pack widgets horizontally, thus building complex layouts
=item L<Curses::Toolkit::Widget::Border>
Add a simple border around any widget
=item L<Curses::Toolkit::Widget::HPaned>
To pack 2 widgets horizontally with a flexible gutter
=item L<Curses::Toolkit::Widget::VPaned>
To pack 2 widgets vertically with a flexible gutter
=item L<Curses::Toolkit::Widget::HScrollBar>
Not yet implemented
=item L<Curses::Toolkit::Widget::VScrollBar.pm>
Not yet implemented
=item L<Curses::Toolkit::Widget::HProgressBar>
An horizontal progress bar widget
=item L<Curses::Toolkit::Widget::HProgressBar>
A vertical progress bar widget
=back
For reference, here are the various hierarchy of the objects/concepts of the
toolkit you might have to use :
=head1 WIDGETS HIERARCHY
This is the inheritance hierarchy of the widgets of the toolkit :
Curses::Toolkit::Widget
|
+-- Curses::Toolkit::Widget::Window
| |
| +-- Curses::Toolkit::Widget::Window::Dialog
| |
| + Curses::Toolkit::Widget::Window::Dialog::About
|
+-- Curses::Toolkit::Widget::Label
|
+-- Curses::Toolkit::Widget::Entry
|
+-- Curses::Toolkit::Widget::Scrollbar
| |
| +-- Curses::Toolkit::Widget::HScrollbar
| |
| +-- Curses::Toolkit::Widget::VScrollbar
|
+-- Curses::Toolkit::Widget::Container
|
+-- Curses::Toolkit::Widget::HBox
|
+-- Curses::Toolkit::Widget::VBox
|
+-- Curses::Toolkit::Widget::Paned
| |
| +-- Curses::Toolkit::Widget::HPaned
| |
| +-- Curses::Toolkit::Widget::VPaned
|
+-- Curses::Toolkit::Widget::Bin
|
+-- Curses::Toolkit::Widget::Border
|
+-- Curses::Toolkit::Widget::Button
|
+-- Curses::Toolkit::Widget::GenericButton
|
+-- Curses::Toolkit::Widget::ProgressBar
|
+-- Curses::Toolkit::Widget::HProgressBar
|
+-- Curses::Toolkit::Widget::VProgressBar
=head1 SIGNALS HIERARCHY
This is the inheritance hierarchy of the signals :
Curses::Toolkit::Signal
|
+-- Curses::Toolkit::Signal::Clicked
|
+-- Curses::Toolkit::Signal::Content
| |
| +-- Curses::Toolkit::Signal::Content::Changed
|
+-- Curses::Toolkit::Signal::Focused
|
+-- Curses::Toolkit::Signal::Focused::In
|
+-- Curses::Toolkit::Signal::Focused::Out
=head1 THEMES HIERARCHY
This is the inheritance hierarchy of the themes :
Curses::Toolkit::Theme
|
+-- Curses::Toolkit::Theme::Default
|
+-- Curses::Toolkit::Theme::Default::Color
|
+-- Curses::Toolkit::Theme::Default::Color::Pink
|
+-- Curses::Toolkit::Theme::Default::Color::Yellow
=head1 OBJECTS HIERARCHY
This is the list of objects of the toolkit :
Curses::Toolkit::Object
|
+-- Curses::Toolkit::Object::Coordinates
|
+-- Curses::Toolkit::Object::MarkupString
|
+-- Curses::Toolkit::Object::Shape
=head1 ROLES HIERARCHY
For now there is only one role
Curses::Toolkit::Role::Focusable
=head1 TYPES HIERARCHY
For now there is only one types class :
Curses::Toolkit::Types
=head1 CLASS METHODS
=head2 init_root_window
my $root = Curses::Toolkit->init_root_window();
Initialize the Curses environment, and return an object representing it. This
is not really a constructor, because you can't have more than one
Curses::Toolkit object for one Curses environment. Think of it more like a
service.
input : theme_name : optional, the name of the theme to use as default display theme
mainloop : optional, the mainloop object that will be used for event handling
quit_key : the key used to quit the whole application. Default to 'q'. If set to undef, it's disabled
switch_key : the key used to switch between windows. Default to 'r'. If set to undef, it's disabled
test_environment : optional, a hashref, if set, Curses::Toolkit will be in test mode
output : a Curses::Toolkit object
=head1 METHODS
=head2 get_theme_name
my $theme_name = $root_window->get_theme_name();
Return the theme associated with the root window. Typically used to get a
usable default theme name. Use that instead of hard-coding
'Curses::Toolkit::Theme::Default'
=head2 add_event_listener
$root->add_event_listener($event_listener);
Adds an event listener to the root window. That allows the root window to
respond to some events
input : a Curses::Toolkit::EventListener
output : the root window
=head2 get_event_listeners
my @listeners = $root->get_event_listener();
Returns the list of listeners connected to the root window.
input : none
output : an ARRAY of Curses::Toolkit::EventListener
=head2 get_focused_widget
my $widget = $root->get_focused_widget();
Returns the widget currently focused. Warning, the returned widget could well
be a window.
input : none
output : a Curses::Toolkit::Widget or void
=head2 get_focused_window
my $window = $root->get_focused_window();
Returns the window currently focused.
input : none
output : a Curses::Toolkit::Widget::Window or void
=head2 get_focused_window
my $window = $root->get_nexd_window();
Returns the next window.
input : none
output : a Curses::Toolkit::Widget::Window or void
=head2 set_mainloop
$root->set_mainloop($mainloop)
Sets the mainloop object to be used by the Curses::Toolkit root object. The
mainloop object will be called when a new event has to be registered. The
mainloop object is in charge to listen to the events and call $root->dispatch_event()
input : a mainloop object
output : the Curses::Toolkit object
=head2 get_mainloop
my $mainloop = $root->get_mainloop()
Return the mainloop object associated to the root object. Might be undef if no
mainloop were associated.
input : none
output : the mainloop object, or undef
=head2 get_shape
my $coordinate = $root->get_shape();
Returns a coordinate object that represents the size of the root window.
input : none
output : a Curses::Toolkit::Object::Coordinates object
=head2 add_window
my $window = Curses::Toolkit::Widget::Window->new();
$root->add_window($window);
Adds a window on the root window. Returns the root window
input : a Curses::Toolkit::Widget::Window object
output : the root window
=head2 bring_window_to_front()
$root_window->bring_window_to_front($window)
Brings the window to front
input : a Curses::Toolkit::Widget::Window
output : the root window
=head2 needs_redraw
$root->needs_redraw()
When called, signify to the root window that a redraw is needed. Has an effect
only if a mainloop is active ( see POE::Component::Curses )
input : none
output : the root window
=head2 get_windows
my @windows = $root->get_windows();
Returns the list of windows loaded
input : none
output : ARRAY of Curses::Toolkit::Widget::Window
=head2 set_modal_widget
Set a widget to be modal
input : a widget
output : the root window
=head2 unset_modal_widget
Unset the widget to be modal
input : none
output : the root window
=head2 get_modal_widget
returns the modal widget, or void
input : none
output : the modal widget or void
=head2 show_all
$root->show_all();
Set visibility property to true for every element. Returns the root windows
input : none
output : the root window
=head2 render
$root->render();
Build everything in the buffer. You need to call 'display' after that to display it
input : none
output : the root window
=head2 display
$root->display();
Refresh the screen.
input : none
output : the root window
=head2 dispatch_event
my $event = Curses::Toolkit::Event::SomeEvent->new(...)
$root->dispatch_event($event);
Given an event, dispatch it to the appropriate widgets / windows, or to the
root window. You probably don't want to use this method directly. Use Signals instead.
input : a Curses::Toolkit::Event
optional, a widget. if given, the event dispatching will start with this wisget (and not the focused one)
output : true if the event were handled, false if not
=head2 fire_event
$widget->fire_event($event, $widget);
Sends an event to the mainloop so it gets dispatched. You probably don't want
to use this method.
input : a Curses::Toolkit::Event
optional, a widget. if given, the event dispatching will start with this widget (and not the focused one)
output : the root_window
=head2 add_delay
Has an effect only if a mainloop is active ( see POE::Component::Curses )
$root_window->add_delay($seconds, \&code, @args)
$root_window->add_delay(5, sub { print "wow, 5 seconds wasted, dear $_[0]\n"; }, $name);
Add a timer that will execute the \&code once, in $seconds seconds. $seconds
can be a fraction. @args will be passed to the code reference
input : number of seconds
a code reference
an optional list of arguments to be passed to the code reference
output : a timer unique identifier or void
=head1 BUGS
Please report any bugs or feature requests to
C<bug-curses-toolkit at rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Curses-Toolkit>.
I will be notified, and then you'll automatically be notified of progress on
your bug as I make changes.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc Curses::Toolkit
You can also look for information at:
=over 4
=item * AnnoCPAN: Annotated CPAN documentation
L<http://annocpan.org/dist/Curses-Toolkit>
=item * CPAN Ratings
L<http://cpanratings.perl.org/d/Curses-Toolkit>
=item * RT: CPAN's request tracker
L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Curses-Toolkit>
=item * Search CPAN
L<http://search.cpan.org/dist/Curses-Toolkit>
=back
=head1 AUTHOR
Damien "dams" Krotkine
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2011 by Damien "dams" Krotkine.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut