Source code for tdl.event

"""
    This module handles user input.

    To handle user input you will likely want to use the :any:`event.get`
    function or create a subclass of :any:`event.App`.

    - :any:`tdl.event.get` iterates over recent events.
    - :any:`tdl.event.App` passes events to the overridable methods: ``ev_*``
      and ``key_*``.

    But there are other options such as :any:`event.key_wait` and
    :any:`event.is_window_closed`.

    A few event attributes are actually string constants.
    Here's a reference for those:

    - :any:`Event.type`:
      'QUIT', 'KEYDOWN', 'KEYUP', 'MOUSEDOWN', 'MOUSEUP', or 'MOUSEMOTION.'
    - :any:`MouseButtonEvent.button` (found in :any:`MouseDown` and
      :any:`MouseUp` events):
      'LEFT', 'MIDDLE', 'RIGHT', 'SCROLLUP', 'SCROLLDOWN'
    - :any:`KeyEvent.key` (found in :any:`KeyDown` and :any:`KeyUp` events):
      'NONE', 'ESCAPE', 'BACKSPACE', 'TAB', 'ENTER', 'SHIFT', 'CONTROL',
      'ALT', 'PAUSE', 'CAPSLOCK', 'PAGEUP', 'PAGEDOWN', 'END', 'HOME', 'UP',
      'LEFT', 'RIGHT', 'DOWN', 'PRINTSCREEN', 'INSERT', 'DELETE', 'LWIN',
      'RWIN', 'APPS', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
      'KP0', 'KP1', 'KP2', 'KP3', 'KP4', 'KP5', 'KP6', 'KP7', 'KP8', 'KP9',
      'KPADD', 'KPSUB', 'KPDIV', 'KPMUL', 'KPDEC', 'KPENTER', 'F1', 'F2',
      'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
      'NUMLOCK', 'SCROLLLOCK', 'SPACE', 'CHAR'
"""

import time as _time

from tcod import ffi as _ffi
from tcod import lib as _lib

import tdl as _tdl
from . import style as _style

_eventQueue = []
_pushedEvents = []
_eventsflushed = False

_mousel = 0
_mousem = 0
_mouser = 0

# this interprets the constants from libtcod and makes a key -> keyname dictionary
def _parseKeyNames(lib):
    """
    returns a dictionary mapping of human readable key names to their keycodes
    this parses constants with the names of K_* and makes code=name pairs
    this is for KeyEvent.key variable and that enables things like:
    if (event.key == 'PAGEUP'):
    """
    _keyNames = {}
    for attr in dir(lib): # from the modules variables
        if attr[:6] == 'TCODK_': # get the K_* constants
            _keyNames[getattr(lib, attr)] = attr[6:] # and make CODE=NAME pairs
    return _keyNames

_keyNames = _parseKeyNames(_lib)

