Skip to content

Instantly share code, notes, and snippets.

@yhyu13
Created July 10, 2022 10:15
Show Gist options
  • Save yhyu13/9f18f9248064d8551b1b71c20a029962 to your computer and use it in GitHub Desktop.
Save yhyu13/9f18f9248064d8551b1b71c20a029962 to your computer and use it in GitHub Desktop.
Dearpygui v1.6.2 InputManager
"""
Handle User inputs and manage input states
Example:
Main loop:
inputManager.init(args)
while dpg.is_dearpygui_running():
inputManager.run()
Events: check Crtl+Shift+S is toggled
if inputManager.keyStateMatchAll([dpg.mvKey_S], [EButtonState.toggled]) \
and inputManager.modStateMatchExact(Modifier.MOD_SHIFT | Modifier.MOD_CRTL)
"""
import enum
import dearpygui.dearpygui as dpg
from Source.Logger.logger import spdLogger
# It's a class that defines the state of a button
class EButtonState(enum.Enum):
released = 0
pressed = 1
downed = 2 # to handle period pressed events, e.g. holding a key for half a second to toggle a new window
toggled = 3 # change from pressed/downed to release in current frame is toggled
doubleClicked = 4 # mouse only
dragged = 5 # mouse only
wheeled = 6 # mouse only
# It stores the state of a button
class _InputState:
def __init__(self, _id, _name, _state):
self.id = _id
self.name = _name
self.state = _state
# stores previous state
self.prevState = EButtonState.released
# DPG sometimes invoke downed event right after pressed event, we use a flag to prevent that
self.isPressedCurrentFrame = False
# It stores the state of a button
class _MouseInputState(_InputState):
def __init__(self, _id, _name, _state):
super().__init__(_id, _name, _state)
self.isDoubleClickedCurrentFrame = False
class Modifier:
MOD_SHIFT = 1 << 0
MOD_CTRL = 1 << 1
MOD_ALT = 1 << 2
# MOD_CAPSLOCK = 1 << 3 # we do not handle lock modifier
# MOD_NUMLOCK = 1 << 4 # we do not handle lock modifier
MOD_WINDOWS = 1 << 5
# MOD_COMMAND = 1 << 6 # equivalent to windows by dpg
# MOD_OPTION = 1 << 7 # not supported by dpg
# MOD_SCROLLLOCK = 1 << 8 # we do not handle lock modifier
# MOD_FUNCTION = 1 << 9 # not supported by dpg
Key2Mod = {
dpg.mvKey_Shift : MOD_SHIFT,
dpg.mvKey_LShift : MOD_SHIFT,
dpg.mvKey_RShift : MOD_SHIFT,
dpg.mvKey_LControl: MOD_CTRL,
dpg.mvKey_Control : MOD_CTRL,
dpg.mvKey_RControl: MOD_CTRL,
dpg.mvKey_Alt : MOD_ALT,
dpg.mvKey_LWin : MOD_WINDOWS,
dpg.mvKey_RWin : MOD_WINDOWS,
}
Mod2Keys = {
MOD_SHIFT : [dpg.mvKey_Shift, dpg.mvKey_LShift, dpg.mvKey_RShift],
MOD_CTRL : [dpg.mvKey_Control, dpg.mvKey_LControl, dpg.mvKey_RControl],
MOD_ALT : [dpg.mvKey_Alt],
MOD_WINDOWS: [dpg.mvKey_LWin, dpg.mvKey_RWin],
}
@classmethod
def modif2Str(cls, modifierState) -> str:
ret = ''
if modifierState & cls.MOD_SHIFT:
ret += 'MOD_SHIFT | '
if modifierState & cls.MOD_CTRL:
ret += 'MOD_CTRL | '
if modifierState & cls.MOD_ALT:
ret += 'MOD_ALT | '
if modifierState & cls.MOD_WINDOWS:
ret += 'MOD_WINDOWS | '
# if modifierState & cls.MOD_FUNCTION:
# ret += 'MOD_FUNCTION | '
# if modifierState & cls.MOD_COMMAND:
# ret += 'MOD_COMMAND | '
# if modifierState & cls.MOD_OPTION:
# ret += 'MOD_OPTION | '
return ret[:-3] if len(ret) > 3 else ret
class _InputManager:
_keyMap = {}
_mouseMap = {}
_modifierState = 0 # bit masked state for modifiers such as SHIFT, ALT, CAPSLOCK, etc
def __init__(self):
for const in dir(dpg):
if const.startswith('mvKey_'):
c = getattr(dpg, const)
self._keyMap[c] = _InputState(c, const, EButtonState.released)
elif const.startswith('mvMouseButton_'):
c = getattr(dpg, const)
self._mouseMap[c] = _InputState(c, const, EButtonState.released)
def init(self, args):
# https://dearpygui.readthedocs.io/en/latest/tutorials/item-usage.html?highlight=current%20widget#using-item-handlers
with dpg.handler_registry():
dpg.add_key_press_handler(callback = self._key_pressed_event)
dpg.add_key_down_handler(callback = self._key_down_event)
dpg.add_key_release_handler(callback = self._key_release_event)
for key in Modifier.Key2Mod.keys():
dpg.add_key_press_handler(key = key, callback = self._mod_pressed_event)
# dpg.add_key_down_handler(key = key, callback = self._mod_down_event)
dpg.add_key_release_handler(key = key, callback = self._mod_release_event)
dpg.add_mouse_click_handler(callback = self._mouse_pressed_event)
dpg.add_mouse_down_handler(callback = self._mouse_down_event)
dpg.add_mouse_release_handler(callback = self._mouse_release_event)
dpg.add_mouse_drag_handler(callback = self._mouse_drag_event)
dpg.add_mouse_double_click_handler(callback = self._mouse_doubleclicked_event)
spdLogger.info('InputManager initialized')
def run(self):
self._clearState()
def finalize(self):
pass
# Clear state beginning of every frame
def _clearState(self):
for value in self._keyMap.values():
value.prevState = value.state
value.state = EButtonState.released
value.isPressedCurrentFrame = False
for value in self._mouseMap.values():
value.prevState = value.state
value.state = EButtonState.released
value.isPressedCurrentFrame = False
value.isDoubleClickedCurrentFrame = False
def key2Str(self, key) -> str:
return self._keyMap[key].name
def mouse2Str(self, mouse) -> str:
return self._mouseMap[mouse].name
def modif2Str(self) -> str:
return Modifier.modif2Str(self._modifierState)
def modStateMatchExact(self, modstates) -> bool:
"""
`modStateMatchExact` returns `True` if the `modstates` argument is equal to the `_modifierState` attribute of the
`Key` object
:param modstates: A list of modifier states
:return: A boolean value.
"""
return modstates == self._modifierState
def mouseStateMatchAll(self, keys, states) -> bool:
"""
Returns true if all of the mouse keys in the list `keys` are in the list of states `states`
:param keys: A list of mouse keys to check
:param states: a list of states to check for
:return: A boolean value.
"""
assert len(keys) > 0 and len(states) > 0
for key in keys:
if not self._mouseMap[key].state in states:
return False
return True
def keyStateMatchAll(self, keys, states) -> bool:
"""
Returns true if all of the keys in the keys list are in the states list
:param keys: A list of keys to check
:param states: a list of states to check for
:return: A boolean value.
"""
assert len(keys) > 0 and len(states) > 0
for key in keys:
if not self._keyMap[key].state in states:
return False
return True
# def keyStateMatchNone(self, keys, states) -> bool:
# """
# Either supply multiple keys with array of single state, or supply matching sizes of keys and states
# (e.g. KeyStateMatchNone([KeyA, KeyB], [Down]) or KeyStateMatchNone([KeyA, KeyB], [Down, Release]))
# """
# assert len(keys) > 0 and len(states) > 0
# if len(states) == 1:
# for i, key in enumerate(keys):
# if self._keyMap[key].state == states[0]:
# return False
# else:
# assert len(states) == len(keys)
# for i, (key, state) in enumerate(zip(keys, states)):
# if self._keyMap[key].state == state:
# return False
# return True
# def keyStateMatchAtLeastOne(self, keys, states) -> bool:
# """
# Either supply multiple keys with array of single state, or supply matching sizes of keys and states
# (e.g. KeyStateMatchAtLeastOne([KeyA, KeyB], [Down]) or KeyStateMatchAtLeastOne([KeyA, KeyB], [Down, Release]))
# """
# assert len(keys) > 0 and len(states) > 0
# if len(states) == 1:
# for i, key in enumerate(keys):
# if self._keyMap[key].state == states[0]:
# return True
# else:
# assert len(states) == len(keys)
# for i, (key, state) in enumerate(zip(keys, states)):
# if self._keyMap[key].state == state:
# return True
# return False
# Generic key events --------------------------------------------------------------
def _key_pressed_event(self, sender, app_data):
key = app_data
keystate = self._keyMap[key]
keystate.state = EButtonState.pressed
keystate.isPressedCurrentFrame = True
spdLogger.trace(f'Key pressed : {self.key2Str(key)}')
def _key_down_event(self, sender, app_data):
key = app_data[0]
keystate = self._keyMap[key]
if not keystate.isPressedCurrentFrame:
keystate.state = EButtonState.downed
spdLogger.trace(f'Key down : {self.key2Str(key)}')
def _key_release_event(self, sender, app_data):
key = app_data
keystate = self._keyMap[key]
if keystate.prevState == EButtonState.pressed or keystate.prevState == EButtonState.downed:
keystate.state = EButtonState.toggled
spdLogger.trace(f'Key toggled : {self.key2Str(key)}')
else:
keystate.state = EButtonState.released
spdLogger.trace(f'Key release : {self.key2Str(key)}')
# Generic mouse events --------------------------------------------------------------
def _mouse_pressed_event(self, sender, app_data):
key = app_data
keystate = self._mouseMap[key]
keystate.state = EButtonState.pressed
keystate.isPressedCurrentFrame = True
spdLogger.trace(f'Mouse pressed : {self.mouse2Str(key)}')
def _mouse_down_event(self, sender, app_data):
key = app_data[0]
keystate = self._mouseMap[key]
if not keystate.isPressedCurrentFrame and not keystate.isDoubleClickedCurrentFrame:
keystate.state = EButtonState.downed
spdLogger.trace(f'Mouse down : {self.mouse2Str(key)}')
def _mouse_release_event(self, sender, app_data):
key = app_data
keystate = self._mouseMap[key]
if keystate.prevState == EButtonState.pressed or keystate.prevState == EButtonState.downed:
keystate.state = EButtonState.toggled
spdLogger.trace(f'Mouse toggled : {self.mouse2Str(key)}')
else:
keystate.state = EButtonState.released
spdLogger.trace(f'Mouse release : {self.mouse2Str(key)}')
def _mouse_drag_event(self, sender, app_data):
# print(app_data)
key = app_data[0]
keystate = self._mouseMap[key]
keystate.state = EButtonState.dragged
spdLogger.trace(f'Mouse dragged : {self.mouse2Str(key)}')
def _mouse_doubleclicked_event(self, sender, app_data):
key = app_data
keystate = self._mouseMap[key]
keystate.state = EButtonState.doubleClicked
keystate.isDoubleClickedCurrentFrame = True
spdLogger.trace(f'Mouse double clicked : {self.mouse2Str(key)}')
# Modifier events --------------------------------------------------------------
def _mod_pressed_event(self, sender, app_data):
key = app_data
mod = Modifier.Key2Mod[key]
self._modifierState |= mod
spdLogger.trace(f'Mod pressed : {self.key2Str(key)}, mod state {self.modif2Str()}')
# def _mod_down_event(self, sender, app_data):
# # no need to handle modifier down as it would not change modifier state
# pass
def _mod_release_event(self, sender, app_data):
key = app_data
mod = Modifier.Key2Mod[key]
keys = Modifier.Mod2Keys[mod]
# only clear modifier state on all this modifier keys are released (e.g. both L/R Shift are released)
if self.keyStateMatchAll(keys, [EButtonState.released, EButtonState.toggled]):
self._modifierState &= ~mod
spdLogger.trace(f'Mod released : {self.key2Str(key)}, mod state {self.modif2Str()}')
inputManager = _InputManager()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment