controls.py 5.8 KB

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