dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

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()