mouse_grid.py (10079B)
1 # courtesy of https://github.com/timo/ 2 # see https://github.com/timo/talon_scripts 3 from talon import Module, Context, app, canvas, screen, settings, ui, ctrl, cron 4 from talon.skia import Shader, Color, Paint, Rect 5 from talon.types.point import Point2d 6 from talon_plugins import eye_mouse, eye_zoom_mouse 7 from typing import Union 8 9 import math, time 10 11 import typing 12 13 mod = Module() 14 narrow_expansion = mod.setting( 15 "grid_narrow_expansion", 16 type=int, 17 default=0, 18 desc="""After narrowing, grow the new region by this many pixels in every direction, to make things immediately on edges easier to hit, and when the grid is at its smallest, it allows you to still nudge it around""", 19 ) 20 21 mod.tag("mouse_grid_showing", desc="Tag indicates whether the mouse grid is showing") 22 mod.tag("mouse_grid_enabled", desc="Tag enables the mouse grid commands.") 23 ctx = Context() 24 25 26 class MouseSnapNine: 27 def __init__(self): 28 self.screen = None 29 self.rect = None 30 self.history = [] 31 self.img = None 32 self.mcanvas = None 33 self.active = False 34 self.count = 0 35 self.was_control_mouse_active = False 36 self.was_zoom_mouse_active = False 37 38 def setup(self, *, rect: Rect = None, screen_num: int = None): 39 screens = ui.screens() 40 # each if block here might set the rect to None to indicate failure 41 if rect is not None: 42 try: 43 screen = ui.screen_containing(*rect.center) 44 except Exception: 45 rect = None 46 if rect is None and screen_num is not None: 47 screen = screens[screen_num % len(screens)] 48 rect = screen.rect 49 if rect is None: 50 screen = screens[0] 51 rect = screen.rect 52 self.rect = rect.copy() 53 self.screen = screen 54 self.count = 0 55 self.img = None 56 if self.mcanvas is not None: 57 self.mcanvas.close() 58 self.mcanvas = canvas.Canvas.from_screen(screen) 59 if self.active: 60 self.mcanvas.register("draw", self.draw) 61 self.mcanvas.freeze() 62 63 def show(self): 64 if self.active: 65 return 66 # noinspection PyUnresolvedReferences 67 if eye_zoom_mouse.zoom_mouse.enabled: 68 self.was_zoom_mouse_active = True 69 eye_zoom_mouse.toggle_zoom_mouse(False) 70 if eye_mouse.control_mouse.enabled: 71 self.was_control_mouse_active = True 72 eye_mouse.control_mouse.toggle() 73 self.mcanvas.register("draw", self.draw) 74 self.mcanvas.freeze() 75 self.active = True 76 return 77 78 def close(self): 79 if not self.active: 80 return 81 self.mcanvas.unregister("draw", self.draw) 82 self.mcanvas.close() 83 self.mcanvas = None 84 self.img = None 85 86 self.active = False 87 if self.was_control_mouse_active and not eye_mouse.control_mouse.enabled: 88 eye_mouse.control_mouse.toggle() 89 if self.was_zoom_mouse_active and not eye_zoom_mouse.zoom_mouse.enabled: 90 eye_zoom_mouse.toggle_zoom_mouse(True) 91 92 self.was_zoom_mouse_active = False 93 self.was_control_mouse_active = False 94 95 def draw(self, canvas): 96 paint = canvas.paint 97 98 def draw_grid(offset_x, offset_y, width, height): 99 canvas.draw_line( 100 offset_x + width // 3, 101 offset_y, 102 offset_x + width // 3, 103 offset_y + height, 104 ) 105 canvas.draw_line( 106 offset_x + 2 * width // 3, 107 offset_y, 108 offset_x + 2 * width // 3, 109 offset_y + height, 110 ) 111 112 canvas.draw_line( 113 offset_x, 114 offset_y + height // 3, 115 offset_x + width, 116 offset_y + height // 3, 117 ) 118 canvas.draw_line( 119 offset_x, 120 offset_y + 2 * height // 3, 121 offset_x + width, 122 offset_y + 2 * height // 3, 123 ) 124 125 def draw_crosses(offset_x, offset_y, width, height): 126 for row in range(0, 2): 127 for col in range(0, 2): 128 cx = offset_x + width / 6 + (col + 0.5) * width / 3 129 cy = offset_y + height / 6 + (row + 0.5) * height / 3 130 131 canvas.draw_line(cx - 10, cy, cx + 10, cy) 132 canvas.draw_line(cx, cy - 10, cx, cy + 10) 133 134 grid_stroke = 1 135 136 def draw_text(offset_x, offset_y, width, height): 137 canvas.paint.text_align = canvas.paint.TextAlign.CENTER 138 for row in range(3): 139 for col in range(3): 140 text_string = "" 141 if settings["user.grids_put_one_bottom_left"]: 142 text_string = f"{(2 - row)*3+col+1}" 143 else: 144 text_string = f"{row*3+col+1}" 145 text_rect = canvas.paint.measure_text(text_string)[1] 146 background_rect = text_rect.copy() 147 background_rect.center = Point2d( 148 offset_x + width / 6 + col * width / 3, 149 offset_y + height / 6 + row * height / 3, 150 ) 151 background_rect = background_rect.inset(-4) 152 paint.color = "9999995f" 153 paint.style = Paint.Style.FILL 154 canvas.draw_rect(background_rect) 155 paint.color = "00ff00ff" 156 canvas.draw_text( 157 text_string, 158 offset_x + width / 6 + col * width / 3, 159 offset_y + height / 6 + row * height / 3 + text_rect.height / 2, 160 ) 161 162 if self.count < 2: 163 paint.color = "00ff007f" 164 for which in range(1, 10): 165 gap = 35 - self.count * 10 166 if not self.active: 167 gap = 45 168 draw_crosses(*self.calc_narrow(which, self.rect)) 169 170 paint.stroke_width = grid_stroke 171 if self.active: 172 paint.color = "ff0000ff" 173 else: 174 paint.color = "000000ff" 175 if self.count >= 2: 176 aspect = self.rect.width / self.rect.height 177 if aspect >= 1: 178 w = self.screen.width / 3 179 h = w / aspect 180 else: 181 h = self.screen.height / 3 182 w = h * aspect 183 x = self.screen.x + (self.screen.width - w) / 2 184 y = self.screen.y + (self.screen.height - h) / 2 185 self.draw_zoom(canvas, x, y, w, h) 186 draw_grid(x, y, w, h) 187 draw_text(x, y, w, h) 188 else: 189 draw_grid(self.rect.x, self.rect.y, self.rect.width, self.rect.height) 190 191 paint.textsize += 12 - self.count * 3 192 draw_text(self.rect.x, self.rect.y, self.rect.width, self.rect.height) 193 194 def calc_narrow(self, which, rect): 195 rect = rect.copy() 196 bdr = narrow_expansion.get() 197 row = int(which - 1) // 3 198 col = int(which - 1) % 3 199 if settings["user.grids_put_one_bottom_left"]: 200 row = 2 - row 201 rect.x += int(col * rect.width // 3) - bdr 202 rect.y += int(row * rect.height // 3) - bdr 203 rect.width = (rect.width // 3) + bdr * 2 204 rect.height = (rect.height // 3) + bdr * 2 205 return rect 206 207 def narrow(self, which, move=True): 208 if which < 1 or which > 9: 209 return 210 self.save_state() 211 rect = self.calc_narrow(which, self.rect) 212 # check count so we don't bother zooming in _too_ far 213 if self.count < 5: 214 self.rect = rect.copy() 215 self.count += 1 216 if move: 217 ctrl.mouse_move(*rect.center) 218 if self.count >= 2: 219 self.update_screenshot() 220 else: 221 self.mcanvas.freeze() 222 223 def update_screenshot(self): 224 def finish_capture(): 225 self.img = screen.capture_rect(self.rect) 226 self.mcanvas.freeze() 227 228 self.mcanvas.hide() 229 cron.after("16ms", finish_capture) 230 231 def draw_zoom(self, canvas, x, y, w, h): 232 if self.img: 233 src = Rect(0, 0, self.img.width, self.img.height) 234 dst = Rect(x, y, w, h) 235 canvas.draw_image_rect(self.img, src, dst) 236 237 def narrow_to_pos(self, x, y): 238 col_size = int(self.width // 3) 239 row_size = int(self.height // 3) 240 col = math.floor((x - self.rect.x) / col_size) 241 row = math.floor((y - self.rect.x) / row_size) 242 self.narrow(1 + col + 3 * row, move=False) 243 244 def save_state(self): 245 self.history.append((self.count, self.rect.copy())) 246 247 def go_back(self): 248 # FIXME: need window and screen tracking 249 self.count, self.rect = self.history.pop() 250 self.mcanvas.freeze() 251 252 253 mg = MouseSnapNine() 254 255 256 @mod.action_class 257 class GridActions: 258 def grid_activate(): 259 """Show mouse grid""" 260 if not mg.mcanvas: 261 mg.setup() 262 mg.show() 263 ctx.tags = ["user.mouse_grid_showing"] 264 265 def grid_place_window(): 266 """Places the grid on the currently active window""" 267 mg.setup(rect=ui.active_window().rect) 268 269 def grid_reset(): 270 """Resets the grid to fill the whole screen again""" 271 if mg.active: 272 mg.setup() 273 274 def grid_select_screen(screen: int): 275 """Brings up mouse grid""" 276 mg.setup(screen_num=screen - 1) 277 mg.show() 278 279 def grid_narrow_list(digit_list: typing.List[str]): 280 """Choose fields multiple times in a row""" 281 for d in digit_list: 282 GridActions.grid_narrow(int(d)) 283 284 def grid_narrow(digit: Union[int, str]): 285 """Choose a field of the grid and narrow the selection down""" 286 mg.narrow(int(digit)) 287 288 def grid_go_back(): 289 """Sets the grid state back to what it was before the last command""" 290 mg.go_back() 291 292 def grid_close(): 293 """Close the active grid""" 294 ctx.tags = [] 295 mg.close()