File Coverage

blib/lib/POE/XUL/ChangeManager.pm
Criterion Covered Total %
statement 208 471 44.1
branch 54 168 32.1
condition 12 63 19.0
subroutine 39 69 56.5
pod 5 53 9.4
total 318 824 38.5


line stmt bran cond sub pod time code
1             package
2             POE::XUL::ChangeManager;
3             # $Id: ChangeManager.pm 1566 2010-11-03 03:13:32Z fil $
4             # Copyright Philip Gwyn 2007-2010. All rights reserved.
5             # Based on code Copyright 2003-2004 Ran Eilam. All rights reserved.
6              
7             #
8             # POE::XUL::Node and POE::XUL::TextNode will be calling us whenever they
9             # change attributes or children.
10             # We keep a list of POE::XUL::State objects that hold all these changes
11             # so that they may be mirrored in the browser. To speed things up a lot
12             # we break POE::XUL::State's encapsulation.
13             #
14             # We also maintain a list of all the nodes, available via ->getElementById.
15             #
16              
17 19     19   2916 use strict;
  19         22  
  19         440  
18 19     19   70 use warnings;
  19         21  
  19         452  
19              
20 19     19   58 use Carp qw( carp confess croak cluck );
  19         28  
  19         961  
21 19     19   2489 use HTTP::Status;
  19         15021  
  19         4296  
22 19     19   12079 use JSON::XS;
  19         75493  
  19         1006  
23 19     19   7186 use POE::XUL::Logging;
  19         34  
  19         998  
24 19     19   7021 use POE::XUL::State;
  19         31  
  19         399  
25 19     19   6296 use POE::XUL::Encode;
  19         41  
  19         567  
26 19     19   106 use Scalar::Util qw( weaken blessed );
  19         23  
  19         948  
27              
28 19     19   75 use constant DEBUG => 0;
  19         21  
  19         4940  
29              
30             our $VERSION = '0.0601';
31             our $WIN_NAME = 'POEXUL000';
32              
33             ##############################################################
34             sub new
35             {
36 5     5 0 1335 my( $package ) = @_;
37              
38 5         48 my $self = bless {
39             window => undef(),
40             current_event => undef(),
41             states => {},
42             nodes => {},
43             destroyed => [],
44             prepend => [],
45             other_windows => []
46             }, $package;
47              
48 5         21 $self->build_json;
49 5         10 return $self;
50             }
51              
52             ##############################################################
53             sub current_event
54             {
55 0     0 0 0 my $self = shift;
56 0         0 my $rv = $self->{current_event};
57 0 0       0 $self->{current_event} = $_[0] if $_[0];
58 0         0 return $rv;
59             }
60              
61             ##############################################################
62             sub window
63             {
64 0     0 0 0 my( $self ) = @_;
65 0         0 return $self->{window};
66             }
67              
68             ##############################################################
69             sub responded
70             {
71 0     0 0 0 my( $self ) = @_;
72 0         0 return $self->{responded};
73             }
74              
75              
76              
77             ##############################################################
78             sub build_json
79             {
80 5     5 0 9 my( $self ) = @_;
81 5         115 my $coder = JSON::XS->new->space_after( 1 );
82 5         18 $coder->ascii;
83 5         30 $self->{json_coder} = $coder;
84             }
85              
86             ##############################################################
87             sub json_encode
88             {
89 0     0 0 0 my( $self, $out ) = @_;
90 0         0 my $json = eval { $self->{json_coder}->encode( $out ) };
  0         0  
91 0 0       0 if( $@ ) {
92 19     19   8870 use Data::Dumper;
  19         68486  
  19         2287  
93 0         0 warn "Error encoding JSON: $@\n", Dumper $out;
94 0         0 my $err = $@;
95 0         0 $err =~ s/"/\x22/g;
96 0         0 $json = qq(["ERROR", "", "$err"]);
97             }
98              
99             DEBUG and
100 0         0 do {
101             my $foo = $json;
102             $foo =~ s/], /],\n/g;
103 19     19   112 use bytes;
  19         20  
  19         71  
