_actions.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 abc
  16. import logging
  17. import random
  18. import time
  19. import typing
  20. import Xlib.protocol.event
  21. import Xlib.X
  22. import Xlib.XK
  23. from rescriptoon._keys import keysym_to_label
  24. _XKEYEVENT_TYPE = typing.Union[
  25. Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease
  26. ]
  27. class _Action:
  28. @abc.abstractmethod
  29. def execute(self, overlay: "rescriptoon.Overlay", xkeyevent: _XKEYEVENT_TYPE):
  30. raise NotImplementedError()
  31. @abc.abstractproperty
  32. def description(self) -> str:
  33. raise NotImplementedError()
  34. class EngineAction(_Action):
  35. def __init__(self, target_engine_index: typing.Optional[int] = None):
  36. self._target_engine_index = target_engine_index
  37. @abc.abstractmethod
  38. def execute_on_window(
  39. self,
  40. overlay: "rescriptoon.Overlay",
  41. xkeyevent: _XKEYEVENT_TYPE,
  42. engine_window: "Xlib.display.Window",
  43. ):
  44. raise NotImplementedError()
  45. def execute(
  46. self, overlay: "rescriptoon.Overlay", xkeyevent: _XKEYEVENT_TYPE,
  47. ):
  48. if self._target_engine_index is None:
  49. for target_window in overlay.engine_windows:
  50. self.execute_on_window(overlay, xkeyevent, target_window)
  51. elif self._target_engine_index >= len(overlay.engine_windows):
  52. logging.warning("target engine index out of bounds")
  53. else:
  54. self.execute_on_window(
  55. overlay, xkeyevent, overlay.engine_windows[self._target_engine_index]
  56. )
  57. @property
  58. def _engine_label(self) -> str:
  59. if self._target_engine_index is None:
  60. return "all engines"
  61. return "engine #{}".format(self._target_engine_index)
  62. class CenterClickAction(EngineAction):
  63. def __init__(self, target_engine_index: int, factor_x: float, factor_y: float):
  64. super().__init__(target_engine_index=target_engine_index,)
  65. self._button = Xlib.X.Button1
  66. self._factor_x = factor_x
  67. self._factor_y = factor_y
  68. def execute_on_window(
  69. self,
  70. overlay: "rescriptoon.Overlay",
  71. xkeyevent: _XKEYEVENT_TYPE,
  72. engine_window: "Xlib.display.Window",
  73. ):
  74. engine_geometry = engine_window.get_geometry()
  75. smaller_dimension = min(engine_geometry.width, engine_geometry.height)
  76. attr = dict(
  77. window=engine_window,
  78. detail=self._button,
  79. state=xkeyevent.state,
  80. event_x=int(engine_geometry.width / 2 + smaller_dimension * self._factor_x),
  81. event_y=int(
  82. engine_geometry.height / 2 + smaller_dimension * self._factor_y
  83. ),
  84. # apparently root_x & root_y do not need to correspond with event_x/y.
  85. # attributes are still required to be set.
  86. root_x=0, # xkeyevent.root_x,
  87. root_y=0, # xkeyevent.root_y,
  88. child=xkeyevent.child,
  89. root=xkeyevent.root,
  90. time=xkeyevent.time, # X.CurrentTime
  91. same_screen=xkeyevent.same_screen,
  92. )
  93. if isinstance(xkeyevent, Xlib.protocol.event.KeyPress):
  94. button_event = Xlib.protocol.event.ButtonPress(**attr)
  95. else:
  96. button_event = Xlib.protocol.event.ButtonRelease(**attr)
  97. engine_window.send_event(button_event)
  98. class SelectGagAction(CenterClickAction):
  99. X_OFFSET = -0.286
  100. X_FACTOR = 0.081
  101. def __init__(
  102. self,
  103. target_engine_index: int,
  104. column_index: int,
  105. factor_y: float,
  106. gag_name: str,
  107. ):
  108. super().__init__(
  109. target_engine_index=target_engine_index,
  110. factor_x=self.X_OFFSET + self.X_FACTOR * column_index,
  111. factor_y=factor_y,
  112. )
  113. self._gag_name = gag_name
  114. @property
  115. def description(self) -> str:
  116. return "select {} in battle @ {}".format(self._gag_name, self._engine_label)
  117. def _send_rewritten_xkeyevent(
  118. event_template: _XKEYEVENT_TYPE,
  119. window: "Xlib.display.Window",
  120. event_type: typing.Optional[typing.Type] = None,
  121. keycode=None,
  122. ) -> _XKEYEVENT_TYPE:
  123. if not event_type:
  124. event_type = type(event_template)
  125. window.send_event(
  126. event_type(
  127. window=window,
  128. detail=keycode if keycode else event_template.detail,
  129. state=event_template.state,
  130. root_x=event_template.root_x,
  131. root_y=event_template.root_y,
  132. event_x=event_template.event_x,
  133. event_y=event_template.event_y,
  134. child=event_template.child,
  135. root=event_template.root,
  136. time=event_template.time, # X.CurrentTime
  137. same_screen=event_template.same_screen,
  138. )
  139. )
  140. class RewriteKeyEventAction(EngineAction):
  141. def __init__(
  142. self,
  143. target_engine_index: typing.Optional[int] = None,
  144. keysym: typing.Optional[int] = None,
  145. ):
  146. super().__init__(target_engine_index=target_engine_index,)
  147. self._keysym = keysym
  148. def execute_on_window(
  149. self,
  150. overlay: "rescriptoon.Overlay",
  151. xkeyevent: _XKEYEVENT_TYPE,
  152. engine_window: "Xlib.display.Window",
  153. ):
  154. _send_rewritten_xkeyevent(
  155. event_template=xkeyevent,
  156. window=engine_window,
  157. keycode=overlay.xdisplay.keysym_to_keycode(self._keysym)
  158. if self._keysym is not None
  159. else xkeyevent.detail,
  160. )
  161. @property
  162. def description(self) -> str:
  163. if self._keysym is None:
  164. return "forward to {}".format(self._engine_label)
  165. return "send {} to {}".format(keysym_to_label(self._keysym), self._engine_label)
  166. class LowThrowAction(EngineAction):
  167. _THROW_KEYSYM = Xlib.XK.XK_Delete # pylint: no-member
  168. def execute_on_window(
  169. self,
  170. overlay: "rescriptoon.Overlay",
  171. xkeyevent: _XKEYEVENT_TYPE,
  172. engine_window: "Xlib.display.Window",
  173. ):
  174. if isinstance(xkeyevent, Xlib.protocol.event.KeyRelease):
  175. return
  176. keycode = overlay.xdisplay.keysym_to_keycode(self._THROW_KEYSYM)
  177. _send_rewritten_xkeyevent(
  178. event_template=xkeyevent,
  179. window=engine_window,
  180. keycode=keycode,
  181. event_type=Xlib.protocol.event.KeyPress,
  182. )
  183. time.sleep(random.uniform(0.04, 0.08))
  184. _send_rewritten_xkeyevent(
  185. event_template=xkeyevent,
  186. window=engine_window,
  187. keycode=keycode,
  188. event_type=Xlib.protocol.event.KeyRelease,
  189. )
  190. @property
  191. def description(self) -> str:
  192. return "low throw @ {}".format(self._engine_label)
  193. class ToggleOverlayAction(_Action):
  194. def execute(self, overlay: "rescriptoon.Overlay", xkeyevent: _XKEYEVENT_TYPE):
  195. if isinstance(xkeyevent, Xlib.protocol.event.KeyPress):
  196. overlay.toggle()
  197. @property
  198. def description(self) -> str:
  199. return "enable/disable rescriptoon"