__init__.py 9.5 KB

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