[docs]class Event(object): """Base Event class. You can easily subclass this to make your own events. Be sure to set the class attribute L{Event.type} for it to be passed to a custom :any:`App` ev_* method. """ type = None """String constant representing the type of event. The :any:`App` ev_* methods depend on this attribute. Can be: 'QUIT', 'KEYDOWN', 'KEYUP', 'MOUSEDOWN', 'MOUSEUP', or 'MOUSEMOTION.' """
[docs] def __repr__(self): """List an events public attributes when printed. """ attrdict = {} for varname in dir(self): if '_' == varname[0]: continue attrdict[varname] = self.__getattribute__(varname) return '%s Event %s' % (self.__class__.__name__, repr(attrdict))
[docs]class Quit(Event): """Fired when the window is closed by the user. """ __slots__ = () type = 'QUIT'
[docs]class KeyEvent(Event): """Base class for key events.""" def __init__(self, key='', char='', text='', shift=False, left_alt=False, right_alt=False, left_control=False, right_control=False, left_meta=False, right_meta=False): # Convert keycodes into string, but use string if passed self.key = key if isinstance(key, str) else _keyNames[key] """Text: Human readable names of the key pressed. Non special characters will show up as 'CHAR'. Can be one of 'NONE', 'ESCAPE', 'BACKSPACE', 'TAB', 'ENTER', 'SHIFT', 'CONTROL', 'ALT', 'PAUSE', 'CAPSLOCK', 'PAGEUP', 'PAGEDOWN', 'END', 'HOME', 'UP', 'LEFT', 'RIGHT', 'DOWN', 'PRINTSCREEN', 'INSERT', 'DELETE', 'LWIN', 'RWIN', 'APPS', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'KP0', 'KP1', 'KP2', 'KP3', 'KP4', 'KP5', 'KP6', 'KP7', 'KP8', 'KP9', 'KPADD', 'KPSUB', 'KPDIV', 'KPMUL', 'KPDEC', 'KPENTER', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'NUMLOCK', 'SCROLLLOCK', 'SPACE', 'CHAR' For the actual character instead of 'CHAR' use :any:`keychar`. """ self.char = char.replace('\x00', '') # change null to empty string """Text: A single character string of the letter or symbol pressed. Special characters like delete and return are not cross-platform. L{key} or L{keychar} should be used instead for special keys. Characters are also case sensitive. """ # get the best out of self.key and self.char self.keychar = self.char if self.key == 'CHAR' else self.key """Similar to L{key} but returns a case sensitive letter or symbol instead of 'CHAR'. This variable makes available the widest variety of symbols and should be used for key-mappings or anywhere where a narrower sample of keys isn't needed. """ self.text = text self.left_alt = self.leftAlt = bool(left_alt) """bool:""" self.right_alt = self.rightAlt = bool(right_alt) """bool:""" self.left_control = self.leftCtrl = bool(left_control) """bool:""" self.right_control = self.rightCtrl = bool(right_control) """bool:""" self.shift = bool(shift) """bool: True if shift was held down during this event.""" self.alt = self.left_alt or self.right_alt """bool: True if alt was held down during this event.""" self.control = self.left_control or self.right_control """bool: True if control was held down during this event.""" self.left_meta = bool(left_meta) self.right_meta = bool(right_meta) self.meta = self.left_meta or self.right_meta def __repr__(self): parameters = [] for attr in ('key', 'char', 'text', 'shift', 'left_alt', 'right_alt', 'left_control', 'right_control', 'left_meta', 'right_meta'): value = getattr(self, attr) if value: parameters.append('%s=%r' % (attr, value)) return '%s(%s)' % (self.__class__.__name__, ', '.join(parameters))
[docs]class KeyDown(KeyEvent): """Fired when the user presses a key on the keyboard or a key repeats. """ type = 'KEYDOWN'
[docs]class KeyUp(KeyEvent): """Fired when the user releases a key on the keyboard. """ type = 'KEYUP'
_mouseNames = {1: 'LEFT', 2: 'MIDDLE', 3: 'RIGHT', 4: 'SCROLLUP', 5: 'SCROLLDOWN'}
[docs]class MouseButtonEvent(Event): """Base class for mouse button events.""" def __init__(self, button, pos, cell): self.button = _mouseNames[button] """Text: Can be one of 'LEFT', 'MIDDLE', 'RIGHT', 'SCROLLUP', 'SCROLLDOWN' """ self.pos = pos """Tuple[int, int]: (x, y) position of the mouse on the screen.""" self.cell = cell """Tuple[int, int]: (x, y) position of the mouse snapped to a cell on the root console """
[docs]class MouseDown(MouseButtonEvent): """Fired when a mouse button is pressed.""" __slots__ = () type = 'MOUSEDOWN'
[docs]class MouseUp(MouseButtonEvent): """Fired when a mouse button is released.""" __slots__ = () type = 'MOUSEUP'
[docs]class MouseMotion(Event): """Fired when the mouse is moved.""" type = 'MOUSEMOTION' def __init__(self, pos, cell, motion, cellmotion): self.pos = pos """(x, y) position of the mouse on the screen. type: (int, int)""" self.cell = cell """(x, y) position of the mouse snapped to a cell on the root console. type: (int, int)""" self.motion = motion """(x, y) motion of the mouse on the screen. type: (int, int)""" self.cellmotion = cellmotion """(x, y) mostion of the mouse moving over cells on the root console. type: (int, int)"""
[docs]class App(object): """ Application framework. - ev_*: Events are passed to methods based on their :any:`Event.type` attribute. If an event type is 'KEYDOWN' the ev_KEYDOWN method will be called with the event instance as a parameter. - key_*: When a key is pressed another method will be called based on the :any:`KeyEvent.key` attribute. For example the 'ENTER' key will call key_ENTER with the associated :any:`KeyDown` event as its parameter. - :any:`update`: This method is called every loop. It is passed a single parameter detailing the time in seconds since the last update (often known as deltaTime.) You may want to call drawing routines in this method followed by :any:`tdl.flush`. """ __slots__ = ('__running', '__prevTime')
[docs] def ev_QUIT(self, event): """Unless overridden this method raises a SystemExit exception closing the program.""" raise SystemExit()
[docs] def ev_KEYDOWN(self, event): """Override this method to handle a :any:`KeyDown` event."""
[docs] def ev_KEYUP(self, event): """Override this method to handle a :any:`KeyUp` event."""
[docs] def ev_MOUSEDOWN(self, event): """Override this method to handle a :any:`MouseDown` event."""
[docs] def ev_MOUSEUP(self, event): """Override this method to handle a :any:`MouseUp` event."""
[docs] def ev_MOUSEMOTION(self, event): """Override this method to handle a :any:`MouseMotion` event."""
[docs] def update(self, deltaTime): """Override this method to handle per frame logic and drawing. Args: deltaTime (float): This parameter tells the amount of time passed since the last call measured in seconds as a floating point number. You can use this variable to make your program frame rate independent. Use this parameter to adjust the speed of motion, timers, and other game logic. """ pass
[docs] def suspend(self): """When called the App will begin to return control to where :any:`App.run` was called. Some further events are processed and the :any:`App.update` method will be called one last time before exiting (unless suspended during a call to :any:`App.update`.) """ self.__running = False
[docs] def run(self): """Delegate control over to this App instance. This function will process all events and send them to the special methods ev_* and key_*. A call to :any:`App.suspend` will return the control flow back to where this function is called. And then the App can be run again. But a single App instance can not be run multiple times simultaneously. """ if getattr(self, '_App__running', False): raise _tdl.TDLError('An App can not be run multiple times simultaneously') self.__running = True while self.__running: self.runOnce()
[docs] def run_once(self): """Pump events to this App instance and then return. This works in the way described in :any:`App.run` except it immediately returns after the first :any:`update` call. Having multiple :any:`App` instances and selectively calling runOnce on them is a decent way to create a state machine. """ if not hasattr(self, '_App__prevTime'): self.__prevTime = _time.clock() # initiate __prevTime for event in get(): if event.type: # exclude custom events with a blank type variable # call the ev_* methods method = 'ev_%s' % event.type # ev_TYPE getattr(self, method)(event) if event.type == 'KEYDOWN': # call the key_* methods method = 'key_%s' % event.key # key_KEYNAME if hasattr(self, method): # silently exclude undefined methods getattr(self, method)(event) newTime = _time.clock() self.update(newTime - self.__prevTime) self.__prevTime = newTime
#_tdl.flush() def _processEvents(): """Flushes the event queue from libtcod into the global list _eventQueue""" global _mousel, _mousem, _mouser, _eventsflushed, _pushedEvents _eventsflushed = True events = _pushedEvents # get events from event.push _pushedEvents = [] # then clear the pushed events queue mouse = _ffi.new('TCOD_mouse_t *') libkey = _ffi.new('TCOD_key_t *') while 1: libevent = _lib.TCOD_sys_check_for_event(_lib.TCOD_EVENT_ANY, libkey, mouse) if not libevent: # no more events from libtcod break #if mouse.dx or mouse.dy: if libevent & _lib.TCOD_EVENT_MOUSE_MOVE: events.append(MouseMotion((mouse.x, mouse.y), (mouse.cx, mouse.cy), (mouse.dx, mouse.dy), (mouse.dcx, mouse.dcy))) mousepos = ((mouse.x, mouse.y), (mouse.cx, mouse.cy)) for oldstate, newstate, released, button in \ zip((_mousel, _mousem, _mouser), (mouse.lbutton, mouse.mbutton, mouse.rbutton), (mouse.lbutton_pressed, mouse.mbutton_pressed, mouse.rbutton_pressed), (1, 2, 3)): if released: if not oldstate: events.append(MouseDown(button, *mousepos)) events.append(MouseUp(button, *mousepos)) if newstate: events.append(MouseDown(button, *mousepos)) elif newstate and not oldstate: events.append(MouseDown(button, *mousepos)) if mouse.wheel_up: events.append(MouseDown(4, *mousepos)) if mouse.wheel_down: events.append(MouseDown(5, *mousepos)) _mousel = mouse.lbutton _mousem = mouse.mbutton _mouser = mouse.rbutton if libkey.vk == _lib.TCODK_NONE: break if libkey.pressed: keyevent = KeyDown else: keyevent = KeyUp events.append( keyevent( libkey.vk, libkey.c.decode('ascii', errors='ignore'), _ffi.string(libkey.text).decode('utf-8'), libkey.shift, libkey.lalt, libkey.ralt, libkey.lctrl, libkey.rctrl, libkey.lmeta, libkey.rmeta, ) ) if _lib.TCOD_console_is_window_closed(): events.append(Quit()) _eventQueue.extend(events)
[docs]def get(): """Flushes the event queue and returns the list of events. This function returns :any:`Event` objects that can be identified by their type attribute or their class. Returns: Iterator[Type[Event]]: An iterable of Events or anything put in a :any:`push` call. If the iterator is deleted or otherwise interrupted before finishing the excess items are preserved for the next call. """ _processEvents() return _event_generator()
def _event_generator(): while _eventQueue: # if there is an interruption the rest of the events stay untouched # this means you can break out of a event.get loop without losing # the leftover events yield(_eventQueue.pop(0))
[docs]def wait(timeout=None, flush=True): """Wait for an event. Args: timeout (Optional[int]): The time in seconds that this function will wait before giving up and returning None. With the default value of None, this will block forever. flush (bool): If True a call to :any:`tdl.flush` will be made before listening for events. Returns: Type[Event]: An event, or None if the function has timed out. Anything added via :any:`push` will also be returned. """ if timeout is not None: timeout = timeout + _time.clock() # timeout at this time while True: if _eventQueue: return _eventQueue.pop(0) if flush: # a full 'round' of events need to be processed before flushing _tdl.flush() if timeout and _time.clock() >= timeout: return None # return None on timeout _time.sleep(0.001) # sleep 1ms _processEvents()
[docs]def push(event): """Push an event into the event buffer. Args: event (Any): This event will be available on the next call to :any:`event.get`. An event pushed in the middle of a :any:`get` will not show until the next time :any:`get` called preventing push related infinite loops. This object should at least have a 'type' attribute. """ _pushedEvents.append(event)
[docs]def key_wait(): """Waits until the user presses a key. Then returns a :any:`KeyDown` event. Key events will repeat if held down. A click to close the window will be converted into an Alt+F4 KeyDown event. Returns: tdl.event.KeyDown: The pressed key. """ while 1: for event in get(): if event.type == 'KEYDOWN': return event if event.type == 'QUIT': # convert QUIT into alt+F4 return KeyDown('F4', '', True, False, True, False, False) _time.sleep(.001)
[docs]def set_key_repeat(delay=500, interval=0): """Does nothing. """ pass
[docs]def is_window_closed(): """Returns True if the exit button on the window has been clicked and stays True afterwards. Returns: bool: """ return _lib.TCOD_console_is_window_closed()
__all__ = [_var for _var in locals().keys() if _var[0] != '_'] App.runOnce = _style.backport(App.run_once) keyWait = _style.backport(key_wait) setKeyRepeat = _style.backport(set_key_repeat) isWindowClosed = _style.backport(is_window_closed)