Skip to content

Commit

Permalink
Version 1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
yezhiyi9670 committed Nov 11, 2024
1 parent 0c32936 commit 261b424
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 254 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/slow-mouse.spec
/config.json
/pid.log
__pycache__
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,31 @@ Slow Mouse

> [Buy me a coffee](https://afdian.net/a/yezhiyi9670) if you like this project.
Just as the name suggests, the aim of this project is simple - now you can slow your mouse down by simply holding a modifier key. In this way, you can easily align your mouse to a certain pixel. It's useful under circumstances such as:
Just as the name suggests, the aim of this project is simple - now you can slow your mouse down by simply holding a modifier key. In this way, you can easily align your mouse to a certain pixel. **It also works for touchpads on Windows 11 24H2 or later**.

It's useful under circumstances such as:

- Dragging some slider such as the volume slider, and trying to obtain an accurate value.
- Trying to resize a window or a box but simply having trouble aligning the mouse to the anchor.
- Trying to select an accurate rectangle of the screen for screenshot or capturing.
- Trying to drag a piece of image to an accurate place. (it's tencent's captcha, isn't it?)

> Note: This application is Windows only. It works by changing the mouse sensitivity settings, so won't work for a touchpad. It should also not affect games that are using raw mouse input.
> Note: This application is Windows only. It works by changing various settings ([details in code here](./app/winparam_values.py)), and will not affect games that are using raw mouse input (e.g. Minecraft 1.14+ under default settings).
## Usage

To download prebuilt binaries, go to the Github releases page.
If you just want to use it, download `exe` in [**GitHub Releases**](https://github.com/yezhiyi9670/slow-mouse/releases).

Launch the application and you will see a tray icon. Right click it to modify the settings.
Launch the application and you will see a tray icon. Right click it to modify the settings. Note that as of version 1.0.0, **the app does nothing by default**. You have to enable the slow-down functionalities in the settings.

## Start on boot

This application does NOT start on boot automatically. You can configure it to do so by [dropping its shortcut into the Startup folder](https://cn.bing.com/search?q=dropping+shortcut+into+startup+folder).
This application does NOT start on boot automatically. You can configure it to do so by [dropping its shortcut into the Startup folder](https://cn.bing.com/search?q=Add+shortcut+to+startup).

## Testing and building

To test it, simply run `python slow-mouse.py`.

To build, run `build.cmd`. It relies on the `pyinstaller` package.

Check `requirements.txt` if necessary.
28 changes: 28 additions & 0 deletions app/app_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version = '1.0.0'

import os
import sys

main_file_path = ''

def is_packaged():
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
return True
else:
return False

def get_program_dir():
return os.path.dirname(sys.executable if is_packaged() else main_file_path)

def get_program_basename():
return os.path.basename(sys.executable if is_packaged() else main_file_path)

def has_touchpad_speed_support():
flag = True
try:
sys_win_ver = sys.getwindowsversion()
if not sys_win_ver.build or sys_win_ver.build < 26000:
flag = False
except AttributeError:
flag = False
return flag
58 changes: 58 additions & 0 deletions app/config_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os, json
from typing import Union

from app.winparam_manager import WinparamManager

class ConfigManager:
defaults: dict[str, Union[int, str]] = {
'key': 'RightAlt'
}

'File path'
path: str = ''

'Attached winparam manager'
winparam_manager: WinparamManager

'Data'
data: dict[str, Union[int, str]] = {}

'Read or initialize data'
def __init__(self, path: str, winparam_manager: WinparamManager):
self.path = path
self.winparam_manager = winparam_manager

for name in winparam_manager.value_obj_map:
self.defaults['winparam:' + name] = -1

if not os.path.exists(self.path):
open(self.path, 'w').write(json.dumps(self.defaults))
self.data = json.load(open(self.path, 'r'))

'Write data'
def commit(self):
open(self.path, 'w').write(json.dumps(self.data))

'Get item'
def get(self, key):
value = self.data.get(key)
if value == None:
return self.defaults[key]
return value

'Set item'
def set(self, key, val):
self.data[key] = val

def set_and_commit(self, key, val):
self.set(key, val)
self.commit()

'Set values on the attached winparam manager using the configuration'
def set_winparam_values(self):
for name in self.winparam_manager.value_obj_map:
value_obj = self.winparam_manager.value_obj_map[name]
config_val = self.get('winparam:' + name)
if isinstance(config_val, str) or config_val < 0:
continue
value_obj.set(config_val)
35 changes: 35 additions & 0 deletions app/instance_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os
import psutil
from app import app_env

'The facility that prevents running multiple instances'
class SelfDetect:
path: str = ''
def detect(self):
self.path = os.path.join('', 'pid.log')
if os.path.exists(self.path):
fp = open(self.path,'r')
split_flag = fp.read().strip().split('/')
if len(split_flag) < 2:
return False
pid, identifier = split_flag[0], split_flag[1]
fp.close()
try:
target_pid = int(pid)
pid_iter = psutil.process_iter()
for pid in pid_iter:
if pid.pid == target_pid and pid.name() == identifier:
return True
return False
except:
return False
else:
return False
def write(self):
pid = os.getpid()
fp = open(self.path, 'w')
fp.write(str(pid) + '/' + app_env.get_program_basename())
fp.close()
def clean(self):
if os.path.exists(self.path):
os.unlink(self.path)
65 changes: 65 additions & 0 deletions app/touchpad_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import ctypes

# More info on this: https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-systemparametersinfoa
# https://github.com/ntdiff/headers/blob/ebe89c140e89b475005cd8696597ddf7406dfb8a/Win11_2409_24H2/x64/System32/ole32.dll/Standalone/TOUCHPAD_PARAMETERS.h#L17

# <ai-assisted-content>

# Define the enums with actual values
class LEGACY_TOUCHPAD_FEATURES(ctypes.c_uint):
LEGACY_TOUCHPAD_FEATURE_NONE = 0
LEGACY_TOUCHPAD_FEATURE_ENABLE_DISABLE = 1
LEGACY_TOUCHPAD_FEATURE_REVERSE_SCROLL_DIRECTION = 4

class TOUCHPAD_SENSITIVITY_LEVEL(ctypes.c_uint):
TOUCHPAD_SENSITIVITY_LEVEL_MOST_SENSITIVE = 0
TOUCHPAD_SENSITIVITY_LEVEL_HIGH_SENSITIVITY = 1
TOUCHPAD_SENSITIVITY_LEVEL_MEDIUM_SENSITIVITY = 2
TOUCHPAD_SENSITIVITY_LEVEL_LOW_SENSITIVITY = 3
TOUCHPAD_SENSITIVITY_LEVEL_LEAST_SENSITIVE = 4

# Define the structure for the bitfields
class TouchpadFlags1(ctypes.Structure):
_fields_ = [
("touchpadPresent", ctypes.c_int, 1),
("legacyTouchpadPresent", ctypes.c_int, 1),
("externalMousePresent", ctypes.c_int, 1),
("touchpadEnabled", ctypes.c_int, 1),
("touchpadActive", ctypes.c_int, 1),
("feedbackSupported", ctypes.c_int, 1),
("clickForceSupported", ctypes.c_int, 1),
("Reserved1", ctypes.c_int, 25),
]

class TouchpadFlags2(ctypes.Structure):
_fields_ = [
("allowActiveWhenMousePresent", ctypes.c_int, 1),
("feedbackEnabled", ctypes.c_int, 1),
("tapEnabled", ctypes.c_int, 1),
("tapAndDragEnabled", ctypes.c_int, 1),
("twoFingerTapEnabled", ctypes.c_int, 1),
("rightClickZoneEnabled", ctypes.c_int, 1),
("mouseAccelSettingHonored", ctypes.c_int, 1),
("panEnabled", ctypes.c_int, 1),
("zoomEnabled", ctypes.c_int, 1),
("scrollDirectionReversed", ctypes.c_int, 1),
("Reserved2", ctypes.c_int, 22),
]

# Define the main structure
class TOUCHPAD_PARAMETERS(ctypes.Structure):
_fields_ = [
("versionNumber", ctypes.c_uint),
("maxSupportedContacts", ctypes.c_uint),
("legacyTouchpadFeatures", LEGACY_TOUCHPAD_FEATURES),
("flags1", TouchpadFlags1),
("flags2", TouchpadFlags2),
("sensitivityLevel", TOUCHPAD_SENSITIVITY_LEVEL),
("cursorSpeed", ctypes.c_uint),
("feedbackIntensity", ctypes.c_uint),
("clickForceSensitivity", ctypes.c_uint),
("rightClickZoneWidth", ctypes.c_uint),
("rightClickZoneHeight", ctypes.c_uint),
]

# </ai-assisted-content>
38 changes: 38 additions & 0 deletions app/winparam_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from app import winparam_values

class WinparamManager:
value_obj_map: dict[str, winparam_values.ValueInterface] = {}
initial_value_map: dict[str, int] = {}

def __init__(self):
values_to_init: list[winparam_values.ValueInterface] = [
winparam_values.ValueInterfaceMouseSpeed(),
winparam_values.ValueInterfaceHScroll(),
winparam_values.ValueInterfaceVScroll(),
winparam_values.ValueInterfaceTouchpadSpeed()
]
for value in values_to_init:
if not value.is_available():
continue
self.value_obj_map[value.name()] = value
self.retrieve_initials()

def retrieve_initials(self):
for name in self.value_obj_map:
value = self.value_obj_map[name]
self.initial_value_map[name] = value.get()
# print('initials', self.initial_value_map)

def revert_initials(self):
for name in self.value_obj_map:
value = self.value_obj_map[name]
value.set(self.initial_value_map[name])

def set_values(self, value_dict: dict[str, int]):
for name in self.value_obj_map:
value = self.value_obj_map[name]
to_set = value_dict.get(name)
if to_set == None:
continue
value.set(to_set)

77 changes: 77 additions & 0 deletions app/winparam_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import ctypes
from app.touchpad_params import TOUCHPAD_PARAMETERS
from app import app_env

def set_sys_value(key, value):
ctypes.windll.user32.SystemParametersInfoA(key, value, value, 0)

def get_sys_value(key):
ptr = ctypes.c_void_p(1)
ctypes.windll.user32.SystemParametersInfoA(key, 0, ctypes.byref(ptr), 0)
val = ptr.value
if val == None:
return 0
if(val < 1): return 1
if(val > 20): return 20
return val

class ValueInterface:
def name(self) -> str:
return '?'
def menu_entry(self) -> str:
return self.name()
def is_available(self) -> bool:
return True
def get(self) -> int:
raise NotImplemented()
def set(self, value) -> None:
raise NotImplemented()

class ValueInterfaceMouseSpeed(ValueInterface):
def name(self):
return 'mouse_speed'
def menu_entry(self) -> str:
return 'Reduced &Mouse Speed'
def get(self):
return get_sys_value(112)
def set(self, value):
set_sys_value(113, value)

class ValueInterfaceHScroll(ValueInterface):
def name(self):
return 'hscroll_speed'
def menu_entry(self) -> str:
return 'Reduced &H-scroll Speed'
def get(self):
return get_sys_value(0x6C)
def set(self, value):
set_sys_value(0x6D, value)

class ValueInterfaceVScroll(ValueInterface):
def name(self):
return 'vscroll_speed'
def menu_entry(self) -> str:
return 'Reduced &V-scroll Speed'
def get(self):
return get_sys_value(0x68)
def set(self, value):
set_sys_value(0x69, value)

class ValueInterfaceTouchpadSpeed(ValueInterface):
def name(self):
return 'touchpad_speed'
def menu_entry(self) -> str:
return 'Reduced &Touchpad Speed'
def is_available(self):
return super().is_available() and app_env.has_touchpad_speed_support()
def get(self):
params = TOUCHPAD_PARAMETERS()
params.versionNumber = 1
ctypes.windll.user32.SystemParametersInfoA(0xAE, ctypes.sizeof(params), ctypes.byref(params), 0)
return params.cursorSpeed
def set(self, value):
params = TOUCHPAD_PARAMETERS()
params.versionNumber = 1
ctypes.windll.user32.SystemParametersInfoA(0xAE, ctypes.sizeof(params), ctypes.byref(params), 0)
params.cursorSpeed = value
ctypes.windll.user32.SystemParametersInfoA(0xAF, ctypes.sizeof(params), ctypes.byref(params), 0)
2 changes: 1 addition & 1 deletion build.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@echo off
pyinstaller -F -w -i icon/main.ico slow-mouse.py --add-data "./icon/main.png;./icon"
pyinstaller -F -w -i icon/main.ico main.py --add-data "./icon/main.png;./icon" -n slow-mouse
Loading

0 comments on commit 261b424

Please sign in to comment.