efb4763b31b087b44fdfdc48819631bfa39ff7dd
[catagits/Reaction.git] / lib / Reaction / UI / ViewPort.pm
1 package Reaction::UI::ViewPort;
2
3 use Reaction::Class;
4 use Scalar::Util qw/blessed/;
5
6 use namespace::clean -except => [ qw(meta) ];
7
8
9 sub DEBUG_EVENTS () { $ENV{REACTION_UI_VIEWPORT_DEBUG_EVENTS} }
10
11 has location => (isa => 'Str', is => 'rw', required => 1);
12 has layout => (isa => 'Str', is => 'rw', lazy_build => 1);
13 has layout_args => (isa => 'HashRef', is => 'ro', default => sub { {} });
14 has outer => (isa => 'Reaction::UI::ViewPort', is => 'rw', weak_ref => 1);
15 has inner => (isa => 'Reaction::UI::ViewPort', is => 'rw');
16 has focus_stack => (
17   isa => 'Reaction::UI::FocusStack', is => 'rw', weak_ref => 1
18 );
19 has _tangent_stacks => (
20   isa => 'HashRef', is => 'ro', default => sub { {} }
21 );
22 has ctx => (isa => 'Catalyst', is => 'ro', weak_ref => 1); #, required => 1);
23
24 sub _build_layout {
25   '';
26 }
27
28 sub create_tangent {
29   my ($self, $name) = @_;
30   my $t_map = $self->_tangent_stacks;
31   if (exists $t_map->{$name}) {
32     confess "Can't create tangent with already existing name ${name}";
33   }
34   my $loc = join('.', $self->location, $name);
35   my $tangent = Reaction::UI::FocusStack->new(loc_prefix => $loc);
36   $t_map->{$name} = $tangent;
37   return $tangent;
38 }
39
40 sub focus_tangent {
41   my ($self, $name) = @_;
42   if (my $tangent = $self->_tangent_stacks->{$name}) {
43     return $tangent;
44   } else {
45     return;
46   }
47 }
48
49 sub focus_tangents {
50   return keys %{shift->_tangent_stacks};
51 }
52
53 sub child_event_sinks {
54   my $self = shift;
55   return values %{$self->_tangent_stacks};
56 }
57
58 sub apply_events {
59   my ($self, $events) = @_;
60   return unless keys %$events;
61   $self->apply_child_events($events);
62   $self->apply_our_events($events);
63 }
64
65 sub apply_child_events {
66   my ($self, $events) = @_;
67   return unless keys %$events;
68   foreach my $child ($self->child_event_sinks) {
69     confess blessed($child) ."($child) is not a valid object"
70       unless blessed($child) && $child->can('apply_events');
71     $child->apply_events($events);
72   }
73 }
74
75 sub apply_our_events {
76   my ($self, $events) = @_;
77   my @keys = keys %$events;
78   return unless @keys;
79   my $loc = $self->location;
80   my %our_events;
81   foreach my $key (keys %$events) {
82     if ($key =~ m/^${loc}:(.*)$/) {
83       $our_events{$1} = $events->{$key};
84     }
85   }
86   if (keys %our_events) {
87     #warn "$self: events ".join(', ', %our_events)."\n";
88     $self->handle_events(\%our_events);
89   }
90 }
91
92 sub handle_events {
93   my ($self, $events) = @_;
94   my $exists = exists $events->{exists};
95   if ($exists) {
96     my %force = $self->force_events;
97     my @need = grep { !exists $events->{$_} } keys %force;
98     @{$events}{@need} = @force{@need};
99   }
100   foreach my $event ($self->accept_events) {
101     if (exists $events->{$event}) {
102       if (DEBUG_EVENTS) {
103         my $name = join(' at ', $self, $self->location);
104         print STDERR
105           "Applying Event: $event on $name with value: "
106           .(defined $events->{$event} ? $events->{$event} : '<undef>');
107       }
108       $self->$event($events->{$event});
109     }
110   }
111 }
112
113 sub accept_events { () }
114
115 sub force_events { () }
116
117 sub event_id_for {
118   my ($self, $name) = @_;
119   return join(':', $self->location, $name);
120 }
121
122 sub sort_by_spec {
123   my ($self, $spec, $items) = @_;
124   return [@$items] if not defined $spec;
125
126   my @order;
127   if (ref $spec eq 'ARRAY') {
128     return [sort @$items] unless @$spec;
129     @order = @$spec;
130   }
131   elsif (not ref $spec) {
132     return [@$items] unless length $spec;
133     @order = split /\s+/, $spec;
134   }
135
136   my %order_map = map {$_ => 0} @$items;
137   for my $order_num (0..$#order) {
138     $order_map{ $order[$order_num] } = ($#order - $order_num) + 1;
139   }
140
141   return [sort {$order_map{$b} <=> $order_map{$a}} @$items];
142 }
143
144 __PACKAGE__->meta->make_immutable;
145
146
147 1;
148
149
150 =head1 NAME
151
152 Reaction::UI::ViewPort - Page layout building block
153
154 =head1 SYNOPSIS
155
156   # Create a new ViewPort:
157   # $stack isa Reaction::UI::FocusStack object
158   my $vp = $stack->push_viewport('Reaction::UI::ViewPort', layout => 'xthml');
159
160   # Fetch ViewPort higher up the stack (further out)
161   my $outer = $vp->outer();
162
163   # Fetch ViewPort lower down (further in)
164   my $inner = $vp->inner();
165
166   # Create a named tangent stack for this ViewPort
167   my $substack = $vp->create_tangent('name');
168
169   # Retrieve a tangent stack for this ViewPort
170   my $substack = $vp->forcus_tangent('name');
171
172   # Get the names of all the tangent stacks for this ViewPort
173   my @names = $vp->focus_tangents();
174
175   # Fetch all the tangent stacks for this ViewPort
176   # This is called by apply_events
177   my $stacks = $vp->child_event_sinks();
178
179
180   ### The following methods are all called automatically when using
181   ### Reaction::UI::Controller(s)
182   # Resolve current events with this ViewPort
183   $vp->apply_events($ctx, $param_hash);
184
185   # Apply current events to all tangent stacks
186   # This is called by apply_events
187   $vp->apply_child_events($ctx, $params_hash);
188
189   # Apply current events to this ViewPort
190   # This is called by apply_events
191   $vp->apply_our_events($ctx, $params_hash);
192
193 =head1 DESCRIPTION
194
195 A ViewPort describes part of a page, it can be a field, a form or
196 an entire page. ViewPorts are created on a
197 L<Reaction::UI::FocusStack>, usually belonging to a controller or
198 another ViewPort. Each ViewPort knows it's own position in the stack
199 it is in, as well as the stack containing it.
200
201 Each ViewPort has a specific location in the heirarchy of viewports
202 making up a page. The hierarchy is determined as follows: The first
203 ViewPort in a stack is labeled C<0>, the second is C<1> and so on. If
204 a ViewPort is in a named tangent, it's location will contain the name
205 of the tangent in it's location.
206
207 For example, the first ViewPort in the 'left' tangent of the main
208 ViewPort has location C<0.left.0>.
209
210 Several ViewPort attributes are set by
211 L<Reaction::UI::FocusStack/push_viewport> when new ViewPorts are
212 created, these are as follows:
213
214 =over
215
216 =item Automatic:
217
218 =over
219
220 =item outer
221
222 The outer attribute is set to the previous ViewPort in the stack when
223 creating a ViewPort, if the ViewPort is the first in the stack, it
224 will be undef.
225
226 =item inner
227
228 The inner attribute is set to the next ViewPort down in the stack when
229 it is created, if this is the last ViewPort in the stack, it will be
230 undef.
231
232 =item focus_stack
233
234 The focus_stack attribute is set to the L<Reaction::UI::FocusStack>
235 object that created the ViewPort.
236
237 =item ctx
238
239 The ctx attribute will be passed automatically when using
240 L<Reaction::UI::Controller/push_viewport> to create a ViewPort in the
241 base stack of a controller. When creating tangent stacks, you may have
242 to pass it in yourself.
243
244 =back
245
246 =item Optional:
247
248 =over
249
250 =item location
251
252 =item layout
253
254 The layout attribute can either be specifically passed when calling
255 C<push_viewport>, or it will be determined using the last part of the
256 ViewPorts classname.
257
258 =item column_order
259
260 This is generally used by more specialised ViewPorts such as the
261 L<ListView|Reaction::UI::ViewPort::ListView> or
262 L<Action|Reaction::UI::ViewPort::Action>. It can be either a
263 space separated list of column names, or an arrayref of column names.
264
265 =back
266
267 =back
268
269 =head1 METHODS
270
271 =head2 outer
272
273 =over
274
275 =item Arguments: none
276
277 =back
278
279 Fetch the ViewPort outside this one in the page hierarchy.
280
281 =head2 inner
282
283 =over
284
285 =item Arguments: none
286
287 =back
288
289 Fetch the ViewPort inside this one in the page hierarchy.
290
291 =head2 create_tangent
292
293 =over
294
295 =item Arguments: $tangent_name
296
297 =back
298
299 Create a new named L<Reaction::UI::FocusStack> inside this
300 ViewPort. The created FocusStack is returned.
301
302 =head2 focus_tangent
303
304 =over
305
306 =item Arguments: $tangent_name
307
308 =back
309
310 Fetch a named FocusStack from this ViewPort.
311
312 =head2 focus_tangents
313
314 =over
315
316 =item Arguments: none
317
318 =back
319
320 Returns a list of names of all the known tangents in this ViewPort.
321
322 =head2 focus_stack
323
324 Return the L<Reaction::UI::FocusStack> object that this ViewPort is in.
325
326 =head2 apply_events
327
328 =over
329
330 =item Arguments: $ctx, $params_hashref
331
332 =back
333
334 This method is called by the FocusStack object to resolve all events
335 for the ViewPort.
336
337 =head2 apply_child_events
338
339 =over
340
341 =item Arguments: $ctx, $params_hashref
342
343 =back
344
345 Resolve the given events for all the tangents of this ViewPort. Called
346 by L<apply_events>.
347
348 =head2 apply_our_events
349
350 =over
351
352 =item Arguments: $ctx, $events
353
354 =back
355
356 Resolve the given events that match the location of this
357 ViewPort. Called by L<apply_events>.
358
359 =head2 handle_events
360
361 =over
362
363 =item Arguments: $events
364
365 =back
366
367 Actually call the event handlers for this ViewPort. Called by
368 L<apply_our_events>. By default this will do nothing, subclass
369 ViewPort and implement L<accept_events>.
370
371 =head2 accept_events
372
373 =over
374
375 =item Arguments: none
376
377 =back
378
379 Implement this method in a subclass and return a list of events that
380 your ViewPort is accepting.
381
382 =head2 event_id_for
383
384 =over
385
386 =item Arguments: $name
387
388 =back
389
390 Create an id for the given event name and this ViewPort. Generally
391 returns the location and the name, joined with a colon.
392
393 =head2 sort_by_spec
394
395 =over
396
397 =item Arguments: $spec, $items
398
399 =back
400
401 Sorts the given list of items such that the ones that also appear in
402 the spec are at the beginning. This is called by
403 L<Reaction::UI::ViewPort::Action> and
404 L<Reaction::UI::ViewPort::ListView>, and gets passed L<column_order>
405 as the spec argument.
406
407 =head1 AUTHORS
408
409 See L<Reaction::Class> for authors.
410
411 =head1 LICENSE
412
413 See L<Reaction::Class> for the license.
414
415 =cut