104             xdebug "JSON: $foo\n";
105             xdebug "JSON size: ", length( $json ), "\n";
106             };
107              
108             # $json =~ s/], /],\n/g;
109 0         0 return $json;
110             }
111              
112             sub poexul_encode
113             {
114 0     0 0 0 my( $self, $out ) = @_;
115 0         0 DEBUG and xdebug "length=", 0+@$out;
116 0         0 return POE::XUL::Encode->encode( $out );
117             }
118              
119             ##############################################################
120             sub dispose
121             {
122 0     0 0 0 my( $self ) = @_;
123              
124 0         0 foreach my $N ( @{ $self->{destroyed} },
  0         0  
125 0         0 values %{ $self->{nodes} },
126 0         0 values %{ $self->{states} } ) {
127 0 0 0     0 next unless defined $N and blessed $N and $N->can( 'dispose' );
      0        
128 0         0 $N->dispose;
129             }
130 0         0 $self->{nodes} = {};
131 0         0 $self->{destroyed} = [];
132 0         0 $self->{states} = {};
133 0         0 $self->{prepend} = [];
134 0         0 $self->{other_windowx} = [];
135             }
136              
137             ##############################################################
138             # Get all changes, send to the browser
139             sub flush
140             {
141 41     41 1 91 my( $self ) = @_;
142 41         42 local $_;
143             # TODO: we could cut down on trafic if we don't flush deleted nodes
144             # that are children of a deleted parent
145              
146             # XXX: How to prevent the flushing of deleted Window() and children?
147 41         36 my @out = @{ $self->{prepend} }; # our stuff
  41         90  
148             my @more = (
149 3         8 map( { $_->flush } @{$self->{destroyed}} ), # old stuff
  41         103  
150             $self->flush_node( $self->{window} ) # new/changed stuff
151 41         47 );
152 41 100       70 if( @more ) {
153 34         62 push @out, [ 'for', '' ], @more;
154             }
155              
156 41 50       38 foreach my $win ( @{ $self->{other_windows} || [] } ) {
  41         104  
157 0         0 push @out, [ 'for', $win->id ];
158 0         0 push @out, $self->flush_node( $win );
159             }
160 41         48 $self->{destroyed} = [];
161 41         46 $self->{prepend} = [];
162 41         93 return \@out;
163             }
164              
165             ##############################################################
166             sub flush_node
167             {
168 173     173 0 141 my ($self, $node) = @_;
169 173 50 33     644 return unless $node and blessed $node;
170 173         209 my $state = $self->node_state( $node );
171 173 50 33     868 return unless $state and blessed $state;
172              
173 173         301 my @defer = $state->as_deferred_command;
174 173         250 my @out = $state->flush;
175 173 50       234 unless( $state->{is_framify} ) {
176 173         305 push @out, $self->flush_node( $_ ) foreach $node->children;
177             }
178 173         146 push @out, @defer;
179 173         244 return @out;
180             }
181              
182             ##############################################################
183             sub node_state
184             {
185 428     428 0 327 my( $self, $node ) = @_;
186              
187 428 100       1210 return $self->{states}{"$node"} if $self->{states}{"$node"};
188              
189 42         136 my $is_tn = UNIVERSAL::isa($node, 'POE::XUL::TextNode');
190              
191 42         35 if( DEBUG ) {
192             confess "Not a node: [$node]" unless
193             UNIVERSAL::isa($node, 'POE::XUL::Node') or $is_tn;
194             }
195              
196 42         135 my $state = POE::XUL::State->new( $node );
197 42         98 $self->{states}{ "$node" } = $state;
198              
199 42         45 DEBUG and
200             xdebug "$self Created state ", $state->id, " for $node\n";
201              
202 42         56 $state->{is_textnode} = !! $is_tn;
203              
204 42         86 $self->register_node( $state->id, $node );
205              
206 42         59 return $state;
207             }
208              
209             ##############################################################
210             sub register_window
211             {
212 5     5 0 6 my( $self, $node ) = @_;
213 5 50       22 if( $self->{window} ) {
214 0         0 DEBUG and xwarn "register_window $node";
215 0         0 push @{ $self->{other_windows} }, $node;
  0         0  
216             }
217             else {
218 5         9 $self->{window} = $node;
219             }
220 5         7 my $server = $POE::XUL::Application::server;
221 5 50       19 if( $server ) {
222 0         0 $server->register_window( $node );
223             }
224             }
225              
226             ##############################################################
227             sub unregister_window
228             {
229 0     0 0 0 my( $self, $node ) = @_;
230 0 0       0 if( $node == $self->{window} ) {
231 0         0 confess "You aren't allowed to unregister the main window!\n";
232             }
233 0         0 DEBUG and xwarn "unregister_window $node";
234 0         0 my @new;
235 0 0       0 foreach my $win ( @{ $self->{other_windows}||[] } ) {
  0         0  
236 0 0       0 next if $win == $node;
237 0         0 push @new, $win;
238             }
239              
240 0         0 $self->{other_windows} = \@new;
241 0         0 return;
242             }
243              
244             ##############################################################
245             sub register_node
246             {
247 43     43 0 48 my( $self, $id, $node ) = @_;
248            
249 43 50       67 confess "Why you trying to be funny with me?" unless $id;
250 43 50 33     100 if( $self->{nodes}{$id} and not $self->{nodes}{$id}{disposed} ) {
251 0         0 confess "I already have a node id=$id";
252             }
253 43 50       63 confess "Why you trying to be funny with me?" unless $node;
254             # xwarn "register $id is $node" if $id eq 'LIST_PREQ-PR_LAST_';
255 43         68 $self->{nodes}{ $id } = $node;
256 43         91 weaken( $self->{nodes}{ $id } );
257 43         42 return;
258             }
259              
260             ##############################################################
261             sub unregister_node
262             {
263 15     15 0 23 my( $self, $id, $node ) = @_;
264             # 2009/04 Perl's DESTROY behaviour can be random; if user created
265             # a new node w/ the same ID, we could see the second register before
266             # the DESTROY. So we make sure we are unregistering the right node.
267 15 100 100     60 if( ($self->{nodes}{$id}||'') ne $node ) {
268 8         6 DEBUG and xwarn "Out of order unregister of $id";
269 8         76 return;
270             }
271 7         10 delete $self->{nodes}{ $id };
272             # 2007/12 do NOT $node->dispose here. unregister_node is also
273             # used by ->after_set_attribute()
274              
275             # xwarn "unregister $id is $node" if $id eq 'LIST_PREQ-PR_LAST_';
276 7         44 return;
277             }
278              
279             ##############################################################
280             sub getElementById
281             {
282 1     1 0 290 my( $self, $id ) = @_;
283 1         4 return $self->{nodes}{ $id };
284             }
285              
286             ##############################################################
287             # We need for the node to have the same ID as the state
288             sub before_creation
289             {
290 28     28 0 29 my( $self, $node ) = @_;
291 28         61 my $state = $self->node_state( $node );
292              
293 28 50       58 return if $node->getAttribute( 'id' );
294 0         0 warn "$node has no ID";
295 0         0 $node->setAttribute( id => $state->{id} );
296             }
297              
298              
299              
300             ##############################################################
301             sub after_destroy
302             {
303 17     17 0 19 my( $self, $node ) = @_;
304             # Don't use state_node, as it will create the state
305 17         28 my $state = delete $self->{states}{"$node"};
306 17         19 my $id;
307 17 100       59 if( $state ) {
    100          
308 3         4 $id = $state->{id};
309             delete $self->{states}{ $state->{style} }
310 3 50       7 if $state->{style};
311             }
312             elsif( $node->can( 'id' ) ) {
313 8         19 $id = $node->id;
314             }
315 17 100       72 return unless $id;
316 11         18 $self->unregister_node( $id, $node );
317             }
318              
319             ##############################################################
320             sub after_set_attribute
321             {
322 63     63 0 63 my( $self, $node, $key, $value ) = @_;
323 63 50       104 return if $self->{ignorechanges};
324 63         81 my $state = $self->node_state($node);
325              
326 63 100 33     271 if ($key eq 'tag') {
    100 33        
    50          
327 27         49 $state->{tag} = $value;
328 27 100       73 $self->register_window( $node ) if $node->is_window;
329             }
330             elsif( $key eq 'id' ) {
331 1         4 $self->_set_id( $node, $key, $value, $state );
332              
333             }
334             elsif( $key eq 'src' or $key eq 'href' or $key eq 'datasources' ) {
335 0         0 $self->_set_uri( $node, $key, $value, $state );
336             }
337             else {
338 35         84 $state->set_attribute($key, $value);
339             # TODO: track exclusive things like focus()
340             }
341              
342             }
343              
344             sub _set_id
345             {
346 1     1   2 my( $self, $node, $key, $value, $state ) = @_;
347              
348 1 50       3 return if $state->{id} eq $value;
349 1         1 DEBUG and
350             xdebug "node $state->{id} is now $value";
351 1         1 my $old_id = $state->{id};
352              
353 1         3 $state->set_attribute($key, $value);
354              
355 1         3 $self->unregister_node( $state->{id}, $node );
356 1         1 $state->{id} = $value;
357 1         2 $self->register_node( $state->{id}, $node );
358             }
359              
360             sub _set_uri
361             {
362 0     0   0 my( $self, $node, $key, $value, $state ) = @_;
363              
364 0         0 my $hidden = "hidden-$key";
365 0         0 my $cb;
366 0 0       0 if( blessed $value ) {
    0          
    0          
367 0 0 0     0 unless( $value->can( 'mime_type' ) and
      0        
368             ( $value->can( 'as_string' ) or $value->can( 'as_xml' ) ) ) {
369 0         0 croak "$key object must implement as_string or as_xml, as well as mime_type methods";
370             }
371 0         0 DEBUG and xwarn "Callback to object $value";
372 0         0 $cb = $hidden;
373             }
374             elsif( ref $value ) {
375             # coderef or array ref for a callback
376 0         0 $cb = $hidden;
377 0 0       0 if( 'ARRAY' eq ref $value ) {
378 0 0 0     0 if( 2 == @$value and 'HASH' eq ref $value->[-1] ) {
379 0         0 $cb = { attribute => $cb,
380             extra => pop @$value
381             };
382             }
383 0 0       0 if( 1 == @$value ) {
384 0         0 unshift @$value,
385             $POE::Kernel::poe_kernel->get_active_session->ID;
386             }
387             }
388             }
389             # binary data
390             elsif( $value !~ m,^(((ftp|file|data|https?):)|/), ) { # not a URI
391 0 0 0     0 if( 30_000 < length $value or not $node->getAttribute( 'content-type' )) {
392             # Don't use a data: url if
393             # - the data is too long
394             # - we don't have a content-type attribute
395             # In the latter case, we hope we'll have one, once we get to the
396             # callback
397 0         0 $cb = $hidden;
398             }
399             else {
400 0         0 my $ct = $node->getAttribute( 'content-type' );
401 0         0 my $uri = URI->new( "data:" );
402 0         0 $uri->media_type( $ct );
403 0         0 $uri->data( $value );
404 0         0 $state->set_attribute( $key, $uri->as_string );
405 0         0 return;
406             }
407             }
408             else {
409 0         0 $state->set_attribute($key, $value);
410 0         0 return;
411             }
412              
413              
414             # Setting a callback attribute cases Runner to set the value of
415             # the attribute to an URL that does a Callback event
416             # (see commandCallback).
417             # This then calls handle_Callback (see below) or the coderef/event
418             # defined in $value
419             # $cb must be either a value (which gets in attribute when it comes back)
420             # or a hashref { extra=>{}, attribute=>'' }
421 0         0 $state->set_attribute( callback => $cb );
422 0         0 local $self->{ignorechanges} = 1; # don't send to browser
423 0         0 $node->setAttribute( $hidden, $value );
424              
425             }
426              
427              
428             ##############################################################
429             sub after_remove_attribute
430             {
431 2     2 0 6 my( $self, $node, $key ) = @_;
432 2 50       8 return if $self->{ignorechanges};
433 2         8 my $state = $self->node_state( $node );
434              
435             delete $self->{states}{ $state->{style} } if $key eq 'style' and
436 2 0 33     8 $state->{style};
437 2         10 $state->remove_attribute( $key );
438             }
439              
440             ##############################################################
441             sub after_method_call
442             {
443 1     1 0 2 my( $self, $node, $key, $args ) = @_;
444 1 50       3 return if $self->{ignorechanges};
445 1         4 my $state = $self->node_state($node);
446              
447 1         4 $state->method_call($key, $args);
448             }
449              
450              
451              
452             ##############################################################
453             sub after_new_style
454             {
455 3     3 0 3 my( $self, $node ) = @_;
456 3         7 my $state = $self->node_state($node);
457             delete $self->{states}{ $state->{style} }
458 3 100       10 if $state->{style};
459 3         14 my $style = $node->get_style;
460 3         6 $state->{style} = 0+$style;
461 3         11 $self->{states}{ $state->{style} } = $state;
462 3         6 $state->set_attribute( style => "$style" );
463 3         5 return;
464             }
465              
466             ##############################################################
467             sub after_style_change
468             {
469 8     8 0 8 my( $self, $style, $property, $value ) = @_;
470 8         16 my $state = $self->{states}{ 0+$style };
471 8         20 $state->style_change( $property, $value );
472             }
473              
474             ##############################################################
475             # when node added, set parent node state id on child node state
476             sub after__add_child_at_index
477             {
478 57     57 0 56 my( $self, $parent, $child, $index ) = @_;
479              
480 57         72 my $child_state = $self->node_state( $child );
481 57         69 $child_state->{parent} = $self->node_state( $parent );
482 57         89 weaken $child_state->{parent};
483 57 50       70 if( defined $child_state->{trueindex} ) {
484 0         0 $child_state->{trueindex} = $index;
485             }
486             else {
487 57         81 $child_state->{index} = $index;
488             }
489              
490 57 100       36 return unless @{$child->{children} || []};
  57 100       206  
491              
492 18         17 my $n = 0;
493 18         17 foreach my $subchild ( @{ $child->{children} } ) {
  18         28  
494 22         35 $self->after__add_child_at_index( $child, $subchild, $n );
495 22         36 $n++;
496             }
497             }
498              
499             sub set_trueindex
500             {
501 35     35 0 37 my( $self, $parent, $child, $trueindex ) = @_;
502 35         40 my $child_state = $self->node_state( $child );
503             # Ignore trueindex for now... It breaks to many things
504 35         58 $child_state->{index} = $trueindex;
505             }
506              
507             ##############################################################
508             # when node destroyed, update state using set_destoyed
509             sub before_remove_child
510             {
511 3     3 0 3 my( $self, $parent, $child, $index ) = @_;
512             # my $child = $parent->_compute_child_and_index($context->params->[1]);
513             # return unless $child;
514 3 50       6 Carp::croak "Why no index" unless defined $index;
515 3         6 my $child_state = $self->node_state($child);
516 3         12 $child_state->is_destroyed( $parent, $index );
517 3         2 push @{$self->{destroyed}}, $child_state;
  3         7  
518              
519 3         9 delete $self->{states}{ "$child" };
520             delete $self->{states}{ $child_state->{style} }
521 3 50       8 if $child_state->{style};
522 3         9 $self->unregister_node( $child_state->{id}, $child );
523             }
524              
525             ##############################################################
526             sub after_cdata_change
527             {
528 6     6 0 6 my( $self, $node ) = @_;
529 6         11 my $state = $self->node_state( $node );
530 6         9 $state->{cdata} = $node->{data};
531 6         11 $state->{is_new} = 1;
532             }
533              
534              
535              
536             ##############################################################
537             # So that we can detect changes between requests
538             sub request_start
539             {
540 0     0 0 0 my( $self, $event ) = @_;
541 0         0 $self->{current_event} = $event;
542 0         0 $self->{responded} = 0;
543             }
544              
545             sub request_done
546             {
547 0     0 0 0 my( $self ) = @_;
548 0         0 $self->{responded} = 1;
549 0         0 my $event = delete $self->{current_event};
550 0 0       0 $event->dispose if $event;
551 0         0 undef( $event );
552              
553             # use Devel::Cycle;
554             # find_cycle( $self );
555             }
556              
557             ##############################################################
558             sub wrapped_error
559             {
560 0     0 0 0 my( $self, $string ) = @_;
561 0 0       0 if( $self->{current_event} ) {
562             # xwarn "wrapped with $self->{current_event}";
563 0         0 $self->error_response( $self->{current_event}->response, $string );
564             }
565             else {
566             # TODO: what to do with errors that happen between events?
567 0         0 xlog "Error between events: $string";
568             }
569             }
570              
571             ##############################################################
572             sub error_response
573             {
574 0     0 0 0 my( $self, $resp, $string ) = @_;
575 0         0 xlog "error_response $string";
576             # confess "ERROR $string";
577 0         0 return $self->cooked_response( $resp, [[ 'ERROR', '', $string]] );
578             }
579              
580             ##############################################################
581             sub response
582             {
583 0     0 0 0 my( $self, $resp ) = @_;
584 0         0 my $out = $self->flush;
585             # xwarn "response = ", 0+@$out;
586 0         0 $self->cooked_response( $resp, $out );
587             }
588              
589             ##############################################################
590             sub cooked_response
591             {
592 0     0 0 0 my( $self, $resp, $out ) = @_;
593              
594 0 0       0 if( $self->{responded} ) {
595 0         0 confess "Already responded";
596 0         0 xcarp "Already responded";
597 0         0 return;
598             }
599 0 0       0 confess "I need a response" unless $resp;
600              
601 0         0 my $data;
602 0 0       0 unless( ref $out ) {
603 0         0 $data = $out;
604             }
605             elsif( 0 ) { # XXX config
606             $resp->content_type( POE::XUL::Encode->content_type );
607             $data = $self->poexul_encode( $out );
608             }
609             else {
610 0         0 $resp->content_type( 'application/json' ); #; charset=UTF-8' );
611 0         0 $data = $self->json_encode( $out );
612             }
613 0         0 DEBUG and
614             xdebug "Response=$data";
615 0         0 $self->__response( $resp, $data );
616             }
617              
618              
619             ##############################################################
620             sub xul_response
621             {
622 0     0 0 0 my( $self, $resp, $xul ) = @_;
623              
624 0         0 $resp->content_type( 'application/vnd.mozilla.xul+xml' );
625 0         0 $self->__response( $resp, $xul );
626             }
627              
628             ##############################################################
629             sub data_response
630             {
631 0     0 0 0 my( $self, $resp, $data ) = @_;
632             # TODO: should we check if there is anything to be flushed?
633             # Idealy, we'd do it non-destructively, so that we could warn but
634             # the changes would wait for next request
635 0         0 $self->__response( $resp, $data );
636             }
637              
638             ##############################################################
639             ## This should be moved to Controler
640             sub __response
641             {
642 0     0   0 my( $self, $resp, $content ) = @_;
643              
644            
645 0         0 do {
646             # HTTP exptects content-length to be number of octets, not chars
647             # The UTF-8 that JSON::XS is producing was screwing up length()
648 19     19   39922 use bytes;
  19         31  
  19         74  
649 0         0 $resp->content_length( length $content );
650             };
651 0         0 $resp->content( $content );
652 0         0 $resp->code( RC_OK );
653 0         0 $resp->continue(); # but only if we've stoped!
654              
655 0         0 $self->request_done;
656             }
657              
658              
659              
660             ##############################################################
661             sub SID
662             {
663 0     0 0 0 my( $self, $SID ) = @_;
664 0         0 push @{ $self->{ prepend } }, $self->build_SID( $SID );
  0         0  
665             }
666              
667              
668             ##############################################################
669             sub build_SID
670             {
671 0     0 0 0 my( $self, $SID ) = @_;
672 0         0 return POE::XUL::State->make_command_SID( $SID );
673             }
674              
675             ##############################################################
676             # Send a boot message to the client
677             sub Boot
678             {
679 0     0 0 0 my( $self, $msg ) = @_;
680 0         0 push @{ $self->{prepend} }, POE::XUL::State->make_command_boot( $msg );
  0         0  
681             }
682              
683              
684              
685              
686              
687              
688              
689              
690             ##############################################################
691             # Side-effects for a given event
692             ##############################################################
693             sub handle_Click
694             {
695 0     0 0 0 my( $self, $event ) = @_;
696 0         0 return;
697             }
698              
699             ##############################################################
700             # A textbox was changed
701             # Uses source, value
702             sub handle_Change
703             {
704 0     0 0 0 my( $self, $event ) = @_;
705 0         0 local $self->{ignorechanges} = 1;
706 0         0 DEBUG and
707             xdebug "Change value=", $event->value, " source=", $event->source;
708 0         0 $event->source->setAttribute( value=> $event->value );
709             }
710              
711             ##############################################################
712             sub handle_BoxClick
713             {
714 0     0 0 0 my( $self, $event ) = @_;
715 0         0 local $self->{ignorechanges} = 1;
716 0         0 my $checked = $event->checked;
717              
718 0         0 DEBUG and xdebug "Click event=$event source=", $event->source->id;
719             # $checked = defined $checked && $checked eq 'true'? 1: 0;
720 0         0 $event->checked( $checked );
721 0         0 $event->source->checked( $checked );
722             }
723              
724             ##############################################################
725             # A radio button was clicked
726             # Uses : source, selectedId
727             sub handle_RadioClick
728             {
729 0     0 0 0 my( $self, $event ) = @_;
730 0         0 local $self->{ignorechanges} = 1;
731 0         0 my $selectedId = $event->selectedId;
732              
733 0         0 DEBUG and
734             xdebug "RadioClick source=",
735             ($event->source->id||$event->source),
736             " selectedId=$selectedId";
737 0         0 my $radiogroup = $event->source;
738 0         0 my $radio = $self->getElementById( $selectedId );
739              
740 0 0       0 die "Can't find element $selectedId for RadioClick"
741             unless $radio;
742              
743 0         0 $event->event( 'Click' );
744 0         0 foreach my $C ( $radiogroup->children ) {
745 0 0       0 if( $C == $radio ) {
    0          
746 0         0 $C->setAttribute( 'selected', 1 );
747 0         0 DEBUG and xdebug "Found $selectedId\n";
748             # If there was a Click handler on the Radio, we
749             # revert to the former behaviour of running that handler
750             # xdebug "Going to C=$C id=", $C->id;
751 0         0 $event->bubble_to( $radiogroup );
752 0         0 $event->__source_id( $C->id );
753             }
754             elsif( $C->selected ) {
755 0         0 $C->removeAttribute( 'selected' );
756             }
757             }
758             }
759              
760             ##############################################################
761             # A list item was selected
762             # Uses: source, selectedIndex, value
763             sub handle_Select
764             {
765 0     0 0 0 my( $self, $event ) = @_;
766 0         0 local $self->{ignorechanges} = 1;
767              
768 0         0 my $menulist = $event->source;
769              
770 0 0       0 if( $menulist->tagName eq 'tree' ) {
771 0         0 return $self->handle_TreeSelect( $event );
772             }
773              
774 0         0 my $I = $event->selectedIndex;
775             # selecting text in a textbox!
776 0 0 0     0 return unless defined $I and $I ne 'undefined';
777 0         0 my $oI = $menulist->selectedIndex;
778              
779 0         0 DEBUG and
780             xdebug "Select was=$oI, now=$I";
781              
782 0 0 0     0 if( defined $I and $I == -1 ) {
    0 0        
      0        
783 0         0 xdebug "Change Combo I=$I value=", $event->value;
784 0         0 $menulist->selectedIndex( $I );
785 0         0 $menulist->value( $event->value );
786 0         0 return;
787             }
788             elsif( $menulist->editable and $oI and $oI == -1 ) {
789 0         0 xdebug "Change Combo remove 'value'";
790 0         0 $menulist->removeAttribute( 'value' );
791             }
792              
793 0         0 $self->Select_choose( $event, $oI, 'selected', 0 );
794 0         0 $menulist->selectedIndex( $I );
795 0         0 my $item = $self->Select_choose( $event, $I, 'selected', 1 );
796              
797 0 0       0 if( $item ) {
798 0         0 xdebug "Select $I.label=", $item->label;
799             # The event should go to the item first, then the "parent"
800 0         0 $event->bubble_to( $event->source );
801 0         0 $event->__source_id( $item->id );
802             # $menulist->value( $item->value );
803             }
804             }
805              
806              
807             ##############################################################
808             # Turn one menuitem on/off
809             sub Select_choose
810             {
811 0     0 0 0 my( $self, $event, $I, $att, $value ) = @_;
812 0         0 my $list = $event->source;
813 0 0       0 return unless $list;
814 0 0       0 return unless $list->first_child;
815 0 0       0 return unless defined $I;
816              
817 0         0 my $item = $list->getItemAtIndex( $I );
818 0 0       0 return unless $item;
819              
820 0         0 local $self->{ignorechanges} = 0;
821 0 0       0 if( $value ) {
822 0         0 $item->setAttribute( $att, $value );
823             }
824             else {
825 0         0 $item->removeAttribute( $att );
826             }
827 0         0 return $item;
828             }
829              
830             ##############################################################
831             # User picked a colour
832             sub handle_Pick
833             {
834 0     0 0 0 my( $self, $event ) = @_;
835 0         0 local $self->{ignorechanges} = 1;
836 0         0 $event->source->color($self->color);
837             }
838              
839             ##############################################################
840             # Image src="" callbackup
841             sub handle_Callback
842             {
843 0     0 0 0 my( $self, $event ) = @_;
844 0         0 my $node = $event->source;
845 0         0 my $key = $event->attribute;
846             # xdebug( "Callback $key" );
847 0         0 my $cb = $node->getAttribute( $key );
848 0 0       0 if( blessed $cb ) {
    0          
849 0         0 DEBUG and xwarn "Callback with $cb";
850 0         0 $event->response->content_type(
851             $cb->mime_type
852             );
853 0 0       0 if( $cb->can( 'as_xml' ) ) {
854 0         0 $event->data_response( $cb->as_xml );
855             }
856             else {
857 0         0 $event->data_response( $cb->as_string );
858             }
859             }
860             elsif( ref $cb ) {
861 0 0       0 if( 'CODE' eq ref $cb ) {
862 0         0 $cb->( $node, $event );
863             }
864             else {
865             # xdebug( join '/', @$cb );
866 0         0 $POE::Kernel::poe_kernel->call( @$cb, $node, $event );
867             }
868             }
869             else {
870 0         0 $event->response->content_type(
871             $node->getAttribute( 'content-type' )
872             );
873 0         0 $event->data_response( $cb );
874             }
875             }
876              
877             ##############################################################
878             # A row of a tree was selected
879             # Uses: source, selectedIndex, value
880             sub handle_TreeSelect
881             {
882 0     0 0 0 my( $self, $event ) = @_;
883              
884 0         0 local $self->{ignorechanges} = 1;
885              
886 0         0 my $tree = $event->source;
887 0         0 my $rowN = $event->selectedIndex;
888              
889             # Handle user sorting of RDF trees
890 0 0       0 if( $event->primary_col ) {
891 0         0 xdebug "primary_col=", $event->primary_col;
892 0         0 xdebug "primary_text=", $event->primary_text;
893 0         0 my $rdf = $tree->getAttribute( 'hidden-datasources' );
894 0         0 xdebug "rdf: $rdf";
895 0 0 0     0 if( blessed( $rdf ) and $rdf->can( 'index_of' ) ) {
896 0         0 $rowN = $rdf->index_of( $event->primary_col, $event->primary_text );
897 0         0 xdebug "true index is $rowN";
898 0         0 $tree->selectedIndex( $rowN );
899 0         0 $event->selectedIndex( $rowN );
900 0         0 return;
901             }
902             }
903              
904 0         0 $tree->selectedIndex( $rowN );
905              
906             # Find the xul:treechildren node
907 0         0 my $treechildren;
908 0         0 foreach my $node ( $tree->children ) {
909 0 0       0 next unless $node->tagName eq 'treechildren';
910 0         0 $treechildren = $node;
911 0         0 last;
912             }
913              
914 0 0       0 unless( $treechildren ) {
915             # This happens when a tree has a datasource, like RDF
916 0         0 DEBUG and xdebug "Select on a tree w/o treechildren";
917 0         0 return;
918             }
919              
920             DEBUG and
921 0         0 xdebug "treechildren=$treechildren";
922            
923             # Find the row nodes. This could be xul:treeitem or xul::treerow
924 0         0 my @rows;
925 0         0 foreach my $treeitem ( $treechildren->children ) {
926 0         0 my $first = $treeitem->first_child;
927 0 0 0     0 if( $first and $first->tagName eq 'treerow' ) {
928 0         0 push @rows, $first;
929             }
930             else {
931 0         0 push @rows, $treeitem;
932             }
933             }
934             DEBUG and
935 0         0 xdebug "Found ", 0+@rows, " rows";
936              
937 0         0 for( my $r = 0 ; $r<=$#rows ; $r++ ) {
938 0         0 my $prop = $rows[$r]->properties;
939 0 0       0 if( $r == $rowN ) {
    0          
940 0         0 $prop =~ s/\s*selected\s*//g;
941 0 0       0 if( $prop ) { $prop .= ' selected' }
  0         0  
942 0         0 else { $prop = 'seelected' }
943 0         0 DEBUG and xdebug "Row $r properties=$prop";
944 0         0 $rows[$r]->properties( $prop );
945 0         0 $event->bubble_to( $tree );
946 0         0 $event->__source_id( $rows[$r]->id );
947             }
948             elsif( $prop =~ s/\s*selected\s*//g ) {
949 0         0 DEBUG and xdebug "Row $r properties=$prop";
950 0   0     0 $rows[$r]->properties( $prop||'' );
951             }
952             }
953              
954 0         0 return;
955             }
956              
957              
958              
959              
960              
961              
962             ##############################################################
963             sub Prepend
964             {
965 6     6 0 7 my( $self, $cmd ) = @_;
966 6         4 push @{ $self->{prepend} }, $cmd;
  6         10  
967 6         5 return 0+@{ $self->{prepend} };
  6         10  
968             }
969              
970             ##############################################################
971             sub flush_to_prepend
972             {
973 1     1 0 1 my( $self ) = @_;
974 1         3 my $out = $self->flush;
975 1 50       3 return unless @$out;
976 1         2 push @{ $self->{prepend} }, @$out;
  1         2  
977 1         1 return 0+@{ $self->{prepend} };
  1         3  
978             }
979              
980             ##############################################################
981             sub timeslice
982             {
983 2     2 1 2 my( $self ) = @_;
984 2         9 $self->Prepend( [ 'timeslice' ] );
985             }
986              
987             ##############################################################
988             sub popup_window
989             {
990 4     4 1 6 my( $self, $name, $features ) = @_;
991 4   66     9 $name ||= $WIN_NAME++;
992 4   100     9 $features ||= {};
993 4 50       8 croak "Features must be a hashref" unless 'HASH' eq ref $features;
994 4         9 $self->Prepend( [ 'popup_window', $name, $features ] );
995 4         10 return $name;
996             }
997              
998             ##############################################################
999             sub close_window
1000             {
1001 0     0 1 0 my( $self, $name ) = @_;
1002 0         0 $self->Prepend( [ 'close_window', $name ] );
1003             }
1004              
1005             ##############################################################
1006             # Send some instructions to Runner.js. Or other control of the CM
1007             sub instruction
1008             {
1009 9     9 1 9 my( $self, $inst ) = @_;
1010              
1011 9         5 my( $op, @param );
1012 9 100       13 if( ref $inst ) {
1013 4         6 ( $op, @param ) = @$inst;
1014             }
1015             else {
1016 5         5 $op = $inst;
1017             }
1018              
1019 9 100       24 if( $op eq 'flush' ) { # flush changes to output buffer
    100          
    100          
    50          
    0          
1020 1         4 return $self->flush_to_prepend;
1021             }
1022             elsif( $op eq 'empty' ) { # empty all changes
1023 2         4 return $self->flush;
1024             }
1025             elsif( $op eq 'timeslice' ) { # give up a timeslice
1026 2         5 return $self->timeslice;
1027             }
1028             elsif( $op eq 'popup_window' ) {
1029 4         8 return $self->popup_window( @param );
1030             }
1031             elsif( $op eq 'close_window' ) {
1032 0           return $self->close_window( @param );
1033             }
1034             else {
1035 0           die "Unknown instruction: $op";
1036             }
1037             }
1038              
1039             1;
1040              
1041             __END__