1 | """ |
||
2 | This module handles user input. |
||
3 | |||
4 | To handle user input you will likely want to use the :any:`event.get` |
||
5 | function or create a subclass of :any:`event.App`. |
||
6 | |||
7 | - :any:`tdl.event.get` iterates over recent events. |
||
8 | - :any:`tdl.event.App` passes events to the overridable methods: ``ev_*`` |
||
9 | and ``key_*``. |
||
10 | |||
11 | But there are other options such as :any:`event.key_wait` and |
||
12 | :any:`event.is_window_closed`. |
||
13 | |||
14 | A few event attributes are actually string constants. |
||
15 | Here's a reference for those: |
||
16 | |||
17 | - :any:`Event.type`: |
||
18 | 'QUIT', 'KEYDOWN', 'KEYUP', 'MOUSEDOWN', 'MOUSEUP', or 'MOUSEMOTION.' |
||
19 | - :any:`MouseButtonEvent.button` (found in :any:`MouseDown` and |
||
20 | :any:`MouseUp` events): |
||
21 | 'LEFT', 'MIDDLE', 'RIGHT', 'SCROLLUP', 'SCROLLDOWN' |
||
22 | - :any:`KeyEvent.key` (found in :any:`KeyDown` and :any:`KeyUp` events): |
||
23 | 'NONE', 'ESCAPE', 'BACKSPACE', 'TAB', 'ENTER', 'SHIFT', 'CONTROL', |
||
24 | 'ALT', 'PAUSE', 'CAPSLOCK', 'PAGEUP', 'PAGEDOWN', 'END', 'HOME', 'UP', |
||
25 | 'LEFT', 'RIGHT', 'DOWN', 'PRINTSCREEN', 'INSERT', 'DELETE', 'LWIN', |
||
26 | 'RWIN', 'APPS', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', |
||
27 | 'KP0', 'KP1', 'KP2', 'KP3', 'KP4', 'KP5', 'KP6', 'KP7', 'KP8', 'KP9', |
||
28 | 'KPADD', 'KPSUB', 'KPDIV', 'KPMUL', 'KPDEC', 'KPENTER', 'F1', 'F2', |
||
29 | 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', |
||
30 | 'NUMLOCK', 'SCROLLLOCK', 'SPACE', 'CHAR' |
||
31 | """ |
||
32 | |||
33 | import time as _time |
||
34 | |||
35 | from tcod import ffi as _ffi |
||
36 | from tcod import lib as _lib |
||
37 | |||
38 | import tdl as _tdl |
||
39 | from . import style as _style |
||
40 | |||
41 | _eventQueue = [] |
||
42 | _pushedEvents = [] |
||
43 | _eventsflushed = False |
||
44 | |||
45 | _mousel = 0 |
||
46 | _mousem = 0 |
||
47 | _mouser = 0 |
||
48 | |||
49 | # this interprets the constants from libtcod and makes a key -> keyname dictionary |
||
50 | def _parseKeyNames(lib): |
||
51 | """ |
||
52 | returns a dictionary mapping of human readable key names to their keycodes |
||
53 | this parses constants with the names of K_* and makes code=name pairs |
||
54 | this is for KeyEvent.key variable and that enables things like: |
||
55 | if (event.key == 'PAGEUP'): |
||
56 | """ |
||
57 | _keyNames = {} |
||
0 ignored issues
–
show
|
|||
58 | for attr in dir(lib): # from the modules variables |
||
59 | if attr[:6] == 'TCODK_': # get the K_* constants |
||
60 | _keyNames[getattr(lib, attr)] = attr[6:] # and make CODE=NAME pairs |
||
61 | return _keyNames |
||
62 | |||
63 | _keyNames = _parseKeyNames(_lib) |
||
64 | |||
65 | class Event(object): |
||
66 | """Base Event class. |
||
67 | |||
68 | You can easily subclass this to make your own events. Be sure to set |
||
69 | the class attribute L{Event.type} for it to be passed to a custom |
||
70 | :any:`App` ev_* method. |
||
71 | """ |
||
72 | type = None |
||
73 | """String constant representing the type of event. |
||
74 | |||
75 | The :any:`App` ev_* methods depend on this attribute. |
||
76 | |||
77 | Can be: 'QUIT', 'KEYDOWN', 'KEYUP', 'MOUSEDOWN', 'MOUSEUP', |
||
78 | or 'MOUSEMOTION.' |
||
79 | """ |
||
80 | |||
81 | def __repr__(self): |
||
82 | """List an events public attributes when printed. |
||
83 | """ |
||
84 | attrdict = {} |
||
85 | for varname in dir(self): |
||
86 | if '_' == varname[0]: |
||
87 | continue |
||
88 | attrdict[varname] = self.__getattribute__(varname) |
||
89 | return '%s Event %s' % (self.__class__.__name__, repr(attrdict)) |
||
90 | |||
91 | class Quit(Event): |
||
92 | """Fired when the window is closed by the user. |
||
93 | """ |
||
94 | __slots__ = () |
||
95 | type = 'QUIT' |
||
96 | |||
97 | class KeyEvent(Event): |
||
98 | """Base class for key events.""" |
||
99 | |||
100 | def __init__(self, key='', char='', text='', shift=False, |
||
101 | left_alt=False, right_alt=False, |
||
102 | left_control=False, right_control=False, |
||
103 | left_meta=False, right_meta=False): |
||
104 | # Convert keycodes into string, but use string if passed |
||
105 | self.key = key if isinstance(key, str) else _keyNames[key] |
||
106 | """Text: Human readable names of the key pressed. |
||
107 | Non special characters will show up as 'CHAR'. |
||
108 | |||
109 | Can be one of |
||
110 | 'NONE', 'ESCAPE', 'BACKSPACE', 'TAB', 'ENTER', 'SHIFT', 'CONTROL', |
||
111 | 'ALT', 'PAUSE', 'CAPSLOCK', 'PAGEUP', 'PAGEDOWN', 'END', 'HOME', 'UP', |
||
112 | 'LEFT', 'RIGHT', 'DOWN', 'PRINTSCREEN', 'INSERT', 'DELETE', 'LWIN', |
||
113 | 'RWIN', 'APPS', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', |
||
114 | 'KP0', 'KP1', 'KP2', 'KP3', 'KP4', 'KP5', 'KP6', 'KP7', 'KP8', 'KP9', |
||
115 | 'KPADD', 'KPSUB', 'KPDIV', 'KPMUL', 'KPDEC', 'KPENTER', 'F1', 'F2', |
||
116 | 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', |
||
117 | 'NUMLOCK', 'SCROLLLOCK', 'SPACE', 'CHAR' |
||
118 | |||
119 | For the actual character instead of 'CHAR' use :any:`keychar`. |
||
120 | """ |
||
121 | self.char = char.replace('\x00', '') # change null to empty string |
||
122 | """Text: A single character string of the letter or symbol pressed. |
||
123 | |||
124 | Special characters like delete and return are not cross-platform. |
||
125 | L{key} or L{keychar} should be used instead for special keys. |
||
126 | Characters are also case sensitive. |
||
127 | """ |
||
128 | # get the best out of self.key and self.char |
||
129 | self.keychar = self.char if self.key == 'CHAR' else self.key |
||
130 | """Similar to L{key} but returns a case sensitive letter or symbol |
||
131 | instead of 'CHAR'. |
||
132 | |||
133 | This variable makes available the widest variety of symbols and should |
||
134 | be used for key-mappings or anywhere where a narrower sample of keys |
||
135 | isn't needed. |
||
136 | """ |
||
137 | self.text = text |
||
138 | |||
139 | self.left_alt = self.leftAlt = bool(left_alt) |
||
140 | """bool:""" |
||
141 | self.right_alt = self.rightAlt = bool(right_alt) |
||
142 | """bool:""" |
||
143 | self.left_control = self.leftCtrl = bool(left_control) |
||
144 | """bool:""" |
||
145 | self.right_control = self.rightCtrl = bool(right_control) |
||
146 | """bool:""" |
||
147 | self.shift = bool(shift) |
||
148 | """bool: True if shift was held down during this event.""" |
||
149 | self.alt = self.left_alt or self.right_alt |
||
150 | """bool: True if alt was held down during this event.""" |
||
151 | self.control = self.left_control or self.right_control |
||
152 | """bool: True if control was held down during this event.""" |
||
153 | self.left_meta = bool(left_meta) |
||
154 | self.right_meta = bool(right_meta) |
||
155 | self.meta = self.left_meta or self.right_meta |
||
156 | |||
157 | def __repr__(self): |
||
158 | parameters = [] |
||
159 | for attr in ('key', 'char', 'text', 'shift', |
||
160 | 'left_alt', 'right_alt', |
||
161 | 'left_control', 'right_control', |
||
162 | 'left_meta', 'right_meta'): |
||
163 | value = getattr(self, attr) |
||
164 | if value: |
||
165 | parameters.append('%s=%r' % (attr, value)) |
||
166 | return '%s(%s)' % (self.__class__.__name__, ', '.join(parameters)) |
||
167 | |||
168 | class KeyDown(KeyEvent): |
||
169 | """Fired when the user presses a key on the keyboard or a key repeats. |
||
170 | """ |
||
171 | type = 'KEYDOWN' |
||
172 | |||
173 | class KeyUp(KeyEvent): |
||
174 | """Fired when the user releases a key on the keyboard. |
||
175 | """ |
||
176 | type = 'KEYUP' |
||
177 | |||
178 | _mouseNames = {1: 'LEFT', 2: 'MIDDLE', 3: 'RIGHT', 4: 'SCROLLUP', 5: 'SCROLLDOWN'} |
||
179 | class MouseButtonEvent(Event): |
||
180 | """Base class for mouse button events.""" |
||
181 | |||
182 | def __init__(self, button, pos, cell): |
||
183 | self.button = _mouseNames[button] |
||
184 | """Text: Can be one of |
||
185 | 'LEFT', 'MIDDLE', 'RIGHT', 'SCROLLUP', 'SCROLLDOWN' |
||
186 | """ |
||
187 | self.pos = pos |
||
188 | """Tuple[int, int]: (x, y) position of the mouse on the screen.""" |
||
189 | self.cell = cell |
||
190 | """Tuple[int, int]: (x, y) position of the mouse snapped to a cell on |
||
191 | the root console |
||
192 | """ |
||
193 | |||
194 | class MouseDown(MouseButtonEvent): |
||
195 | """Fired when a mouse button is pressed.""" |
||
196 | __slots__ = () |
||
197 | type = 'MOUSEDOWN' |
||
198 | |||
199 | class MouseUp(MouseButtonEvent): |
||
200 | """Fired when a mouse button is released.""" |
||
201 | __slots__ = () |
||
202 | type = 'MOUSEUP' |
||
203 | |||
204 | class MouseMotion(Event): |
||
205 | """Fired when the mouse is moved.""" |
||
206 | type = 'MOUSEMOTION' |
||
207 | |||
208 | def __init__(self, pos, cell, motion, cellmotion): |
||
209 | self.pos = pos |
||
210 | """(x, y) position of the mouse on the screen. |
||
211 | type: (int, int)""" |
||
212 | self.cell = cell |
||
213 | """(x, y) position of the mouse snapped to a cell on the root console. |
||
214 | type: (int, int)""" |
||
215 | self.motion = motion |
||
216 | """(x, y) motion of the mouse on the screen. |
||
217 | type: (int, int)""" |
||
218 | self.cellmotion = cellmotion |
||
219 | """(x, y) mostion of the mouse moving over cells on the root console. |
||
220 | type: (int, int)""" |
||
221 | |||
222 | class App(object): |
||
223 | """ |
||
224 | Application framework. |
||
225 | |||
226 | - ev_*: Events are passed to methods based on their :any:`Event.type` |
||
227 | attribute. |
||
228 | If an event type is 'KEYDOWN' the ev_KEYDOWN method will be called |
||
229 | with the event instance as a parameter. |
||
230 | |||
231 | - key_*: When a key is pressed another method will be called based on the |
||
232 | :any:`KeyEvent.key` attribute. For example the 'ENTER' key will call |
||
233 | key_ENTER with the associated :any:`KeyDown` event as its parameter. |
||
234 | |||
235 | - :any:`update`: This method is called every loop. It is passed a single |
||
236 | parameter detailing the time in seconds since the last update |
||
237 | (often known as deltaTime.) |
||
238 | |||
239 | You may want to call drawing routines in this method followed by |
||
240 | :any:`tdl.flush`. |
||
241 | |||
242 | """ |
||
243 | __slots__ = ('__running', '__prevTime') |
||
244 | |||
245 | def ev_QUIT(self, event): |
||
246 | """Unless overridden this method raises a SystemExit exception closing |
||
247 | the program.""" |
||
248 | raise SystemExit() |
||
249 | |||
250 | def ev_KEYDOWN(self, event): |
||
251 | """Override this method to handle a :any:`KeyDown` event.""" |
||
252 | |||
253 | def ev_KEYUP(self, event): |
||
254 | """Override this method to handle a :any:`KeyUp` event.""" |
||
255 | |||
256 | def ev_MOUSEDOWN(self, event): |
||
257 | """Override this method to handle a :any:`MouseDown` event.""" |
||
258 | |||
259 | def ev_MOUSEUP(self, event): |
||
260 | """Override this method to handle a :any:`MouseUp` event.""" |
||
261 | |||
262 | def ev_MOUSEMOTION(self, event): |
||
263 | """Override this method to handle a :any:`MouseMotion` event.""" |
||
264 | |||
265 | def update(self, deltaTime): |
||
266 | """Override this method to handle per frame logic and drawing. |
||
267 | |||
268 | Args: |
||
269 | deltaTime (float): |
||
270 | This parameter tells the amount of time passed since |
||
271 | the last call measured in seconds as a floating point |
||
272 | number. |
||
273 | |||
274 | You can use this variable to make your program |
||
275 | frame rate independent. |
||
276 | Use this parameter to adjust the speed of motion, |
||
277 | timers, and other game logic. |
||
278 | """ |
||
279 | pass |
||
280 | |||
281 | def suspend(self): |
||
282 | """When called the App will begin to return control to where |
||
283 | :any:`App.run` was called. |
||
284 | |||
285 | Some further events are processed and the :any:`App.update` method |
||
286 | will be called one last time before exiting |
||
287 | (unless suspended during a call to :any:`App.update`.) |
||
288 | """ |
||
289 | self.__running = False |
||
290 | |||
291 | def run(self): |
||
292 | """Delegate control over to this App instance. This function will |
||
293 | process all events and send them to the special methods ev_* and key_*. |
||
294 | |||
295 | A call to :any:`App.suspend` will return the control flow back to where |
||
296 | this function is called. And then the App can be run again. |
||
297 | But a single App instance can not be run multiple times simultaneously. |
||
298 | """ |
||
299 | if getattr(self, '_App__running', False): |
||
300 | raise _tdl.TDLError('An App can not be run multiple times simultaneously') |
||
301 | self.__running = True |
||
302 | while self.__running: |
||
303 | self.runOnce() |
||
304 | |||
305 | def run_once(self): |
||
306 | """Pump events to this App instance and then return. |
||
307 | |||
308 | This works in the way described in :any:`App.run` except it immediately |
||
309 | returns after the first :any:`update` call. |
||
310 | |||
311 | Having multiple :any:`App` instances and selectively calling runOnce on |
||
312 | them is a decent way to create a state machine. |
||
313 | """ |
||
314 | if not hasattr(self, '_App__prevTime'): |
||
315 | self.__prevTime = _time.clock() # initiate __prevTime |
||
316 | for event in get(): |
||
317 | if event.type: # exclude custom events with a blank type variable |
||
318 | # call the ev_* methods |
||
319 | method = 'ev_%s' % event.type # ev_TYPE |
||
320 | getattr(self, method)(event) |
||
321 | if event.type == 'KEYDOWN': |
||
322 | # call the key_* methods |
||
323 | method = 'key_%s' % event.key # key_KEYNAME |
||
324 | if hasattr(self, method): # silently exclude undefined methods |
||
325 | getattr(self, method)(event) |
||
326 | newTime = _time.clock() |
||
327 | self.update(newTime - self.__prevTime) |
||
328 | self.__prevTime = newTime |
||
329 | #_tdl.flush() |
||
330 | |||
331 | def _processEvents(): |
||
332 | """Flushes the event queue from libtcod into the global list _eventQueue""" |
||
333 | global _mousel, _mousem, _mouser, _eventsflushed, _pushedEvents |
||
334 | _eventsflushed = True |
||
335 | events = _pushedEvents # get events from event.push |
||
336 | _pushedEvents = [] # then clear the pushed events queue |
||
337 | |||
338 | mouse = _ffi.new('TCOD_mouse_t *') |
||
339 | libkey = _ffi.new('TCOD_key_t *') |
||
340 | while 1: |
||
341 | libevent = _lib.TCOD_sys_check_for_event(_lib.TCOD_EVENT_ANY, libkey, mouse) |
||
342 | if not libevent: # no more events from libtcod |
||
343 | break |
||
344 | |||
345 | #if mouse.dx or mouse.dy: |
||
346 | if libevent & _lib.TCOD_EVENT_MOUSE_MOVE: |
||
347 | events.append(MouseMotion((mouse.x, mouse.y), |
||
348 | (mouse.cx, mouse.cy), |
||
349 | (mouse.dx, mouse.dy), |
||
350 | (mouse.dcx, mouse.dcy))) |
||
351 | |||
352 | mousepos = ((mouse.x, mouse.y), (mouse.cx, mouse.cy)) |
||
353 | |||
354 | for oldstate, newstate, released, button in \ |
||
355 | zip((_mousel, _mousem, _mouser), |
||
356 | (mouse.lbutton, mouse.mbutton, mouse.rbutton), |
||
357 | (mouse.lbutton_pressed, mouse.mbutton_pressed, |
||
358 | mouse.rbutton_pressed), |
||
359 | (1, 2, 3)): |
||
360 | if released: |
||
361 | if not oldstate: |
||
362 | events.append(MouseDown(button, *mousepos)) |
||
363 | events.append(MouseUp(button, *mousepos)) |
||
364 | if newstate: |
||
365 | events.append(MouseDown(button, *mousepos)) |
||
366 | elif newstate and not oldstate: |
||
367 | events.append(MouseDown(button, *mousepos)) |
||
368 | |||
369 | if mouse.wheel_up: |
||
370 | events.append(MouseDown(4, *mousepos)) |
||
371 | if mouse.wheel_down: |
||
372 | events.append(MouseDown(5, *mousepos)) |
||
373 | |||
374 | _mousel = mouse.lbutton |
||
375 | _mousem = mouse.mbutton |
||
376 | _mouser = mouse.rbutton |
||
377 | |||
378 | if libkey.vk == _lib.TCODK_NONE: |
||
379 | break |
||
380 | if libkey.pressed: |
||
381 | keyevent = KeyDown |
||
382 | else: |
||
383 | keyevent = KeyUp |
||
384 | |||
385 | events.append( |
||
386 | keyevent( |
||
387 | libkey.vk, |
||
388 | libkey.c.decode('ascii', errors='ignore'), |
||
389 | _ffi.string(libkey.text).decode('utf-8'), |
||
390 | libkey.shift, |
||
391 | libkey.lalt, |
||
392 | libkey.ralt, |
||
393 | libkey.lctrl, |
||
394 | libkey.rctrl, |
||
395 | libkey.lmeta, |
||
396 | libkey.rmeta, |
||
397 | ) |
||
398 | ) |
||
399 | |||
400 | if _lib.TCOD_console_is_window_closed(): |
||
401 | events.append(Quit()) |
||
402 | |||
403 | _eventQueue.extend(events) |
||
404 | |||
405 | def get(): |
||
406 | """Flushes the event queue and returns the list of events. |
||
407 | |||
408 | This function returns :any:`Event` objects that can be identified by their |
||
409 | type attribute or their class. |
||
410 | |||
411 | Returns: Iterator[Type[Event]]: An iterable of Events or anything |
||
412 | put in a :any:`push` call. |
||
413 | |||
414 | If the iterator is deleted or otherwise interrupted before finishing |
||
415 | the excess items are preserved for the next call. |
||
416 | """ |
||
417 | _processEvents() |
||
418 | return _event_generator() |
||
419 | |||
420 | def _event_generator(): |
||
421 | while _eventQueue: |
||
422 | # if there is an interruption the rest of the events stay untouched |
||
423 | # this means you can break out of a event.get loop without losing |
||
424 | # the leftover events |
||
425 | yield(_eventQueue.pop(0)) |
||
426 | raise StopIteration() |
||
427 | |||
428 | |||
429 | def wait(timeout=None, flush=True): |
||
430 | """Wait for an event. |
||
431 | |||
432 | Args: |
||
433 | timeout (Optional[int]): The time in seconds that this function will |
||
434 | wait before giving up and returning None. |
||
435 | |||
436 | With the default value of None, this will block forever. |
||
437 | flush (bool): If True a call to :any:`tdl.flush` will be made before |
||
438 | listening for events. |
||
439 | |||
440 | Returns: Type[Event]: An event, or None if the function |
||
441 | has timed out. |
||
442 | Anything added via :any:`push` will also be returned. |
||
443 | """ |
||
444 | if timeout is not None: |
||
445 | timeout = timeout + _time.clock() # timeout at this time |
||
446 | while True: |
||
447 | if _eventQueue: |
||
448 | return _eventQueue.pop(0) |
||
449 | if flush: |
||
450 | # a full 'round' of events need to be processed before flushing |
||
451 | _tdl.flush() |
||
452 | if timeout and _time.clock() >= timeout: |
||
453 | return None # return None on timeout |
||
454 | _time.sleep(0.001) # sleep 1ms |
||
455 | _processEvents() |
||
456 | |||
457 | |||
458 | def push(event): |
||
459 | """Push an event into the event buffer. |
||
460 | |||
461 | Args: |
||
462 | event (Any): This event will be available on the next call to |
||
463 | :any:`event.get`. |
||
464 | |||
465 | An event pushed in the middle of a :any:`get` will not show until |
||
466 | the next time :any:`get` called preventing push related |
||
467 | infinite loops. |
||
468 | |||
469 | This object should at least have a 'type' attribute. |
||
470 | """ |
||
471 | _pushedEvents.append(event) |
||
472 | |||
473 | def key_wait(): |
||
474 | """Waits until the user presses a key. |
||
475 | Then returns a :any:`KeyDown` event. |
||
476 | |||
477 | Key events will repeat if held down. |
||
478 | |||
479 | A click to close the window will be converted into an Alt+F4 KeyDown event. |
||
480 | |||
481 | Returns: |
||
482 | tdl.event.KeyDown: The pressed key. |
||
483 | """ |
||
484 | while 1: |
||
485 | for event in get(): |
||
486 | if event.type == 'KEYDOWN': |
||
487 | return event |
||
488 | if event.type == 'QUIT': |
||
489 | # convert QUIT into alt+F4 |
||
490 | return KeyDown('F4', '', True, False, True, False, False) |
||
491 | _time.sleep(.001) |
||
492 | |||
493 | def set_key_repeat(delay=500, interval=0): |
||
494 | """Does nothing. |
||
495 | """ |
||
496 | pass |
||
497 | |||
498 | def is_window_closed(): |
||
499 | """Returns True if the exit button on the window has been clicked and |
||
500 | stays True afterwards. |
||
501 | |||
502 | Returns: bool: |
||
503 | """ |
||
504 | return _lib.TCOD_console_is_window_closed() |
||
505 | |||
506 | __all__ = [_var for _var in locals().keys() if _var[0] != '_'] |
||
507 | |||
508 | App.runOnce = _style.backport(App.run_once) |
||
509 | keyWait = _style.backport(key_wait) |
||
510 | setKeyRepeat = _style.backport(set_key_repeat) |
||
511 | isWindowClosed = _style.backport(is_window_closed) |
||
512 | |||
513 |
It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior: