PygHandler.py

From Osvidwiki
Jump to: navigation, search

Code to create "trainable" interfaces that map keyboard and/or joystick events to commands.

"""
PyHandler

Copyright (c) 2009 Michael Murtaugh, http://automatist.org
Dual licensed under the MIT and GPL licenses.

"""

import pygame, pickle, time

class PygHandler (object):
    """
PygHandler is a trainable command dispatcher for PyGame

Currently PygHandler only handles "press/release" events, namely:
KeyDown/Up
MouseButtonDown/Up
JoyButtonDown/Up
JoyHatMotion(Position/Release)

You register any number of callbacks, then (at least once)
use the train method to associate one or more control events to
trigger each callback.

Trainings can then be saved and loaded to a file.
By default, saves only record the triggers for each callback name, and *not*
the actual callback functionality. When the saveFunctions=True, these will be saved
(so pickling limitations may apply on the callbacks -- for instance lambda's don't pickle!).

Multiple trainings can be loaded in sequence to combine different triggers.

Future Extensions:
* Record joystick name to allow different bindings for different kinds of joysticks
* Handle motion events

    """

    def __init__ (self):
        self.callbacks = []
        self.accept_types = (
            pygame.KEYDOWN,
            pygame.MOUSEBUTTONDOWN,
            pygame.JOYBUTTONDOWN,
            pygame.JOYHATMOTION
        )
        self.training_timeout = 5
        self.release_stack = []

    def register (self, name, fn, *args, **keys):
        callback = None
        for c in self.callbacks:
            # REUSE/RESET existing callback for name
            if c['name'] == name:
                callback = c
                if callback.has_key('onrelease'):
                    del callback['onrelease']
                    del callback['onrelease_args']
                break
        if not callback:
            # create a new callback
            callback = {'name': name, 'triggers': []}

        # Record the fn, args, onrelease, onrelease_args
        callback['fn'] = fn
        callback['args'] = args
        
        if keys.has_key('onrelease'):
            callback['onrelease'] = keys['onrelease']
            if keys.has_key('args'):
                callback['onrelease_args'] = keys['args']
            elif keys.has_key('arg'):
                callback['onrelease_args'] = (keys['arg'],)
            else:
                callback['onrelease_args'] = ()
        else:
            callback['onrelease'] = None
            callback['onrelease_args'] = ()
        self.callbacks.append(callback)

    def train(self):
        """ starts a training session, appends new bindings to any existing ones """
        clock = pygame.time.Clock()
        print "Begin training..."
        i = 0

        if i < len(self.callbacks):
            print self.callbacks[i]['name']
        start = time.time()

        while i < len(self.callbacks):
            for event in pygame.event.get():
                # print event
                if event.type == pygame.QUIT:
                    print "Training cancelled"
                    return False
                if (event.type in self.accept_types):
                
                    # Ignore hat releases (as a trigger start)
                    if event.type == pygame.JOYHATMOTION and event.value == (0, 0):
                        continue

                    # event objects can't be directly pickled
                    # so instead we store the type & dict
                    trigger = {'type': event.type}
                    if event.type == pygame.KEYDOWN:
                        trigger['key'] = event.key
                        trigger['scancode'] = event.scancode
                    elif event.type == pygame.MOUSEBUTTONDOWN:
                        trigger['button'] = event.button
                    elif event.type == pygame.JOYBUTTONDOWN:
                        trigger['button'] = event.button
                    elif event.type == pygame.JOYHATMOTION:
                        trigger['hat'] = event.hat
                        trigger['value'] = event.value
                    
                    if not trigger in self.callbacks[i]['triggers']:
                        self.callbacks[i]['triggers'].append(trigger)
                        print "\t%s" % event
                        start = time.time()
 
            # auto-advance
            if (time.time()-start) > self.training_timeout:
                i += 1
                if i < len(self.callbacks):
                    print self.callbacks[i]['name']
                start = time.time()
                
            clock.tick(10)
        print "Training complete"
        return True

    def save_training(self, f, saveFunctions = False):
        """ save callbacks to file, f can be a pathname or file """
        if (type(f) == str or type(f) == unicode):
            f = open(f, "w")
        
        if not saveFunctions:
            dump = []
            for c in self.callbacks:
                rec = {'name': c['name']}
                rec['triggers'] = c['triggers']
                dump.append(rec)
        else:
            dump = self.callbacks        

        pickle.dump(dump, f)
            
        
        f.close()

    def load_training(self, f):
        """
        loads callbacks from file, f can be a pathname or file
        merges with any existing callbacks
        NB: may reset / change callback functions iff the stored file has them (save_training called with saveFunctions)
        """
        if (type(f) == str or type(f) == unicode):
            f = open(f)
        callbacks = pickle.load(f)
        for lc in callbacks:
            merged = False
            for c in self.callbacks:
                if c['name'] == lc['name']:
                    # MERGE WITH EXISTING CALLBACK
                    for trigger in lc['triggers']:
                        if not trigger in c['triggers']:
                            c['triggers'].append(trigger)
                    # fn, args, onrelease, onrelease_args
                    if lc.has_key('fn'):
                        # STORED CALLBACK HAS FUNCTIONS
                        c['fn'] = lc['fn']
                        c['args'] = lc['args']
                        c['onrelease'] = lc['onrelease']
                        c['onrelease_args'] = lc['onrelease_args']
                    merged = True
                    break
            if not merged:
                # simply use the stored callback as is
                self.callbacks.append(lc)    

        # self.callbacks = pickle.load(f)
        f.close()

    # Event Samples
    # In each case a button/key is pressed/released, then a different button/key is pressed/released

    # Key
    #<Event(2-KeyDown {'scancode': 116, 'key': 274, 'unicode': u'', 'mod': 4096})>
    #<Event(3-KeyUp {'scancode': 116, 'key': 274, 'mod': 4096})>
    #<Event(2-KeyDown {'scancode': 113, 'key': 276, 'unicode': u'', 'mod': 4096})>
    #<Event(3-KeyUp {'scancode': 113, 'key': 276, 'mod': 4096})>

    # MouseButton
    #<Event(5-MouseButtonDown {'button': 1, 'pos': (111, 59)})>
    #<Event(6-MouseButtonUp {'button': 1, 'pos': (111, 59)})>
    #<Event(5-MouseButtonDown {'button': 3, 'pos': (111, 59)})>
    #<Event(6-MouseButtonUp {'button': 3, 'pos': (111, 59)})>

    # JoyButton
    #<Event(10-JoyButtonDown {'joy': 0, 'button': 1})>
    #<Event(11-JoyButtonUp {'joy': 0, 'button': 1})>
    #<Event(10-JoyButtonDown {'joy': 0, 'button': 0})>
    #<Event(11-JoyButtonUp {'joy': 0, 'button': 0})>

    # JoyHat
    # JoyHat "releases" are not unique (relative the initial trigger), nor do they have an "Up" type
    #<Event(9-JoyHatMotion {'joy': 0, 'hat': 0, 'value': (0, -1)})>
    #<Event(9-JoyHatMotion {'joy': 0, 'hat': 0, 'value': (0, 0)})>
    #<Event(9-JoyHatMotion {'joy': 0, 'hat': 0, 'value': (-1, 0)})>
    #<Event(9-JoyHatMotion {'joy': 0, 'hat': 0, 'value': (0, 0)})>

    # "salient" features
    # Key: scancode, key (not unicode, mod)
    # MouseButton: button (not pos)
    # JoyButton: button (not joy)
    # JoyHat: hat, value (not joy)

    def isPress(self, e):
        return (e.type == pygame.KEYDOWN or e.type == pygame.MOUSEBUTTONDOWN or e.type == pygame.JOYBUTTONDOWN or \
            (e.type == pygame.JOYHATMOTION and e.value != (0, 0)))

    def isRelease(self, e):
        return (e.type == pygame.KEYUP or e.type == pygame.MOUSEBUTTONUP or e.type == pygame.JOYBUTTONUP or \
            (e.type == pygame.JOYHATMOTION and e.value == (0, 0)))
        
    def handle (self, e):
        """
        attempts to map a PyGame event to one or more registered callbacks
        returns:
            True -> if one or more callbacks were triggered
            False, otherwise
        """
        ret = False
        # return immediately on potentially high frequency events
