controls.py 6.6 KB

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