__init__.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. """
  2. rescriptoon
  3. Copyright (C) 2018-2019 Fabian Peter Hammerle <fabian@hammerle.me>
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. """
  15. import enum
  16. import logging
  17. import os
  18. import select
  19. import time
  20. import typing
  21. import Xlib.display
  22. from Xlib import X
  23. from rescriptoon._actions import ToggleOverlayAction
  24. from rescriptoon._keys import keysym_to_label
  25. from rescriptoon._mapping import get_keycode_action_mapping
  26. from rescriptoon._ui import SystemTrayUnavailable, TrayIcon
  27. _TOONTOWN_ENGINE_WINDOW_NAME = "Toontown Rewritten"
  28. def _x_walk_children_windows(
  29. parent_window: "Xlib.display.Window",
  30. ) -> typing.Iterator["Xlib.display.Window"]:
  31. yield parent_window
  32. for child_window in parent_window.query_tree().children:
  33. for subchild_window in _x_walk_children_windows(child_window):
  34. yield subchild_window
  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 Overlay:
  47. def __init__(self, display: Xlib.display.Display, toggle_keycode: int):
  48. self._xdisplay = display
  49. self._toggle_keycode = toggle_keycode
  50. self._keycode_mappings = get_keycode_action_mapping()
  51. if self._toggle_keycode in self._keycode_mappings:
  52. logging.warning(
  53. "ignoring mapping for toggle key %d (%s)",
  54. toggle_keycode,
  55. self._toggle_key_label,
  56. )
  57. self._keycode_mappings[self._toggle_keycode] = ToggleOverlayAction()
  58. self._active_key_registry = {}
  59. self._enabled = False
  60. self._engine_windows = None
  61. try:
  62. self._tray_icon = TrayIcon(display=self._xdisplay)
  63. except SystemTrayUnavailable:
  64. self._tray_icon = None
  65. @property
  66. def xdisplay(self) -> Xlib.display.Display:
  67. return self._xdisplay
  68. @property
  69. def engine_windows(self) -> typing.List["Xlib.display.Window"]:
  70. return self._engine_windows
  71. @property
  72. def _engine_windows_open(self) -> bool:
  73. for window in self._engine_windows:
  74. try:
  75. window.get_wm_state()
  76. except Xlib.error.BadWindow:
  77. logging.info("engine window %x is no longer available", window.id)
  78. return False
  79. return True
  80. def run(self) -> None:
  81. self._engine_windows = self._find_engine_windows()
  82. logging.debug("engine window ids %r", [hex(w.id) for w in self._engine_windows])
  83. if not self._engine_windows:
  84. raise Exception("no toontown window found")
  85. self._grab_key(self._toggle_keycode)
  86. print("key bindings:")
  87. for keycode, action in self._keycode_mappings.items():
  88. keysym: int = self._xdisplay.keycode_to_keysym(keycode, index=0)
  89. print("{}: {}".format(keysym_to_label(keysym), action.description,))
  90. self.enable()
  91. while self._engine_windows_open:
  92. while self.xdisplay.pending_events():
  93. self._handle_xevent(self.xdisplay.next_event())
  94. self._check_active_key_registry()
  95. # keep timeout low for _check_active_key_registry()
  96. # to be called frequently
  97. _x_wait_for_event(self.xdisplay, timeout_seconds=0.05)
  98. self._disable()
  99. def _draw_tray_icon(self) -> None:
  100. if self._tray_icon:
  101. self._tray_icon.draw(self.enabled)
  102. def _handle_xevent(self, xevent: Xlib.protocol.rq.Event) -> None:
  103. if isinstance(
  104. xevent, (Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease)
  105. ):
  106. self._handle_xkeyevent(xevent)
  107. elif self._tray_icon and xevent.type == Xlib.X.ConfigureNotify:
  108. self._draw_tray_icon()
  109. def _handle_xkeyevent(
  110. self,
  111. xkeyevent: typing.Union[
  112. Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease
  113. ],
  114. ) -> None:
  115. self._update_active_key_registry(xkeyevent)
  116. keycode_in = xkeyevent.detail
  117. try:
  118. action = self._keycode_mappings[keycode_in]
  119. except KeyError:
  120. keysym_in = self.xdisplay.keycode_to_keysym(keycode_in, index=0,)
  121. logging.warning(
  122. "received key event of unmapped key %d (%s)",
  123. keycode_in,
  124. keysym_to_label(keysym_in),
  125. )
  126. return
  127. action.execute(self, xkeyevent)
  128. @property
  129. def _toggle_keysym(self) -> int:
  130. return self._xdisplay.keycode_to_keysym(self._toggle_keycode, index=0)
  131. @property
  132. def _toggle_key_label(self) -> str:
  133. return keysym_to_label(self._toggle_keysym)
  134. def enable(self) -> None:
  135. for keycode in self._keycode_mappings.keys():
  136. if keycode != self._toggle_keycode:
  137. self._grab_key(keycode)
  138. self._enabled = True
  139. self._draw_tray_icon()
  140. logging.info(
  141. "rescriptoon is now enabled. press %s to disable.", self._toggle_key_label,
  142. )
  143. def _disable(self) -> None:
  144. for keycode in self._keycode_mappings.keys():
  145. if keycode != self._toggle_keycode:
  146. self._ungrab_key(keycode)
  147. self._enabled = False
  148. self._draw_tray_icon()
  149. def disable(self) -> None:
  150. self._disable()
  151. logging.info(
  152. "rescriptoon is now disabled. press %s to enable.", self._toggle_key_label,
  153. )
  154. @property
  155. def enabled(self) -> bool:
  156. return self._enabled
  157. def toggle(self) -> None:
  158. if self.enabled:
  159. self.disable()
  160. else:
  161. self.enable()
  162. def _grab_key(self, keycode):
  163. for window in self._engine_windows:
  164. window.grab_key(
  165. keycode,
  166. X.AnyModifier,
  167. # owner_events
  168. # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event
  169. # False,
  170. True,
  171. X.GrabModeAsync,
  172. X.GrabModeAsync,
  173. )
  174. def _ungrab_key(self, keycode):
  175. for window in self._engine_windows:
  176. window.ungrab_key(keycode, X.AnyModifier)
  177. def _find_engine_windows(self) -> typing.List["Xlib.display.Window"]:
  178. return sorted(
  179. filter(
  180. lambda w: w.get_wm_name() == _TOONTOWN_ENGINE_WINDOW_NAME,
  181. _x_walk_children_windows(self.xdisplay.screen().root),
  182. ),
  183. # relative x
  184. # window managers reparent
  185. key=lambda w: w.query_tree().parent.get_geometry().x,
  186. # workaround to get absolute position:
  187. # http://science.su/stuff/so/print_frame_geometry_of_all_windows.py
  188. )
  189. def _update_active_key_registry(self, xkeyevent):
  190. # see self._check_active_key_registry
  191. keycode = xkeyevent.detail
  192. if isinstance(xkeyevent, Xlib.protocol.event.KeyPress):
  193. self._active_key_registry[keycode] = xkeyevent
  194. elif keycode in self._active_key_registry:
  195. del self._active_key_registry[keycode]
  196. def _check_active_key_registry(self):
  197. """
  198. WORKAROUND
  199. For an unknown reason some key release events don't get queued
  200. when multiple keys are being released simultaneously.
  201. So we keep a hashmap of supposedly currently pressed keys
  202. and periodically compare it with xdispaly.query_keymap().
  203. ref: https://stackoverflow.com/q/18160792/5894777
  204. """
  205. # https://tronche.com/gui/x/xlib/input/XQueryKeymap.html
  206. keymap = self.xdisplay.query_keymap()
  207. missed_releases = []
  208. for keycode, press_event in self._active_key_registry.items():
  209. byte_index = keycode >> 3
  210. bit_index = keycode & ((1 << 3) - 1)
  211. if not keymap[byte_index] & (1 << bit_index):
  212. logging.debug("missed release event of key %d", keycode)
  213. missed_releases.append(
  214. Xlib.protocol.event.KeyRelease(
  215. window=press_event.window,
  216. detail=press_event.detail,
  217. state=press_event.state,
  218. root_x=press_event.root_x,
  219. root_y=press_event.root_y,
  220. event_x=press_event.event_x,
  221. event_y=press_event.event_y,
  222. child=press_event.child,
  223. root=press_event.root,
  224. time=X.CurrentTime,
  225. same_screen=press_event.same_screen,
  226. )
  227. )
  228. for release_event in missed_releases:
  229. self._handle_xkeyevent(release_event)