#        if (e.type == pygame.MOUSEMOTION or e.type == pygame.JOYAXISMOTION or e.type == pygame.JOYBALLMOTION):
#            return
 
        # HANDLE PRESSES
        if self.isPress(e):
            for callback in self.callbacks:
                for trigger in callback['triggers']:
                    # simply comparing the event dictionary to the binding dictionary breaks for a few cases:
                    # Key events include a 'mod' reflecting the current state of modifier keys
                    # Key down events include a unicode representation, while Key up events do not
                    # if (e.type == binding['type']) and (e.dict == binding['dict']):
                    if (e.type == trigger['type']):
                        match = False
                        if (e.type == pygame.KEYDOWN and \
                               (e.dict['scancode'] == trigger['scancode'] and e.dict['key'] == trigger['key'])) or \
                           (e.type == pygame.MOUSEBUTTONDOWN and \
                               (e.dict['button'] == trigger['button'])) or \
                           (e.type == pygame.JOYBUTTONDOWN and \
                               (e.dict['button'] == trigger['button'])) or \
                           (e.type == pygame.JOYHATMOTION and \
                               (e.dict['hat'] == trigger['hat'] and e.dict['value'] == trigger['value'])):

                            callback['fn'](*callback['args'])
                            if callback['onrelease']:
                                self.release_stack.append((callback, trigger))
                            ret = True
                            break # the triggers loop, keeps searching for other callbacks though
        elif self.isRelease(e):
            for release in self.release_stack[:]:
                (callback, trigger) = release
                if (trigger['type'] == pygame.KEYDOWN and e.type == pygame.KEYUP and \
                       (e.dict['scancode'] == trigger['scancode'] and e.dict['key'] == trigger['key'])) or \
                   (trigger['type'] == pygame.MOUSEBUTTONDOWN and e.type == pygame.MOUSEBUTTONUP and \
                       (e.dict['button'] == trigger['button'])) or \
                   (trigger['type'] == pygame.JOYBUTTONDOWN and e.type == pygame.JOYBUTTONUP and \
                       (e.dict['button'] == trigger['button'])) or \
                   (trigger['type'] == pygame.JOYHATMOTION and e.type == pygame.JOYHATMOTION and \
                       (e.dict['value'] == (0, 0))):
                 
                    # onrelease
                    callback['onrelease'](*callback['onrelease_args'])
                    self.release_stack.remove(release)
                
        return ret


# IMPLEMENT AS single has that compares event "signature" to a lookup
# (imagine that there are MANY callbacks?)
# in any case -- very good to reject quickly events like MOTION events!

#for reference:
#pygame event types & associated members
#    QUIT	     none
#    ACTIVEEVENT	     gain, state
#    KEYDOWN	     unicode, key, mod
#    KEYUP	     key, mod
#    MOUSEMOTION	     pos, rel, buttons
#    MOUSEBUTTONUP    pos, button
#    MOUSEBUTTONDOWN  pos, button
#    JOYAXISMOTION    joy, axis, value
#    JOYBALLMOTION    joy, ball, rel
#    JOYHATMOTION     joy, hat, value
#    JOYBUTTONUP      joy, button
#    JOYBUTTONDOWN    joy, button
#    VIDEORESIZE      size, w, h
#    VIDEOEXPOSE      none
#    USEREVENT        code