Perhaps you might benefit from using the state system that I developed for my Panda3D Studio project, as its purpose is precisely to bind specific events to a particular “state” that the application is in:
state.py
:
from direct.showbase.DirectObject import DirectObject
# The following class allows predefining specific bindings of events to their
# handlers.
# Multiple bindings can be set active at once, with the option to stop listening
# for all other events.
class EventBinder:
def __init__(self):
self._listener = DirectObject()
self._bindings = {}
self._active_bindings = {}
def bind(self, binding_id, event_props, handler, arg_list=None, once=False):
"""
Predefine a specific binding of an event to a handler by id.
The event in this binding can be set to be listened for later on by simply
using this id instead of having to set the details of the binding every
single time.
"""
args = (arg_list,) if arg_list else ()
self._bindings[binding_id] = (once, event_props, handler) + args
def unbind(self, binding_id):
"""
Remove the given binding.
Note that, if the binding is currently active, this also stops the event in
that binding being listened for!
Returns True if successful or False if binding_id was not found.
"""
if binding_id not in self._bindings:
return False
event_props = self._bindings[binding_id][1]
del self._bindings[binding_id]
if event_props in self._active_bindings:
self.ignore(event_props)
return True
def accept(self, binding_ids, exclusive=False):
"""
Listen for the events in the bindings whose ids are given.
If "exclusive" is True, the events in all of the predefined bindings other
than the ones given will be ignored.
Returns True if successful or False if not all binding_ids were found.
"""
if exclusive:
self.ignore_all()
binding_ids_found = True
for binding_id in binding_ids:
if binding_id not in self._bindings:
binding_ids_found = False
continue
binding = self._bindings[binding_id]
event_props = binding[1]
self._active_bindings[event_props] = binding_id
method = self._listener.accept_once if binding[0] else self._listener.accept
method(*binding[1:])
return binding_ids_found
def ignore(self, *event_data):
"""
Stop listening for the events whose ids are given.
"""
for event_props in event_data:
self._listener.ignore(event_props)
if event_props in self._active_bindings:
del self._active_bindings[event_props]
def ignore_all(self):
"""
Stop listening for all events.
"""
self._listener.ignore_all()
self._active_bindings = {}
def get_active_bindings(self):
"""
Return a list of the ids of the bindings of the events currently being
listened for.
"""
return list(self._active_bindings.values())
class StateObject:
def __init__(self, state_id, persistence, on_enter=None, on_exit=None):
self.id = state_id
self.persistence = persistence
self.enter_command = on_enter if on_enter else lambda prev_state_id, active: None
self.exit_command = on_exit if on_exit else lambda next_state_id, active: None
self.active = False
self._prev_state = None
@property
def previous_state(self):
prev_state = self._prev_state
while prev_state and not prev_state.active:
prev_state = prev_state.previous_state
return prev_state
@previous_state.setter
def previous_state(self, prev_state):
while prev_state and prev_state.persistence <= self.persistence:
prev_state = prev_state.previous_state
self._prev_state = prev_state
def enter(self, prev_state_id):
self.enter_command(prev_state_id, self.active)
self.active = True
def exit(self, next_state_id):
self.exit_command(next_state_id, self.active)
# The following class manages the different states that the application
# can be in.
class StateManager:
def __init__(self):
self._states = {}
self._default_state_id = ""
self.current_state_id = ""
self._changing_state = False
def add_state(self, state_id, persistence, on_enter=None, on_exit=None):
"""
Define a new state, optionally with commands to be called on entering and/or
exiting that state.
"""
self._states[state_id] = StateObject(state_id, persistence, on_enter, on_exit)
def set_state_enter_command(self, state_id, on_enter):
self._states[state_id].enter_command = on_enter
def set_state_exit_command(self, state_id, on_exit):
self._states[state_id].exit_command = on_exit
def has_state(self, state_id):
""" Check if the state with the given id has been previously defined """
return state_id in self._states
def is_state_binder(self):
""" Check if this object is a StateBinder instance """
return False
def set_default_state(self, state_id):
"""
Set the default state for the application.
Returns True if successful or False if the state with the given id has not
been previously defined or is already the current state.
Also returns False if a state change is already in progress.
"""
if self._changing_state:
return False
if state_id not in self._states:
return False
current_state_id = self.current_state_id
if state_id == current_state_id:
return False
self._changing_state = True
state = self._states[state_id]
state.enter(current_state_id)
if self.is_state_binder():
self._set_state_bindings(state_id)
self._default_state_id = state_id
self.current_state_id = state_id
self._changing_state = False
return True
def enter_state(self, state_id):
"""
Change from the current state to the one with the given id.
Returns True if successful or False if the state with the given id has not
been previously defined or is already the current state.
Also returns False if a state change is already in progress.
"""
if self._changing_state:
return False
if state_id not in self._states:
return False
current_state_id = self.current_state_id
if state_id == current_state_id:
return False
self._changing_state = True
current_state = self._states[current_state_id]
state = self._states[state_id]
state.previous_state = current_state
persistence = state.persistence
if current_state.persistence <= persistence:
current_state.active = False
current_state.exit(state_id)
prev_state = current_state.previous_state
while prev_state and prev_state is not state and prev_state.persistence <= persistence:
prev_state.active = False
prev_state.exit(state_id)
prev_state = prev_state.previous_state
state.enter(self.current_state_id)
if self.is_state_binder():
self._set_state_bindings(state_id)
self.current_state_id = state_id
self._changing_state = False
return True
def exit_state(self, state_id):
"""
Exit the state with the given id. If it is not the current state, it will
merely get deactivated (its exit command will still be called and it will
be marked as inactive, but its previous state will not be restored).
Returns True if successful or False if the state with the given id has not
been previously defined, is inactive or is the default state.
Also returns False if a state change is already in progress.
"""
if self._changing_state:
return False
if state_id not in self._states:
return False
state = self._states[state_id]
if not state.active:
return False
prev_state = state.previous_state
if not prev_state:
# the default state has no previous state and thus cannot be exited
return False
self._changing_state = True
current_state_id = self.current_state_id
state.active = False
if state_id == current_state_id:
prev_state_id = prev_state.id
state.exit(prev_state_id)
prev_state.enter(state_id)
if self.is_state_binder():
self._set_state_bindings(prev_state_id)
self.current_state_id = prev_state_id
else:
state.exit(current_state_id)
self._changing_state = False
return True
def exit_states(self, min_persistence=None):
"""
Exit all states with a persistence lower than or equal to min_persistence.
If no persistence is specified, start by exiting the current state.
Returns False if a state change is already in progress, True otherwise.
"""
if self._changing_state:
return False
self._changing_state = True
current_state_id = self.current_state_id
default_state_id = self._default_state_id
current_state = self._states[current_state_id]
default_state = self._states[default_state_id]
persistence = current_state.persistence if min_persistence is None else min_persistence
prev_state = current_state
while prev_state and prev_state is not default_state:
if prev_state.persistence >= persistence:
prev_state.active = False
prev_state.exit(default_state_id)
prev_state = prev_state.previous_state
if current_state.persistence >= persistence:
default_state.enter(current_state_id)
if self.is_state_binder():
self._set_state_bindings(default_state_id)
self.current_state_id = default_state_id
self._changing_state = False
return True
def get_state_persistence(self, state_id):
return self._states[state_id].persistence
def is_state_active(self, state_id):
return self._states[state_id].active
# The following class associates a particular state with a selection of event
# bindings.
class StateBinder(StateManager):
def __init__(self):
StateManager.__init__(self)
self._state_bindings = {}
self.event_binder = EventBinder()
def is_state_binder(self):
""" Confirm that this object is a StateBinder instance """
return True
def add_state(self, state_id, persistence, on_enter=None, on_exit=None):
"""
Define a new state, optionally with commands to be called on entering and/or
exiting that state.
"""
StateManager.add_state(self, state_id, persistence, on_enter, on_exit)
self._state_bindings[state_id] = set()
def bind(self, state_id, binding_id, event_props, event_handler):
"""
Define an event binding and associate it with a state.
"""
self.event_binder.bind(binding_id, event_props, event_handler)
self._state_bindings[state_id].add(binding_id)
def unbind(self, state_id, binding_id):
"""
Remove the given binding.
Note that, if the binding is currently active, this also stops the event in
that binding being listened for!
Returns True if successful or False if binding_id was not found.
"""
if state_id in self._state_bindings:
self._state_bindings[state_id].discard(binding_id)
return self.event_binder.unbind(binding_id)
return False
def update_active_bindings(self):
"""
Call this after changing any of the bindings associated with the current
state, so they take effect immediately.
"""
self.event_binder.accept(self._state_bindings[self.current_state_id], exclusive=True)
def accept(self, binding_ids, exclusive=False):
"""
Listen for the events in the bindings whose ids are given.
If "exclusive" is True, the events in all of the predefined bindings other
than the ones given will be ignored.
Returns True if successful or False if not all binding_ids were found.
"""
return self.event_binder.accept(binding_ids, exclusive)
def _set_state_bindings(self, state_id):
"""
Change from the current state to the one with the given id.
"""
self.event_binder.accept(self._state_bindings[state_id], exclusive=True)
main script:
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from state import *
class MyApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# set up the different states that the application can be in
self.state_binder = binder = StateBinder()
notify = lambda *args, **kwargs: self.__notify_state_change("default",
*args, **kwargs)
binder.add_state("default", persistence=0, on_enter=notify)
binder.bind("default", "action1", "space", self.__perform_action1)
binder.bind("default", "action2", "control-space", self.__perform_action2)
enter_state1 = lambda: self.state_binder.enter_state("state1")
binder.bind("default", "enter_state1", "1", enter_state1)
notify = lambda *args, **kwargs: self.__notify_state_change("state1",
*args, **kwargs)
binder.add_state("state1", persistence=-1, on_enter=notify)
# any additional state should have a persistence lower than that of the
# default state
binder.bind("state1", "action3", "enter", self.__perform_action3)
binder.bind("state1", "action4", "control-enter", self.__perform_action4)
binder.bind("state1", "change_binding", "c", self.__change_binding)
exit_state1 = lambda: self.state_binder.exit_state("state1")
binder.bind("state1", "exit_state1", "escape", exit_state1)
binder.set_default_state("default")
def __notify_state_change(self, state_id, prev_state_id, active):
print(f'Entered state "{state_id}"!')
print(f'The previous state was "{prev_state_id}".')
def __change_binding(self):
self.state_binder.bind("state1", "action3", "e", self.__perform_action3)
self.state_binder.update_active_bindings()
def __perform_action1(self):
print(f' Performing action1!')
def __perform_action2(self):
print(f' Performing action2!')
def __perform_action3(self):
print(f' Performing action3!')
def __perform_action4(self):
print(f' Performing action4!')
app = MyApp()
app.run()
This system probably does a lot more than you need right now, but as your project grows, its additional functionality might still prove useful. For example, each state has a certain “persistence” associated with it. Right now you don’t have to worry about that; it helps the system to figure out what the previous state was when you exit a state explicitly.
If it’s all a bit too much for your current needs, fair enough. But should the need arise for a more sophisticated way of dealing with (key-) event bindings when your code becomes more complex, then you’re certainly welcome to make use of the code above if you find it suits your purposes
.