__init__.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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, toggle_keysym: int):
  48. self._xdisplay = Xlib.display.Display()
  49. self._toggle_keycode: int = self._xdisplay.keysym_to_keycode(toggle_keysym)
  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 %s", keysym_to_label(toggle_keysym)
  54. )
  55. self._keycode_mappings[self._toggle_keycode] = ToggleOverlayAction()
  56. self._active_key_registry = {}
  57. self._enabled = False
  58. self._engine_windows = None
  59. try:
  60. self._tray_icon = TrayIcon(display=self._xdisplay)
  61. except SystemTrayUnavailable:
  62. self._tray_icon = None
  63. @property
  64. def xdisplay(self) -> Xlib.display.Display:
  65. return self._xdisplay
  66. @property
  67. def engine_windows(self) -> typing.List["Xlib.display.Window"]:
  68. return self._engine_windows
  69. @property
  70. def _engine_windows_open(self) -> bool:
  71. for window in self._engine_windows:
  72. try:
  73. window.get_wm_state()
  74. except Xlib.error.BadWindow:
  75. logging.info("engine window %x is no longer available", window.id)
  76. return False
  77. return True
  78. def run(self) -> None:
  79. self._engine_windows = self._find_engine_windows()
  80. logging.debug("engine window ids %r", [hex(w.id) for w in self._engine_windows])
  81. if not self._engine_windows:
  82. raise Exception("no toontown window found")
  83. self._grab_key(self._toggle_keycode)
  84. print("key bindings:")
  85. for keycode, action in self._keycode_mappings.items():
  86. keysym: int = self._xdisplay.keycode_to_keysym(keycode, index=0)
  87. print("{}: {}".format(keysym_to_label(keysym), action.description,))
  88. self.enable()
  89. while self._engine_windows_open:
  90. while self.xdisplay.pending_events():
  91. self._handle_xevent(self.xdisplay.next_event())
  92. self._check_active_key_registry()
  93. # keep timeout low for _check_active_key_registry()
  94. # to be called frequently
  95. _x_wait_for_event(self.xdisplay, timeout_seconds=0.05)
  96. self._disable()
  97. def _draw_tray_icon(self) -> None:
  98. if self._tray_icon:
  99. self._tray_icon.draw(self.enabled)
  100. def _handle_xevent(self, xevent: Xlib.protocol.rq.Event) -> None:
  101. if isinstance(
  102. xevent, (Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease)
  103. ):
  104. self._handle_xkeyevent(xevent)
  105. elif self._tray_icon and xevent.type == Xlib.X.ConfigureNotify:
  106. self._draw_tray_icon()
  107. def _handle_xkeyevent(
  108. self,
  109. xkeyevent: typing.Union[
  110. Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease
  111. ],
  112. ) -> None:
  113. self._update_active_key_registry(xkeyevent)
  114. keycode_in = xkeyevent.detail
  115. try:
  116. action = self._keycode_mappings[keycode_in]
  117. except KeyError:
  118. keysym_in = self.xdisplay.keycode_to_keysym(keycode_in, index=0,)
  119. logging.warning(
  120. "received key event of unmapped key %d (%s)",
  121. keycode_in,
  122. keysym_to_label(keysym_in),
  123. )
  124. return
  125. action.execute(self, xkeyevent)
  126. @property
  127. def _toggle_keysym(self) -> int:
  128. return self._xdisplay.keycode_to_keysym(self._toggle_keycode, index=0)
  129. @property
  130. def _toggle_key_label(self) -> str:
  131. return keysym_to_label(self._toggle_keysym)
  132. def enable(self) -> None:
  133. for keycode in self._keycode_mappings.keys():
  134. if keycode != self._toggle_keycode:
  135. self._grab_key(keycode)
  136. self._enabled = True
  137. self._draw_tray_icon()
  138. logging.info(
  139. "rescriptoon is now enabled. press %s to disable.", self._toggle_key_label,
  140. )
  141. def _disable(self) -> None:
  142. for keycode in self._keycode_mappings.keys():
  143. if keycode != self._toggle_keycode:
  144. self._ungrab_key(keycode)
  145. self._enabled = False
  146. self._draw_tray_icon()
  147. def disable(self) -> None:
  148. self._disable()
  149. logging.info(
  150. "rescriptoon is now disabled. press %s to enable.", self._toggle_key_label,
  151. )
  152. @property
  153. def enabled(self) -> bool:
  154. return self._enabled
  155. def toggle(self) -> None:
  156. if self.enabled:
  157. self.disable()
  158. else:
  159. self.enable()
  160. def _grab_key(self, keycode):
  161. for window in self._engine_windows:
  162. window.grab_key(
  163. keycode,
  164. X.AnyModifier,
  165. # owner_events
  166. # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event
  167. # False,
  168. True,
  169. X.GrabModeAsync,
  170. X.GrabModeAsync,
  171. )
  172. def _ungrab_key(self, keycode):
  173. for window in self._engine_windows:
  174. window.ungrab_key(keycode, X.AnyModifier)
  175. def _find_engine_windows(self) -> typing.List["Xlib.display.Window"]:
  176. return sorted(
  177. filter(
  178. lambda w: w.get_wm_name() == _TOONTOWN_ENGINE_WINDOW_NAME,
  179. _x_walk_children_windows(self.xdisplay.screen().root),
  180. ),
  181. # relative x
  182. # won't work when using a reparenting window manager
  183. # https://stackoverflow.com/a/12854004/5894777
  184. # possible workaround:
  185. # http://science.su/stuff/so/print_frame_geometry_of_all_windows.py
  186. key=lambda w: w.get_geometry().x,
  187. )
  188. def _update_active_key_registry(self, xkeyevent):
  189. # see self._check_active_key_registry
  190. keycode = xkeyevent.detail
  191. if isinstance(xkeyevent, Xlib.protocol.event.KeyPress):
  192. self._active_key_registry[keycode] = xkeyevent
  193. elif keycode in self._active_key_registry:
  194. del self._active_key_registry[keycode]
  195. def _check_active_key_registry(self):
  196. """
  197. WORKAROUND
  198. For an unknown reason some key release events don't get queued
  199. when multiple keys are being released simultaneously.
  200. So we keep a hashmap of supposedly currently pressed keys
  201. and periodically compare it with xdispaly.query_keymap().
  202. ref: https://stackoverflow.com/q/18160792/5894777
  203. """
  204. # https://tronche.com/gui/x/xlib/input/XQueryKeymap.html
  205. keymap = self.xdisplay.query_keymap()
  206. missed_releases = []
  207. for keycode, press_event in self._active_key_registry.items():
  208. byte_index = keycode >> 3
  209. bit_index = keycode & ((1 << 3) - 1)
  210. if not keymap[byte_index] & (1 << bit_index):
  211. logging.debug("missed release event of key %d", keycode)
  212. missed_releases.append(
  213. Xlib.protocol.event.KeyRelease(
  214. window=press_event.window,
  215. detail=press_event.detail,
  216. state=press_event.state,
  217. root_x=press_event.root_x,
  218. root_y=press_event.root_y,
  219. event_x=press_event.event_x,
  220. event_y=press_event.event_y,
  221. child=press_event.child,
  222. root=press_event.root,
  223. time=X.CurrentTime,
  224. same_screen=press_event.same_screen,
  225. )
  226. )
  227. for release_event in missed_releases:
  228. self._handle_xkeyevent(release_event)