controls.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import copy
  2. import time
  3. from tooncher.actions import *
  4. try:
  5. import psutil
  6. except ImportError:
  7. psutil = False
  8. try:
  9. import Xlib.display
  10. from Xlib import X, XK
  11. except ImportError:
  12. Xlib = False
  13. EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME = 'grave'
  14. if Xlib:
  15. EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS = {
  16. XK.XK_w: RewriteKeyEventAction(keysym=XK.XK_Up),
  17. XK.XK_a: RewriteKeyEventAction(keysym=XK.XK_Left),
  18. XK.XK_s: RewriteKeyEventAction(keysym=XK.XK_Down),
  19. XK.XK_d: RewriteKeyEventAction(keysym=XK.XK_Right),
  20. }
  21. def x_find_window(parent_window, filter_callback):
  22. matching = []
  23. for child_window in parent_window.query_tree().children:
  24. if filter_callback(child_window):
  25. matching.append(child_window)
  26. matching += x_find_window(child_window, filter_callback)
  27. return matching
  28. def x_find_window_by_pid(display, pid):
  29. pid_prop = display.intern_atom('_NET_WM_PID')
  30. def filter_callback(window):
  31. prop = window.get_full_property(pid_prop, X.AnyPropertyType)
  32. return prop and prop.value.tolist() == [pid]
  33. return x_find_window(display.screen().root, filter_callback)
  34. class ExtendedControls:
  35. def __init__(self, engine_pid,
  36. toggle_keysym_name=EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME):
  37. if not psutil:
  38. raise Exception('\n'.join([
  39. 'Extended keyboard controls require the python lib psutil to be installed.',
  40. 'Depending on your system run',
  41. '\t$ sudo apt-get install python3-psutil',
  42. 'or',
  43. '\t$ pip3 install --user psutil',
  44. ]))
  45. if not Xlib:
  46. raise Exception('\n'.join([
  47. 'Extended keyboard controls require xlib for python to be installed.',
  48. 'Depending on your system run',
  49. '\t$ sudo apt-get install python3-xlib',
  50. 'or',
  51. '\t$ pip3 install --user xlib',
  52. ]))
  53. self._engine_pid = engine_pid
  54. self._xdisplay = Xlib.display.Display()
  55. self._toggle_keysym = XK.string_to_keysym(toggle_keysym_name)
  56. if self._toggle_keysym == X.NoSymbol:
  57. raise Exception("Extended keyboard controls toggle:"
  58. + " Unknown keysym name '{}'".format(toggle_keysym_name))
  59. keysym_mappings = copy.deepcopy(
  60. EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS,
  61. )
  62. if self._toggle_keysym in keysym_mappings:
  63. del keysym_mappings[self._toggle_keysym]
  64. print("INFO Extended Controls:"
  65. + " Ignoring mapping for toggle key '{}'".format(toggle_keysym_name))
  66. self._keysym_mappings = keysym_mappings
  67. self._default_action = ForwardKeyEventAction()
  68. self._engine_window = None
  69. self._enabled = False
  70. @property
  71. def engine_running(self):
  72. return psutil.pid_exists(self._engine_pid)
  73. @property
  74. def xdisplay(self):
  75. return self._xdisplay
  76. @property
  77. def engine_window(self):
  78. return self._engine_window
  79. def _wait_for_engine_window(self, timeout_seconds=20, search_interval_seconds=2):
  80. start_epoch = time.time()
  81. while self.engine_running and (time.time() - start_epoch) <= timeout_seconds:
  82. windows = x_find_window_by_pid(
  83. self._xdisplay,
  84. self._engine_pid,
  85. )
  86. assert len(windows) <= 1
  87. if len(windows) == 1:
  88. return windows[0]
  89. time.sleep(search_interval_seconds)
  90. return None
  91. def run(self):
  92. self._engine_window = self._wait_for_engine_window()
  93. if not self._engine_window:
  94. raise Exception('Could not find the game\'s window.')
  95. self._grab_key(
  96. self._xdisplay.keysym_to_keycode(self._toggle_keysym),
  97. )
  98. if not self.enabled:
  99. keysym_name = XK.keysym_to_string(self._toggle_keysym)
  100. print("INFO Extended Controls are currently disabled."
  101. + " Press key '{}' to enable.".format(keysym_name))
  102. while self.engine_running:
  103. # TODO don't block here, engine might have already been stopped
  104. self._handle_xevent(self._xdisplay.next_event())
  105. def _handle_xevent(self, xevent):
  106. # TODO investigate why some release events get lost
  107. if isinstance(xevent, Xlib.protocol.event.KeyPress) \
  108. or isinstance(xevent, Xlib.protocol.event.KeyRelease):
  109. self._handle_xkeyevent(xevent)
  110. def _handle_xkeyevent(self, xkeyevent):
  111. keysym_in = self._xdisplay.keycode_to_keysym(
  112. xkeyevent.detail,
  113. index=0,
  114. )
  115. if keysym_in == self._toggle_keysym:
  116. action = ToggleExtendedControlsAction()
  117. elif self.enabled and keysym_in in self._keysym_mappings:
  118. action = self._keysym_mappings[keysym_in]
  119. else:
  120. action = self._default_action
  121. action.execute(self, xkeyevent)
  122. def enable(self):
  123. for keysym in self._keysym_mappings.keys():
  124. self._grab_key(
  125. self._xdisplay.keysym_to_keycode(keysym),
  126. )
  127. self._enabled = True
  128. print("INFO Enabled Extended Controls")
  129. def disable(self):
  130. for keysym in self._keysym_mappings.keys():
  131. self._ungrab_key(
  132. self._xdisplay.keysym_to_keycode(keysym),
  133. )
  134. self._enabled = False
  135. print("INFO Disabled Extended Controls")
  136. @property
  137. def enabled(self):
  138. return self._enabled
  139. def toggle(self):
  140. if self.enabled:
  141. self.disable()
  142. else:
  143. self.enable()
  144. def _grab_key(self, keycode):
  145. self._engine_window.grab_key(
  146. keycode,
  147. X.AnyModifier,
  148. # owner_events
  149. # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event
  150. # False,
  151. True,
  152. X.GrabModeAsync,
  153. X.GrabModeAsync,
  154. )
  155. def _ungrab_key(self, keycode):
  156. self._engine_window.ungrab_key(keycode, X.AnyModifier)