controls.py 6.6 KB

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