PygHandler.py
From Osvidwiki
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