controls.py 6.3 KB

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