__init__.py 10 KB

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