27
|
1 #############################################################################
|
|
2 #
|
|
3 # Copyright (c) 2010 by Casey Duncan and contributors
|
|
4 # All Rights Reserved.
|
|
5 #
|
|
6 # This software is subject to the provisions of the MIT License
|
|
7 # A copy of the license should accompany this distribution.
|
|
8 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
11 #
|
|
12 #############################################################################
|
|
13 """
|
|
14 Modes manage the state and transition between different application modes.
|
|
15 Typically such modes are presented as different screens that the user can
|
|
16 navigate between, similar to the way a browser navigates web pages. Individual
|
|
17 modes may be things like:
|
|
18
|
|
19 - Title screen
|
|
20 - Options dialog
|
|
21 - About screen
|
|
22 - In-progress game
|
|
23 - Inventory interface
|
|
24
|
|
25 The modal framework provides a simple mechanism to ensure that modes are
|
|
26 activated and deactivated properly. An activated mode is running and receives
|
|
27 events. A deactivated mode is paused and does not receive events.
|
|
28
|
|
29 Modes may be managed as a *last-in-first-out* stack, or as a list, or ring
|
|
30 of modes in sequence, or some combination of all.
|
|
31
|
|
32 For example usage see: :ref:`the mode section of the tutorial <tut-mode-section>`.
|
|
33 """
|
|
34
|
|
35 __version__ = '$Id$'
|
|
36
|
|
37 import abc
|
|
38
|
|
39
|
|
40 class BaseManager(object):
|
|
41 """Mode manager abstract base class.
|
|
42
|
|
43 The mode manager keeps a stack of modes where a single mode
|
|
44 is active at one time. As modes are pushed on and popped from
|
|
45 the stack, the mode at the top is always active. The current
|
|
46 active mode receives events from the manager's event dispatcher.
|
|
47 """
|
|
48
|
|
49 modes = ()
|
|
50 """The mode stack sequence. The last mode in the stack is
|
|
51 the current active mode. Read-only.
|
|
52 """
|
|
53
|
|
54 @property
|
|
55 def current_mode(self):
|
|
56 """The current active mode or ``None``. Read-only"""
|
|
57 try:
|
|
58 return self.modes[-1]
|
|
59 except IndexError:
|
|
60 return None
|
|
61
|
|
62 def on_last_mode_pop(self, mode):
|
|
63 """Hook executed when the last mode is popped from the manager.
|
|
64 Implementing this method is optional for subclasses.
|
|
65
|
|
66 :param mode: The :class:`Mode` object just popped from the manager
|
|
67 """
|
|
68
|
|
69 def activate_mode(self, mode):
|
|
70 """Perform actions to activate a node
|
|
71
|
|
72 :param mode: The :class: 'Mode' object to activate
|
|
73 """
|
|
74 mode.activate(self)
|
|
75
|
|
76 def deactivate_mode(self, mode):
|
|
77 """Perform actions to deactivate a node
|
|
78
|
|
79 :param mode: The :class: 'Mode' object to deactivate
|
|
80 """
|
|
81 mode.deactivate(self)
|
|
82
|
|
83
|
|
84 def push_mode(self, mode):
|
|
85 """Push a mode to the top of the mode stack and make it active
|
|
86
|
|
87 :param mode: The :class:`Mode` object to make active
|
|
88 """
|
|
89 current = self.current_mode
|
|
90 if current is not None:
|
|
91 self.deactivate_mode(current)
|
|
92 self.modes.append(mode)
|
|
93 self.activate_mode(mode)
|
|
94
|
|
95 def pop_mode(self):
|
|
96 """Pop the current mode off the top of the stack and deactivate it.
|
|
97 The mode now at the top of the stack, if any is then activated.
|
|
98
|
|
99 :param mode: The :class:`Mode` object popped from the stack
|
|
100 """
|
|
101 mode = self.modes.pop()
|
|
102 mode.deactivate(self)
|
|
103 self.event_dispatcher.remove_handlers(mode)
|
|
104 current = self.current_mode
|
|
105 if current is not None:
|
|
106 self.activate_mode(current)
|
|
107 else:
|
|
108 self.on_last_mode_pop(mode)
|
|
109 return mode
|
|
110
|
|
111 def swap_modes(self, mode):
|
|
112 """Exchange the specified mode with the mode at the top of the stack.
|
|
113 This is similar to popping the current mode and pushing the specified
|
|
114 one, but without activating the previous mode on the stack or
|
|
115 executing :meth:`on_last_mode_pop()` if there is no previous mode.
|
|
116
|
|
117 :param mode: The :class:`Mode` object that was deactivated and replaced.
|
|
118 """
|
|
119 old_mode = self.modes.pop()
|
|
120 self.deactivate_mode(old_mode)
|
|
121 self.modes.append(mode)
|
|
122 self.activate_mode(mode)
|
|
123 return old_mode
|
|
124
|
|
125 def remove_mode(self, mode):
|
|
126 """Remove the specified mode. If the mode is at the top of the stack,
|
|
127 this is equivilent to :meth:`pop_mode()`. If not, no other modes
|
|
128 are affected. If the mode is not in the manager, do nothing.
|
|
129
|
|
130 :param mode: The :class:`Mode` object to remove from the manager.
|
|
131 """
|
|
132 if self.current_mode is mode:
|
|
133 self.pop_mode()
|
|
134 else:
|
|
135 try:
|
|
136 self.modes.remove(mode)
|
|
137 except ValueError:
|
|
138 pass
|
|
139
|
|
140 class BaseMode(object):
|
|
141 """Application mode very abstract base class
|
|
142 """
|
|
143 __metaclass__ = abc.ABCMeta
|
|
144
|
|
145 manager = None
|
|
146 """The :class:`BaseManager` that manages this mode"""
|
|
147
|
|
148 def __init__(self):
|
|
149 self.active = False
|
|
150
|
|
151 def on_activate(self):
|
|
152 """Being called when the Mode is activated"""
|
|
153 pass
|
|
154
|
|
155 def activate(self, mode_manager):
|
|
156 """Activate the mode for the given mode manager, if the mode is already active,
|
|
157 do nothing
|
|
158
|
|
159 The default implementation schedules time steps at :attr:`step_rate` per
|
|
160 second, sets the :attr:`manager` and sets the :attr:`active` flag to True.
|
|
161 """
|
|
162 if not self.active:
|
|
163 self.on_activate()
|
|
164 self.manager = mode_manager
|
|
165 self.active = True
|
|
166
|
|
167 def on_deactivate(self):
|
|
168 """Being called when the Mode is deactivated"""
|
|
169 pass
|
|
170
|
|
171 def deactivate(self, mode_manager):
|
|
172 """Deactivate the mode, if the mode is not active, do nothing
|
|
173
|
|
174 The default implementation unschedules time steps for the mode and
|
|
175 sets the :attr:`active` flag to False.
|
|
176 """
|
|
177 self.on_deactivate()
|
|
178 self.active = False
|
|
179
|
|
180
|
|
181 class BaseMulti(BaseMode):
|
|
182 """A mode with multiple submodes. One submode is active at one time.
|
|
183 Submodes can be switched to directly or switched in sequence. If
|
|
184 the Multi is active, then one submode is always active.
|
|
185
|
|
186 Multis are useful when modes can switch in an order other than
|
|
187 a LIFO stack, such as in "hotseat" multiplayer games, a
|
|
188 "wizard" style ui, or a sequence of slides.
|
|
189
|
|
190 Note unlike a normal :class:`Mode`, a :class:`Multi` doesn't have it's own
|
|
191 :attr:`clock` and :attr:`step_rate`. The active submode's are used
|
|
192 instead.
|
|
193 """
|
|
194 active_submode = None
|
|
195 """The currently active submode"""
|
|
196
|
|
197 def __init__(self, *submodes):
|
|
198 # We do not invoke the superclass __init__ intentionally
|
|
199 self.active = False
|
|
200 self.submodes = list(submodes)
|
|
201
|
|
202 def add_submode(self, mode, before=None, index=None):
|
|
203 """Add the submode, but do not make it active.
|
|
204
|
|
205 :param mode: The :class:`Mode` object to add.
|
|
206
|
|
207 :param before: The existing mode to insert the mode before.
|
|
208 If the mode specified is not a submode, raise
|
|
209 ValueError.
|
|
210
|
|
211 :param index: The place to insert the mode in the mode list.
|
|
212 Only one of ``before`` or ``index`` may be specified.
|
|
213
|
|
214 If neither ``before`` or ``index`` are specified, the
|
|
215 mode is appended to the end of the list.
|
|
216 """
|
|
217 assert before is None or index is None, (
|
|
218 "Cannot specify both 'before' and 'index' arguments")
|
|
219 if before is not None:
|
|
220 index = self.submodes.index(mode)
|
|
221 if index is not None:
|
|
222 self.submodes.insert(index, mode)
|
|
223 else:
|
|
224 self.submodes.append(mode)
|
|
225
|
|
226 def remove_submode(self, mode=None):
|
|
227 """Remove the submode.
|
|
228
|
|
229 :param mode: The submode to remove, if omitted the active submode
|
|
230 is removed. If the mode is not present, do nothing. If the
|
|
231 mode is active, it is deactivated, and the next mode, if any
|
|
232 is activated. If the last mode is removed, the :class:`Multi`
|
|
233 is removed from its manager.
|
|
234 """
|
|
235 # TODO handle multiple instances of the same subnode
|
|
236 if mode is None:
|
|
237 mode = self.active_submode
|
|
238 elif mode not in self.submodes:
|
|
239 return
|
|
240 next_mode = self.activate_next()
|
|
241 self.submodes.remove(mode)
|
|
242 if next_mode is mode:
|
|
243 if self.manager is not None:
|
|
244 self.manager.remove_mode(self)
|
|
245 self._deactivate_submode()
|
|
246
|
|
247 def activate_subnode(self, mode, before=None, index=None):
|
|
248 """Activate the specified mode, adding it as a subnode
|
|
249 if it is not already. If the mode is already the active
|
|
250 submode, do nothing.
|
|
251
|
|
252 :param mode: The mode to activate, and add as necesary.
|
|
253
|
|
254 :param before: The existing mode to insert the mode before
|
|
255 if it is not already a submode. If the mode specified is not
|
|
256 a submode, raise ValueError.
|
|
257
|
|
258 :param index: The place to insert the mode in the mode list
|
|
259 if it is not already a submode. Only one of ``before`` or
|
|
260 ``index`` may be specified.
|
|
261
|
|
262 If the mode is already a submode, the ``before`` and ``index``
|
|
263 arguments are ignored.
|
|
264 """
|
|
265 if mode not in self.submodes:
|
|
266 self.add_submode(mode, before, index)
|
|
267 if self.active_submode is not mode:
|
|
268 self._activate_submode(mode)
|
|
269
|
|
270 def activate_next(self, loop=True):
|
|
271 """Activate the submode after the current submode in order. If there
|
|
272 is no current submode, the first submode is activated.
|
|
273
|
|
274 Note if there is only one submode, it's active, and `loop` is True
|
|
275 (the default), then this method does nothing and the subnode remains
|
|
276 active.
|
|
277
|
|
278 :param loop: When :meth:`activate_next` is called
|
|
279 when the last submode is active, a True value for ``loop`` will
|
|
280 cause the first submode to be activated. Otherwise the
|
|
281 :class:`Multi` is removed from its manager.
|
|
282 :type loop: bool
|
|
283
|
|
284 :return:
|
|
285 The submode that was activated or None if there is no
|
|
286 other submode to activate.
|
|
287 """
|
|
288 assert self.submodes, "No submode to activate"
|
|
289 next_mode = None
|
|
290 if self.active_submode is None:
|
|
291 next_mode = self.submodes[0]
|
|
292 else:
|
|
293 last_mode = self.active_submode
|
|
294 index = self.submodes.index(last_mode) + 1
|
|
295 if index < len(self.submodes):
|
|
296 next_mode = self.submodes[index]
|
|
297 elif loop:
|
|
298 next_mode = self.submodes[0]
|
|
299 self._activate_submode(next_mode)
|
|
300 return next_mode
|
|
301
|
|
302 def activate_previous(self, loop=True):
|
|
303 """Activate the submode before the current submode in order. If there
|
|
304 is no current submode, the last submode is activated.
|
|
305
|
|
306 Note if there is only one submode, it's active, and `loop` is True
|
|
307 (the default), then this method does nothing and the subnode remains
|
|
308 active.
|
|
309
|
|
310 :param loop: When :meth:`activate_previous` is called
|
|
311 when the first submode is active, a True value for ``loop`` will
|
|
312 cause the last submode to be activated. Otherwise the
|
|
313 :class:`Multi` is removed from its manager.
|
|
314 :type loop: bool
|
|
315
|
|
316 :return:
|
|
317 The submode that was activated or None if there is no
|
|
318 other submode to activate.
|
|
319 """
|
|
320 assert self.submodes, "No submode to activate"
|
|
321 prev_mode = None
|
|
322 if self.active_submode is None:
|
|
323 prev_mode = self.submodes[-1]
|
|
324 else:
|
|
325 last_mode = self.active_submode
|
|
326 index = self.submodes.index(last_mode) - 1
|
|
327 if loop or index >= 0:
|
|
328 prev_mode = self.submodes[index]
|
|
329 self._activate_submode(prev_mode)
|
|
330 return prev_mode
|
|
331
|
|
332 def _set_active_submode(self, submode):
|
|
333 self.active_submode = submode
|
|
334 self.step_rate = submode.step_rate
|
|
335
|
|
336 def _activate_submode(self, submode):
|
|
337 """Activate a submode deactivating any current submode. If the Multi
|
|
338 itself is active, this happens immediately, otherwise the actual
|
|
339 activation is deferred until the Multi is activated. If the submode
|
|
340 is None, the Mulitmode is removed from its manager.
|
|
341
|
|
342 If submode is already the active submode, do nothing.
|
|
343 """
|
|
344 if self.active_submode is submode:
|
|
345 return
|
|
346 assert submode in self.submodes, "Unknown submode"
|
|
347 self._deactivate_submode()
|
|
348 self._set_active_submode(submode)
|
|
349 if submode is not None:
|
|
350 if self.active:
|
|
351 self.manager.activate_mode(submode)
|
|
352 else:
|
|
353 if self.manager is not None:
|
|
354 self.manager.remove_mode(self)
|
|
355
|
|
356 def clear_subnode(self):
|
|
357 """Clear any subnmode data"""
|
|
358 self.active_submode = None
|
|
359 self.step_rate = None
|
|
360
|
|
361 def _deactivate_submode(self, clear_subnode=True):
|
|
362 """Deactivate the current submode, if any. if `clear_subnode` is
|
|
363 True, `active_submode` is always None when this method returns
|
|
364 """
|
|
365 if self.active_submode is not None:
|
|
366 if self.active:
|
|
367 self.manager.deactivate_mode(self.active_submode)
|
|
368 if clear_subnode:
|
|
369 self.clear_subnode()
|
|
370
|
|
371 def activate(self, mode_manager):
|
|
372 """Activate the :class:`Multi` for the specified manager. The
|
|
373 previously active submode of the :class:`Multi` is activated. If there
|
|
374 is no previously active submode, then the first submode is made active.
|
|
375 A :class:`Multi` with no submodes cannot be activated
|
|
376 """
|
|
377 assert self.submodes, "No submode to activate"
|
|
378 self.manager = mode_manager
|
|
379 if self.active_submode is None:
|
|
380 self._set_active_submode(self.submodes[0])
|
|
381 else:
|
|
382 self._set_active_submode(self.active_submode)
|
|
383 self.manager.activate_mode(self.active_submode)
|
|
384 super(BaseMulti, self).activate(mode_manager)
|
|
385
|
|
386 def deactivate(self, mode_manager):
|
|
387 """Deactivate the :class:`Multi` for the specified manager.
|
|
388 The `active_submode`, if any, is deactivated.
|
|
389 """
|
|
390 self._deactivate_submode(clear_subnode=False)
|
|
391 super(BaseMulti, self).deactivate(mode_manager)
|
|
392
|