2025-08-19 08:06:37 -04:00

327 lines
10 KiB
Python

import time
from typing import Literal, Optional
from talon import Context, Module, actions, app, cron, ctrl, imgui, settings, ui
continuous_scroll_mode = ""
scroll_job = None
gaze_job = None
scroll_dir: Literal[-1, 1] = 1
scroll_start_ts: float = 0
hiss_scroll_up = False
control_mouse_forced = False
continuous_scrolling_speed_factor: float = 1.0
is_continuous_scrolling_vertical: bool = True
mod = Module()
ctx = Context()
mod.list(
"continuous_scrolling_direction",
desc="Defines names for directions used with continuous scrolling",
)
mod.setting(
"mouse_wheel_down_amount",
type=int,
default=120,
desc="The amount to scroll up/down (equivalent to mouse wheel on Windows by default)",
)
mod.setting(
"mouse_wheel_horizontal_amount",
type=int,
default=40,
desc="The amount to scroll left/right",
)
mod.setting(
"mouse_continuous_scroll_amount",
type=int,
default=8,
desc="The default amount used when scrolling continuously",
)
mod.setting(
"mouse_continuous_scroll_acceleration",
type=float,
default=1,
desc="The maximum (linear) acceleration factor when scrolling continuously. 1=constant speed/no acceleration",
)
mod.setting(
"mouse_enable_hiss_scroll",
type=bool,
default=False,
desc="Hiss noise scrolls down when enabled",
)
mod.setting(
"mouse_hide_mouse_gui",
type=bool,
default=False,
desc="When enabled, the 'Scroll Mouse' GUI will not be shown.",
)
mod.setting(
"mouse_continuous_scroll_speed_quotient",
type=float,
default=10.0,
desc="When adjusting the continuous scrolling speed through voice commands, the result is that the speed is multiplied by the dictated number divided by this number.",
)
mod.setting(
"mouse_gaze_scroll_speed_multiplier",
type=float,
default=1.0,
desc="This multiplies the gaze scroll speed",
)
mod.tag(
"continuous_scrolling",
desc="Allows commands for adjusting continuous scrolling behavior",
)
@imgui.open(x=700, y=0)
def gui_wheel(gui: imgui.GUI):
gui.text(f"Scroll mode: {continuous_scroll_mode}")
gui.text(f"say a number between 0 and 99 to set scrolling speed")
gui.line()
if gui.button("[Wheel] Stop"):
actions.user.mouse_scroll_stop()
@mod.action_class
class Actions:
def mouse_scroll_up(amount: float = 1):
"""Scrolls up"""
y = amount * settings.get("user.mouse_wheel_down_amount")
actions.mouse_scroll(-y)
def mouse_scroll_down(amount: float = 1):
"""Scrolls down"""
y = amount * settings.get("user.mouse_wheel_down_amount")
actions.mouse_scroll(y)
def mouse_scroll_left(amount: float = 1):
"""Scrolls left"""
x = amount * settings.get("user.mouse_wheel_horizontal_amount")
actions.mouse_scroll(0, -x)
def mouse_scroll_right(amount: float = 1):
"""Scrolls right"""
x = amount * settings.get("user.mouse_wheel_horizontal_amount")
actions.mouse_scroll(0, x)
def mouse_scroll_continuous(direction: str, speed_factor: Optional[int] = None):
"""Scrolls continuously in the given direction"""
match direction:
case "UP":
actions.user.mouse_scroll_up_continuous(speed_factor)
case "DOWN":
actions.user.mouse_scroll_down_continuous(speed_factor)
case "LEFT":
actions.user.mouse_scroll_left_continuous(speed_factor)
case "RIGHT":
actions.user.mouse_scroll_right_continuous(speed_factor)
case _:
raise ValueError(f"Invalid continuous scrolling direction: {direction}")
def mouse_scroll_up_continuous(speed_factor: Optional[int] = None):
"""Scrolls up continuously"""
mouse_scroll_continuous(-1, speed_factor)
def mouse_scroll_down_continuous(speed_factor: Optional[int] = None):
"""Scrolls down continuously"""
mouse_scroll_continuous(1, speed_factor)
def mouse_scroll_right_continuous(speed_factor: Optional[int] = None):
"""Scrolls right continuously"""
mouse_scroll_continuous(1, speed_factor, is_vertical=False)
def mouse_scroll_left_continuous(speed_factor: Optional[int] = None):
"""Scrolls left continuously"""
mouse_scroll_continuous(-1, speed_factor, is_vertical=False)
def mouse_gaze_scroll():
"""Starts gaze scroll"""
global gaze_job, continuous_scroll_mode, control_mouse_forced
ctx.tags = ["user.continuous_scrolling"]
continuous_scroll_mode = "gaze scroll"
gaze_job = cron.interval("16ms", scroll_gaze_helper)
if not settings.get("user.mouse_hide_mouse_gui"):
gui_wheel.show()
# enable 'control mouse' if eye tracker is present and not enabled already
if not actions.tracking.control_enabled():
actions.tracking.control_toggle(True)
control_mouse_forced = True
def mouse_gaze_scroll_toggle():
"""If not scrolling, start gaze scroll, else stop scrolling."""
if continuous_scroll_mode == "":
actions.user.mouse_gaze_scroll()
else:
actions.user.mouse_scroll_stop()
def mouse_scroll_stop() -> bool:
"""Stops scrolling"""
global scroll_job, gaze_job, continuous_scroll_mode, control_mouse_forced, continuous_scrolling_speed_factor
continuous_scroll_mode = ""
continuous_scrolling_speed_factor = 1.0
return_value = False
ctx.tags = []
if scroll_job:
cron.cancel(scroll_job)
scroll_job = None
return_value = True
if gaze_job:
cron.cancel(gaze_job)
gaze_job = None
return_value = True
if control_mouse_forced:
actions.tracking.control_toggle(False)
control_mouse_forced = False
gui_wheel.hide()
return return_value
def mouse_scroll_set_speed(speed: Optional[int]):
"""Sets the continuous scrolling speed for the current scrolling"""
global continuous_scrolling_speed_factor, scroll_start_ts
if scroll_start_ts:
scroll_start_ts = time.perf_counter()
if speed is None:
continuous_scrolling_speed_factor = 1.0
else:
continuous_scrolling_speed_factor = speed / settings.get(
"user.mouse_continuous_scroll_speed_quotient"
)
def mouse_is_continuous_scrolling():
"""Returns whether continuous scroll is in progress"""
return len(continuous_scroll_mode) > 0
def hiss_scroll_up():
"""Change mouse hiss scroll direction to up"""
global hiss_scroll_up
hiss_scroll_up = True
def hiss_scroll_down():
"""Change mouse hiss scroll direction to down"""
global hiss_scroll_up
hiss_scroll_up = False
@ctx.action_class("user")
class UserActions:
def noise_trigger_hiss(active: bool):
if settings.get("user.mouse_enable_hiss_scroll"):
if active:
if hiss_scroll_up:
actions.user.mouse_scroll_up_continuous()
else:
actions.user.mouse_scroll_down_continuous()
else:
actions.user.mouse_scroll_stop()
def mouse_scroll_continuous(
new_scroll_dir: Literal[-1, 1],
speed_factor: Optional[int] = None,
is_vertical: bool = True,
):
global scroll_job, scroll_dir, scroll_start_ts, is_continuous_scrolling_vertical
actions.user.mouse_scroll_set_speed(speed_factor)
was_vertical = is_continuous_scrolling_vertical
is_continuous_scrolling_vertical = is_vertical
update_continuous_scrolling_mode(new_scroll_dir, is_vertical)
if scroll_job:
# Issuing a scroll in the same direction aborts scrolling
if scroll_dir == new_scroll_dir and was_vertical == is_vertical:
actions.user.mouse_scroll_stop()
# Issuing a scroll in the reverse direction resets acceleration
else:
scroll_dir = new_scroll_dir
scroll_start_ts = time.perf_counter()
else:
scroll_dir = new_scroll_dir
scroll_start_ts = time.perf_counter()
scroll_continuous_helper()
scroll_job = cron.interval("16ms", scroll_continuous_helper)
ctx.tags = ["user.continuous_scrolling"]
if not settings.get("user.mouse_hide_mouse_gui"):
gui_wheel.show()
def update_continuous_scrolling_mode(new_scroll_dir: Literal[-1, 1], is_vertical: bool):
global continuous_scroll_mode
if new_scroll_dir == -1:
if is_vertical:
continuous_scroll_mode = "scroll up continuous"
else:
continuous_scroll_mode = "scroll left continuous"
else:
if is_vertical:
continuous_scroll_mode = "scroll down continuous"
else:
continuous_scroll_mode = "scroll right continuous"
def scroll_continuous_helper():
scroll_amount = (
settings.get("user.mouse_continuous_scroll_amount")
* continuous_scrolling_speed_factor
)
acceleration_setting = settings.get("user.mouse_continuous_scroll_acceleration")
acceleration_speed = (
1 + min((time.perf_counter() - scroll_start_ts) / 0.5, acceleration_setting - 1)
if acceleration_setting > 1
else 1
)
scroll_delta = round(scroll_amount * acceleration_speed * scroll_dir)
if scroll_delta == 0:
scroll_delta = scroll_dir
if is_continuous_scrolling_vertical:
actions.mouse_scroll(scroll_delta)
else:
actions.mouse_scroll(0, scroll_delta)
def scroll_gaze_helper():
x, y = ctrl.mouse_pos()
# The window containing the mouse
window = get_window_containing(x, y)
if window is None:
return
rect = window.rect
midpoint = rect.center.y
factor = continuous_scrolling_speed_factor * settings.get(
"user.mouse_gaze_scroll_speed_multiplier"
)
amount = factor * (((y - midpoint) / (rect.height / 10)) ** 3)
actions.mouse_scroll(amount)
def get_window_containing(x: float, y: float):
# on windows, check the active_window first since ui.windows() is not z-ordered
if app.platform == "windows" and ui.active_window().rect.contains(x, y):
return ui.active_window()
for window in ui.windows():
if window.rect.contains(x, y):
return window
return None