controls.py 6.4 KB

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