search spec components factored out of T365
[catagits/Reaction.git] / lib / Reaction / UI / FocusStack.pm
1 package Reaction::UI::FocusStack;
2
3 use Reaction::Class;
4
5 use namespace::clean -except => [ qw(meta) ];
6
7
8 has vp_head => (
9   isa => 'Reaction::UI::ViewPort', is => 'rw',
10   clearer => 'clear_vp_head',
11 );
12 has vp_tail => (
13   isa => 'Reaction::UI::ViewPort', is => 'rw',
14   clearer => 'clear_vp_tail',
15 );
16 has vp_count => (
17   isa => 'Int', is => 'rw', required => 1, default => sub { 0 }
18 );
19 has loc_prefix => (isa => 'Str', is => 'rw', predicate => 'has_loc_prefix');
20 sub push_viewport {
21   my ($self, $class, %create) = @_;
22   my $tail = $self->vp_tail;
23   my $loc = $self->vp_count;
24   if ($self->has_loc_prefix) {
25     $loc = join('-', $self->loc_prefix, $loc);
26   }
27   my $vp = $class->new(
28              %create,
29              location => $loc,
30              focus_stack => $self,
31              (defined $tail ? ( outer => $tail ) : ()), # XXX possibly a bug in
32                                                         #immutable?
33            );
34   if ($tail) {           # if we already have a tail (non-empty vp stack)
35     $tail->inner($vp);     # set the current tail's inner vp to the new vp
36   } else {               # else we're currently an empty stack
37     $self->vp_head($vp);   # so set the head to the new vp
38   }
39   $self->vp_count($self->vp_count + 1);
40   $self->vp_tail($vp);
41   return $vp;
42 };
43
44 sub pop_viewport {
45   my ($self) = @_;
46   my $head = $self->vp_head;
47   confess "Can't pop from empty focus stack" unless defined($head);
48   my $vp = $self->vp_tail;
49   if ($vp eq $head) {
50     $self->clear_vp_head;
51     $self->clear_vp_tail;
52   } else {
53     $self->vp_tail($vp->outer);
54   }
55   $self->vp_count($self->vp_count - 1);
56   return $vp;
57 };
58
59 sub pop_viewports_to {
60   my ($self, $vp) = @_;
61   1 while ($self->pop_viewport ne $vp);
62   return $vp;
63 };
64
65 sub apply_events {
66   my $self = shift;
67   my $all_events = shift;
68   my $vp = $self->vp_tail;
69
70   while (defined $vp && keys %$all_events) {
71     my $loc = $vp->location;
72     my %vp_events = map { $_ => delete $all_events->{$_} }
73       grep { /^${loc}[-:]/ } keys %$all_events;
74     $vp->apply_events(\%vp_events);
75     $vp = $vp->outer;
76   }
77 };
78
79
80 __PACKAGE__->meta->make_immutable;
81
82
83 1;
84
85 =head1 NAME
86
87 Reaction::UI::FocusStack - A linked list of ViewPort-based objects
88
89 =head1 SYNOPSIS
90
91   my $stack = Reaction::UI::FocusStack->new();
92
93   # Or more commonly, in a Reaction::UI::RootController based
94   # Catalyst Controller:
95   my $stack = $ctx->focus_stack;
96
97   # Add a new basic viewport inside the last viewport on the stack:
98   my $vp = $stack->push_viewport('Reaction::UI::ViewPort' => 
99                                   layout => 'xhtml'
100                                 );
101
102   # Fetch the innermost viewport from the stack:
103   my $vp = $stack->pop_viewport();
104
105   # Remove all viewports inside a given viewport:
106   $stack->pop_viewports_to($vp);
107
108   # Create a named stack as a tangent to an existing viewport:
109   my $newstack = $vp->create_tangent('somename');
110
111   # Resolve current events using your stack:
112   # This is called by Reaction::UI::RootController in the end action.
113   $stack->apply_events($ctx, $param_hash);
114
115 =head1 DESCRIPTION
116
117 A FocusStack represents a list of related L<ViewPort|Reaction::UI::ViewPort>
118 objects. The L<Reaction::UI::RootController> creates an empty stack for you in
119 it's begin action, which represents the main thread/container of the page.
120 Typically you add new ViewPorts to this stack as the main parts of your page.
121 To add multiple parallel page subparts, create a tangent from the outer
122 viewport, and add more viewports as normal.
123
124 =head1 METHODS
125
126 =head2 new
127
128 =over
129
130 =item Arguments: none
131
132 =back
133
134 Create a new empty FocusStack. This is done for you in
135 L<Reaction::UI::RootController>.
136
137 =head2 push_viewport
138
139 =over
140
141 =item Arguments: $class, %options
142
143 =back
144
145 Creates a new L<Reaction::UI::ViewPort> based object and adds it to the stack.
146
147 The following attributes of the new ViewPort are set:
148
149 =over 
150
151 =item outer
152
153 Is set to the preceding ViewPort in the stack.
154
155 =item focus_stack
156
157 Is set to the FocusStack object that created the ViewPort.
158
159 =item location
160
161 Is set to the location of the ViewPort in the stack.
162
163 =back
164
165 =head2 pop_viewport
166
167 =over 
168
169 =item Arguments: none
170
171 =back
172
173 Removes the last/innermost ViewPort from the stack and returns it.
174
175 =head2 pop_viewports_to
176
177 =over 
178
179 =item Arguments: $viewport
180
181 =back
182
183 Pops all ViewPorts off the stack until the given ViewPort object
184 remains as the last item. If passed a $viewport not on the stack, this
185 will empty the stack completely (and then die complainingly).
186
187 TODO: Should pop_viewports_to check $vp->focus_stack eq $self first?
188
189 =head2 vp_head
190
191 =over
192
193 =item Arguments: none
194
195 =back
196
197 Retrieve the first ViewPort in this stack. Useful for calling
198 L<Reaction::UI::Window/render_viewport> on a
199 L<Reaction::UI::ViewPort/focus_tangent>.
200
201 =head2 vp_head
202
203 =over 
204
205 =item Arguments: none
206
207 =back
208
209 Retrieve the first ViewPort in this stack. Useful for calling
210 L<Reaction::UI::Window/render_viewport> on a
211 L<Reaction::UI::ViewPort/focus_tangent>.
212
213 =head2 vp_tail
214
215 =over 
216
217 =item Arguments: none
218
219 =back
220
221 Retrieve the last ViewPort in this stack. Useful for calling
222 L<Reaction::UI::Window/render_viewport> on a
223 L<Reaction::UI::ViewPort/focus_tangent>.
224
225 =head2 vp_count
226
227 =over 
228
229 =item Arguments: none
230
231 =back
232
233 =head2 loc_prefix
234
235 =head2 apply_events
236
237 =over 
238
239 =item Arguments: $ctx, $params_hashref
240
241 =back
242
243 Instruct each of the ViewPorts in the stack to apply the given events
244 to each of it's tangent stacks, and then to itself. These are applied
245 starting with the last/innermost ViewPort first.
246
247 =head1 AUTHORS
248
249 See L<Reaction::Class> for authors.
250
251 =head1 LICENSE
252
253 See L<Reaction::Class> for the license.
254
255 =cut