controls.py 6.8 KB

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