| 1 | # Copyright 2003 by Scott McCoy. All rights reserved. Released under |
|---|
| 2 | # the same terms as Perl itself. |
|---|
| 3 | # |
|---|
| 4 | # Portions Copyright 2003 by Rocco Caputo. All rights reserved. Released |
|---|
| 5 | # under the same terms as Perl itself. |
|---|
| 6 | # |
|---|
| 7 | # Portions Copyright 2001-2003 by Maurice Makaay and/or Marcus |
|---|
| 8 | # Thiesen. Released under the same terms as Perl itself. |
|---|
| 9 | |
|---|
| 10 | # Good luck. Send the author feedback. Thanks for trying it. :) |
|---|
| 11 | package Curses::UI::POE; |
|---|
| 12 | |
|---|
| 13 | use warnings FATAL => "all"; |
|---|
| 14 | use strict; |
|---|
| 15 | |
|---|
| 16 | use POE; |
|---|
| 17 | use POSIX qw( fcntl_h ); |
|---|
| 18 | use base qw( Curses::UI ); |
|---|
| 19 | use Curses::UI::Widget; |
|---|
| 20 | |
|---|
| 21 | # Force POE::Kernel to have ran...stops my warnings... |
|---|
| 22 | # We do it in a BEGIN so there can be no sessions prior |
|---|
| 23 | # to our calling this unless somebody is being really, really bad. |
|---|
| 24 | BEGIN { run POE::Kernel } |
|---|
| 25 | |
|---|
| 26 | *VERSION = \0.040; |
|---|
| 27 | our $VERSION; |
|---|
| 28 | |
|---|
| 29 | use constant TOP => -1; |
|---|
| 30 | |
|---|
| 31 | sub import { |
|---|
| 32 | my $caller = caller; |
|---|
| 33 | |
|---|
| 34 | no strict "refs"; |
|---|
| 35 | |
|---|
| 36 | *{ $caller . "::MainLoop" } = \&MainLoop; |
|---|
| 37 | eval "package $caller; use POE;"; |
|---|
| 38 | } |
|---|
| 39 | |
|---|
| 40 | # XXX We assume that there will never be two Curses::UI::POE sessions. |
|---|
| 41 | my @modal_objects; |
|---|
| 42 | my @modal_callbacks; |
|---|
| 43 | |
|---|
| 44 | # The session needed to make curses run in POE. |
|---|
| 45 | sub new { |
|---|
| 46 | my ($type, %options) = @_; |
|---|
| 47 | my $self = &Curses::UI::new(@_); |
|---|
| 48 | # my $self = bless Curses::UI->new, $type; |
|---|
| 49 | # my $self = bless &Curses::UI::new(@_), $type; |
|---|
| 50 | |
|---|
| 51 | # I have to do this here, because if our first order of business is a |
|---|
| 52 | # dialog then the _start event will be too late. This self reference is |
|---|
| 53 | # just so we can stack and peel onto the list of modal objects, and get to |
|---|
| 54 | # ourselves when we reach the top. |
|---|
| 55 | push @modal_objects, $self; |
|---|
| 56 | |
|---|
| 57 | $self->{options} = \%options; |
|---|
| 58 | $self->{__start_callback} = delete $options{inline_states}{_start}; |
|---|
| 59 | |
|---|
| 60 | # Default so we don't get a warning about using undef |
|---|
| 61 | $options{package_states} ||= []; |
|---|
| 62 | $options{object_states} ||= []; |
|---|
| 63 | $options{inline_states} ||= {}; |
|---|
| 64 | $options{options} ||= {}; |
|---|
| 65 | |
|---|
| 66 | POE::Session->create |
|---|
| 67 | ( options => $options{options}, |
|---|
| 68 | args => $options{args}, |
|---|
| 69 | inline_states => $options{inline_states}, |
|---|
| 70 | package_states => $options{package_states}, |
|---|
| 71 | |
|---|
| 72 | object_states => [ |
|---|
| 73 | @{ $options{object_states} }, |
|---|
| 74 | $self, [ qw( _start init keyin timer shutdown ) ] |
|---|
| 75 | ], |
|---|
| 76 | |
|---|
| 77 | # This is to maintain backward compatibility. |
|---|
| 78 | heap => $self ); |
|---|
| 79 | |
|---|
| 80 | # Copy the no-output option |
|---|
| 81 | $self->{-no_output} = $options{-no_output} || 0; |
|---|
| 82 | |
|---|
| 83 | return $self; |
|---|
| 84 | } |
|---|
| 85 | |
|---|
| 86 | # Wait until the kernel actually starts before we muck with things. |
|---|
| 87 | sub _start { $_[KERNEL]->yield("init") } |
|---|
| 88 | |
|---|
| 89 | sub init { |
|---|
| 90 | my ($self, $kernel) = @_[ OBJECT, KERNEL ]; |
|---|
| 91 | |
|---|
| 92 | $kernel->select(\*STDIN, "keyin"); |
|---|
| 93 | |
|---|
| 94 | # Turn blocking back on for STDIN. Some Curses |
|---|
| 95 | # implementations don't deal well with non-blocking STDIN. |
|---|
| 96 | my $flags = fcntl STDIN, F_GETFL, 0 or die $!; |
|---|
| 97 | fcntl STDIN, F_SETFL, $flags & ~O_NONBLOCK or die $!; |
|---|
| 98 | |
|---|
| 99 | # If we're in a dialog, then the TOP modal object is more appropriate than |
|---|
| 100 | # $self, although if we're not in a dialog $self is what this actually is. |
|---|
| 101 | set_read_timeout($modal_objects[TOP]); |
|---|
| 102 | |
|---|
| 103 | # When gpm_mouse isn't enabled, sometimes there is extra garbage during |
|---|
| 104 | # startup. We ignore that garbage during construction, assuming that since |
|---|
| 105 | # the UI isn't rendered yet (we're still creating the root object!) the |
|---|
| 106 | # input must not matter. |
|---|
| 107 | $self->flushkeys; |
|---|
| 108 | |
|---|
| 109 | # Unmask... |
|---|
| 110 | $self->{__start_callback}(@_) |
|---|
| 111 | if defined $self->{__start_callback}; |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | sub _clear_modal_callback { |
|---|
| 115 | my ($self) = @_; |
|---|
| 116 | |
|---|
| 117 | my $top = pop @modal_objects; |
|---|
| 118 | |
|---|
| 119 | # Reset focus |
|---|
| 120 | $top->{-focus} = 0; |
|---|
| 121 | |
|---|
| 122 | # Dispatch callback. |
|---|
| 123 | my $args = pop @modal_callbacks; |
|---|
| 124 | my $sub = shift @$args; |
|---|
| 125 | &{$sub}(@$args); |
|---|
| 126 | } |
|---|
| 127 | |
|---|
| 128 | sub keyin { |
|---|
| 129 | my ($self, $kernel) = @_[ OBJECT, KERNEL ]; |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | until ((my $key = $self->get_key(0)) eq -1) { |
|---|
| 133 | $self->feedkey($key); |
|---|
| 134 | |
|---|
| 135 | unless ($#modal_objects) { |
|---|
| 136 | $self->do_one_event; |
|---|
| 137 | } |
|---|
| 138 | else { |
|---|
| 139 | # dispatch the event to the top-most modal object, or the root. |
|---|
| 140 | $self->do_one_event($modal_objects[TOP]); |
|---|
| 141 | } |
|---|
| 142 | } |
|---|
| 143 | |
|---|
| 144 | # Set the root cursor mode |
|---|
| 145 | unless ($self->{-no_output}) { |
|---|
| 146 | Curses::curs_set($self->{-cursor_mode}); |
|---|
| 147 | } |
|---|
| 148 | } |
|---|
| 149 | |
|---|
| 150 | sub timer { |
|---|
| 151 | my ($self) = @_; |
|---|
| 152 | |
|---|
| 153 | # dispatch the event to the top-most modal object, or the root. |
|---|
| 154 | my $top_object = $modal_objects[TOP]; |
|---|
| 155 | |
|---|
| 156 | $top_object->do_timer; |
|---|
| 157 | |
|---|
| 158 | # Set the root cursor mode. |
|---|
| 159 | unless ($self->{-no_output}) { |
|---|
| 160 | Curses::curs_set($self->{-cursor_mode}); |
|---|
| 161 | } |
|---|
| 162 | |
|---|
| 163 | set_read_timeout($top_object); |
|---|
| 164 | } |
|---|
| 165 | |
|---|
| 166 | sub shutdown { |
|---|
| 167 | my ($kernel) = $_[ KERNEL ]; |
|---|
| 168 | |
|---|
| 169 | # Unselect stdin |
|---|
| 170 | $kernel->select(\*STDIN); |
|---|
| 171 | } |
|---|
| 172 | |
|---|
| 173 | sub mainloop { |
|---|
| 174 | my ($this) = @_; |
|---|
| 175 | |
|---|
| 176 | unless ($this->{-no_output}) { |
|---|
| 177 | $this->focus(undef, 1); |
|---|
| 178 | $this->draw; |
|---|
| 179 | |
|---|
| 180 | Curses::doupdate; |
|---|
| 181 | } |
|---|
| 182 | |
|---|
| 183 | |
|---|
| 184 | |
|---|
| 185 | no warnings "redefine"; |
|---|
| 186 | |
|---|
| 187 | my $modalfocus = \&Curses::UI::Widget::modalfocus; |
|---|
| 188 | |
|---|
| 189 | # Let modalfocus() be a reentrant into the POE Kernel. This is stackable, |
|---|
| 190 | # so it should not impact other behaviors, and POE keeps chugging along |
|---|
| 191 | # uneffected. This is a modal focus without a callback, this method does |
|---|
| 192 | # not return until the modal widget get's cleared out. |
|---|
| 193 | # |
|---|
| 194 | # This is done here so that ->dailog will still work as it did previously. |
|---|
| 195 | # until this is run. And just in case, we save the old modalfocus |
|---|
| 196 | # definition and redefine it later. |
|---|
| 197 | sub Curses::UI::Widget::modalfocus () { |
|---|
| 198 | my ($this) = @_; |
|---|
| 199 | |
|---|
| 200 | # "Fake" focus for this object. |
|---|
| 201 | $this->{-has_modal_focus} = 1; |
|---|
| 202 | $this->focus; |
|---|
| 203 | $this->draw; |
|---|
| 204 | |
|---|
| 205 | push @modal_objects, $this; |
|---|
| 206 | push @modal_callbacks, undef; |
|---|
| 207 | |
|---|
| 208 | # This is reentrant into the POE::Kernel |
|---|
| 209 | while ( $this->{-has_modal_focus} ) { |
|---|
| 210 | $poe_kernel->loop_do_timeslice; |
|---|
| 211 | } |
|---|
| 212 | |
|---|
| 213 | $this->{-focus} = 0; |
|---|
| 214 | |
|---|
| 215 | pop @modal_callbacks; |
|---|
| 216 | pop @modal_objects; |
|---|
| 217 | |
|---|
| 218 | return $this; |
|---|
| 219 | } |
|---|
| 220 | |
|---|
| 221 | POE::Kernel->run; |
|---|
| 222 | |
|---|
| 223 | # Replace previously defined method into the symbol table. |
|---|
| 224 | *{"Curses::UI::Widget::modalfocus"} = $modalfocus; |
|---|
| 225 | } |
|---|
| 226 | |
|---|
| 227 | sub set_read_timeout { |
|---|
| 228 | my $this = shift; |
|---|
| 229 | |
|---|
| 230 | my $new_timeout = -1; |
|---|
| 231 | |
|---|
| 232 | while (my ($id, $config) = each %{$this->{-timers}}) { |
|---|
| 233 | next unless $config->{-enabled}; |
|---|
| 234 | |
|---|
| 235 | $new_timeout = $config->{-time} |
|---|
| 236 | unless $new_timeout != -1 and |
|---|
| 237 | $new_timeout < $config->{-time}; |
|---|
| 238 | } |
|---|
| 239 | |
|---|
| 240 | $poe_kernel->delay(timer => $new_timeout) if $new_timeout >= 0; |
|---|
| 241 | |
|---|
| 242 | # Force the read timeout to be 0, so Curses::UI polls. |
|---|
| 243 | $this->{-read_timeout} = 0; |
|---|
| 244 | |
|---|
| 245 | return $this; |
|---|
| 246 | } |
|---|
| 247 | |
|---|
| 248 | { |
|---|
| 249 | no warnings "redefine"; |
|---|
| 250 | # None of this work's if POE isn't running... |
|---|
| 251 | # Redefine the callbackmodalfocus to ensure that callbacks and objects make |
|---|
| 252 | # it on to our own private stack. |
|---|
| 253 | sub Curses::UI::Widget::callbackmodalfocus { |
|---|
| 254 | my ($this, $cb) = @_; |
|---|
| 255 | |
|---|
| 256 | # "Fake" focus for this object. |
|---|
| 257 | $this->{-has_modal_focus} = 1; |
|---|
| 258 | $this->focus; |
|---|
| 259 | $this->draw; |
|---|
| 260 | |
|---|
| 261 | push @modal_objects, $this; |
|---|
| 262 | |
|---|
| 263 | if (defined $cb) { |
|---|
| 264 | # They need a callback, so register it. |
|---|
| 265 | push @modal_callbacks, $cb; |
|---|
| 266 | } else { |
|---|
| 267 | # Push a null callback. |
|---|
| 268 | push @modal_callbacks, [sub { }]; |
|---|
| 269 | } |
|---|
| 270 | |
|---|
| 271 | # We assume our callers are going to return immediately back to the |
|---|
| 272 | # main event loop, so we don't need a recursive call. |
|---|
| 273 | return; |
|---|
| 274 | } |
|---|
| 275 | |
|---|
| 276 | } |
|---|
| 277 | |
|---|
| 278 | =head1 NAME |
|---|
| 279 | |
|---|
| 280 | Curses::UI::POE - A subclass makes Curses::UI POE Friendly. |
|---|
| 281 | |
|---|
| 282 | =head1 SYNOPSIS |
|---|
| 283 | |
|---|
| 284 | use Curses::UI::POE; |
|---|
| 285 | |
|---|
| 286 | my $cui = new Curses::UI::POE inline_states => { |
|---|
| 287 | _start => sub { |
|---|
| 288 | $_[HEAP]->dialog("Hello!"); |
|---|
| 289 | }, |
|---|
| 290 | |
|---|
| 291 | _stop => sub { |
|---|
| 292 | $_[HEAP]->dialog("Good bye!"); |
|---|
| 293 | }, |
|---|
| 294 | }; |
|---|
| 295 | |
|---|
| 296 | $cui->mainloop |
|---|
| 297 | |
|---|
| 298 | =head1 INTRODUCTION |
|---|
| 299 | |
|---|
| 300 | This is a subclass for Curses::UI that enables it to work with POE. |
|---|
| 301 | It is designed to simply slide over Curses::UI. Keeping the API the |
|---|
| 302 | same and simply forcing Curses::UI to do all of its event handling |
|---|
| 303 | via POE, instead of internal to itself. This allows you to use POE |
|---|
| 304 | behind the scenes for things like networking clients, without Curses::UI |
|---|
| 305 | breaking your programs' functionality. |
|---|
| 306 | |
|---|
| 307 | =head1 ADDITIONS |
|---|
| 308 | |
|---|
| 309 | This is a list of distinct changes between the Curses::UI API, and the |
|---|
| 310 | Curses::UI::POE API. They should all be non-obstructive additions only, |
|---|
| 311 | keeping Curses::UI::POE a drop-in replacement for Curses::UI. |
|---|
| 312 | |
|---|
| 313 | =head2 Constructor Options |
|---|
| 314 | |
|---|
| 315 | =over 2 |
|---|
| 316 | |
|---|
| 317 | =item inline_states |
|---|
| 318 | |
|---|
| 319 | The inline_states constructor option allows insertion of inline states |
|---|
| 320 | into the Curses::UI::POE controlling session. Since Curses::UI::POE is |
|---|
| 321 | implimented with a small session I figured it may be useful provide the |
|---|
| 322 | ability to the controlling session for all POE to Interface interaction. |
|---|
| 323 | |
|---|
| 324 | While Curses::UI events are still seamlessly forced to use POE, this allows |
|---|
| 325 | you to use it for a little bit more, such as catching responses from another |
|---|
| 326 | POE component that should be directly connected with output. (See the IRC |
|---|
| 327 | client example). |
|---|
| 328 | |
|---|
| 329 | In this controlling session, however, the heap is predefined as the root |
|---|
| 330 | Curses::UI object, which is a hash reference. In the Curses::UI object, |
|---|
| 331 | all private data is indexed by a key begining with "-". So if you wish |
|---|
| 332 | to use the heap to store other data, simply dont use the "-" hash index |
|---|
| 333 | prefix to avoid conflicts. |
|---|
| 334 | |
|---|
| 335 | =back |
|---|
| 336 | |
|---|
| 337 | =head1 TIMERS |
|---|
| 338 | |
|---|
| 339 | The undocumented Curses::UI timers ($cui->timer) will still work, and |
|---|
| 340 | they will be translated into POE delays. I would suggest not using them, |
|---|
| 341 | however, as POE's internal alarms and delays are far more robust. |
|---|
| 342 | |
|---|
| 343 | =head1 DIALOGS |
|---|
| 344 | |
|---|
| 345 | The Curses::UI::POE dialog methods contain thier own miniature event loop, |
|---|
| 346 | similar to the way Curses::UI's dialog methods worked. However instead |
|---|
| 347 | of blocking and polling on readkeys, it incites its own custom miniature |
|---|
| 348 | POE Event loop until the dialog has completed, and then its result is |
|---|
| 349 | returned as per the Curses::UI specifications. |
|---|
| 350 | |
|---|
| 351 | =head1 MODALITY |
|---|
| 352 | |
|---|
| 353 | Curses::UI::POE builds its own internal modality structure. This allows |
|---|
| 354 | Curses::UI to manage it, and POE to issue the (hopefully correct) events. |
|---|
| 355 | To do this it uses its own custom (smaller) event loop, which is reentrant |
|---|
| 356 | into the POE::Loop in use (In this case, usually POE::Loop::Select). This |
|---|
| 357 | way there can be several recursed layers of event loops, forcing focus on |
|---|
| 358 | the current modal widget, without stopping other POE::Sessions from running. |
|---|
| 359 | |
|---|
| 360 | =head1 SEE ALSO |
|---|
| 361 | |
|---|
| 362 | L<POE>, L<Curses::UI>. Use of this module requires understanding of both |
|---|
| 363 | the Curses::UI widget set and the POE Framework. |
|---|
| 364 | |
|---|
| 365 | =head1 BUGS |
|---|
| 366 | |
|---|
| 367 | =over 2 |
|---|
| 368 | |
|---|
| 369 | =item Dialogs before ->mainloop() |
|---|
| 370 | |
|---|
| 371 | Dialogs before Curses::UI::Mainloop |
|---|
| 372 | |
|---|
| 373 | =back |
|---|
| 374 | |
|---|
| 375 | Find more? Send them to me! tag@cpan.org |
|---|
| 376 | |
|---|
| 377 | =head1 AUTHOR |
|---|
| 378 | |
|---|
| 379 | =over 2 |
|---|
| 380 | |
|---|
| 381 | =item Rocco Caputo (rcaputo@cpan.org) |
|---|
| 382 | |
|---|
| 383 | Rocco has helped in an astronomical number of ways. He helped me work out |
|---|
| 384 | a number of issues (including how to do this in the first place) and atleast |
|---|
| 385 | half the code if not more came from his fingertips. |
|---|
| 386 | |
|---|
| 387 | =back |
|---|
| 388 | |
|---|
| 389 | =head1 MAINTAINER |
|---|
| 390 | |
|---|
| 391 | =over 2 |
|---|
| 392 | |
|---|
| 393 | =item Scott McCoy (tag@cpan.org) |
|---|
| 394 | |
|---|
| 395 | This was my stupid idea. I also got to maintain it, although the original |
|---|
| 396 | code (some of which may or may not still exist) came from Rocco. |
|---|
| 397 | |
|---|
| 398 | =back |
|---|
| 399 | |
|---|
| 400 | =cut |
|---|
| 401 | |
|---|
| 402 | 1; |
|---|