_actions.py 6.7 KB

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