Skip to content

Commit 461f953

Browse files
committed
Add listener on foreground window change/resize
1 parent ff53376 commit 461f953

File tree

5 files changed

+118
-26
lines changed

5 files changed

+118
-26
lines changed

mousetracks2/components/tracking.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from ..utils.monitor import MonitorData
2424
from ..utils.input import get_cursor_pos
2525
from ..utils.interface import Interfaces
26-
from ..utils.system import MonitorEventListener, ControllerEventListener, hide_child_process
26+
from ..utils.system import MonitorEventListener, ControllerEventListener, ForegroundAppListener, hide_child_process
2727

2828

2929
if XInput is None:
@@ -136,6 +136,9 @@ def __post_init__(self) -> None:
136136
self._controller_listener = ControllerEventListener()
137137
self._controller_listener.start()
138138

139+
self._application_listener = ForegroundAppListener()
140+
self._application_listener.start()
141+
139142
def _receive_data(self) -> None:
140143
for message in self.receive_data():
141144
match message:
@@ -198,6 +201,12 @@ def _receive_data(self) -> None:
198201
print(f'[Tracking] Disabled check for running applications: {message.disable}')
199202
self.update_apps = not message.disable
200203

204+
if message.disable:
205+
self._application_listener.stop()
206+
else:
207+
self._application_listener = ForegroundAppListener()
208+
self._application_listener.start()
209+
201210
case ipc.DebugDisableMonitorCheck():
202211
print(f'[Tracking] Disabled check for monitor changes: {message.disable}')
203212
self.update_monitors = not message.disable
@@ -412,7 +421,7 @@ def run(self) -> None:
412421
self.send_data(ipc.Tick(tick, int(time.time())))
413422

414423
# Check for loaded applications
415-
if self.update_apps and tick and not tick % int(UPDATES_PER_SECOND * GlobalConfig.application_check_frequency):
424+
if self.update_apps and self._application_listener.triggered:
416425
self.send_data(ipc.RequestRunningAppCheck())
417426

418427
# Update monitor data

mousetracks2/config.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class GlobalConfig:
2626
save_frequency: How often to autosave.
2727
max_loaded_profiles: Maximum amount of loaded profiles.
2828
This will only affect profiles without unsaved changes.
29-
application_check_frequency: How often to check the current focused application.
3029
component_check_frequency: How often to check all components are running.
3130
This is used once per message received.
3231
shutdown_timeout: How long to wait before shutting down automatically.
@@ -45,7 +44,6 @@ class GlobalConfig:
4544
inactivity_time: float = 300.0
4645
save_frequency: float = 600.0
4746
max_loaded_profiles: int = 8
48-
application_check_frequency: float = 1.0
4947
component_check_frequency: float = 1.0
5048
shutdown_timeout: float = 15.0
5149
export_notification_timeout: float = 7.0

mousetracks2/utils/system/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from .windows import get_autostart, set_autostart, remove_autostart
1919
from .windows import is_elevated, relaunch_as_elevated
2020
from .windows import Window
21-
from .windows import MonitorEventListener, ControllerEventListener
2221
from .base import hide_child_process
2322
from .windows import prepare_application_icon
2423
from .windows import update_installer_version_number

mousetracks2/utils/system/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class EventListener(threading.Thread):
9898
"""
9999

100100
def __init__(self) -> None:
101-
super().__init__(name='EventListener', daemon=True)
101+
super().__init__(name=type(self).__name__, daemon=True)
102102
self._queue = queue.Queue() # type: queue.Queue[None]
103103
self._running = True
104104

@@ -135,6 +135,10 @@ class ControllerEventListener(EventListener):
135135
"""Listen for controller change events."""
136136

137137

138+
class ForegroundAppListener(EventListener):
139+
"""Listen for application change events."""
140+
141+
138142
def hide_child_process() -> None:
139143
"""This is here to allow macOS to hide the child processes."""
140144

mousetracks2/utils/system/windows.py

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,28 @@
5151

5252
DBT_DEVNODES_CHANGED = 0x0007
5353

54+
EVENT_SYSTEM_FOREGROUND = 0x0003
55+
56+
EVENT_OBJECT_LOCATIONCHANGE = 0x800B
57+
58+
WINEVENT_OUTOFCONTEXT = 0x0000
59+
60+
OBJID_WINDOW = 0x00000000
61+
5462
BOOL = ctypes.wintypes.BOOL
5563

5664
DWORD = ctypes.wintypes.DWORD
5765

66+
LONG = ctypes.wintypes.LONG
67+
5868
HDC = ctypes.wintypes.HDC
5969

6070
HMONITOR = ctypes.wintypes.HMONITOR
6171

6272
HWND = ctypes.wintypes.HWND
6373

74+
HANDLE = ctypes.wintypes.HANDLE
75+
6476
UINT = ctypes.wintypes.UINT
6577

6678
LPARAM = ctypes.wintypes.LPARAM
@@ -83,11 +95,13 @@
8395

8496
WNDPROCTYPE = ctypes.WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM)
8597

98+
WINEVENTPROC = ctypes.WINFUNCTYPE(None, HANDLE, DWORD, HWND, LONG, LONG, DWORD, DWORD)
99+
86100
HCURSOR = ctypes.wintypes.HANDLE
87101

88-
HICON = ctypes.wintypes.HANDLE
102+
HICON = ctypes.wintypes.HICON
89103

90-
HBRUSH = ctypes.wintypes.HANDLE
104+
HBRUSH = ctypes.wintypes.HBRUSH
91105

92106
DPI_AWARENESS_CONTEXT_UNAWARE = ctypes.wintypes.HANDLE(-1)
93107
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = ctypes.wintypes.HANDLE(-2)
@@ -134,12 +148,7 @@
134148
user32.EnumDisplayMonitors.restype = BOOL
135149

136150
user32.DefWindowProcW.restype = LRESULT
137-
user32.DefWindowProcW.argtypes = [
138-
HWND,
139-
UINT,
140-
WPARAM,
141-
LPARAM,
142-
]
151+
user32.DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM]
143152

144153
user32.SetThreadDpiAwarenessContext.argtypes = [ctypes.wintypes.HANDLE]
145154
user32.SetThreadDpiAwarenessContext.restype = ctypes.wintypes.HANDLE
@@ -456,29 +465,29 @@ def size(self) -> tuple[int, int]:
456465
return self._pid.size
457466

458467

459-
class EventListener(base.EventListener):
460-
"""Base Windows event listener.
468+
class _WindowMessageListener(base.EventListener):
469+
"""Listen for Windows messages.
461470
462-
Override the `check` method to implement this.
471+
Override the `check` method to implement triggers.
463472
"""
464473

465474
def __init__(self) -> None:
466475
super().__init__()
467-
self._hwnd = None # type: int | None
476+
self._hwnd: int | None = None
468477

469478
def check(self, hwnd: int, msg: int, wparam: int, lparam: int) -> bool:
470479
"""Determine if a specific event has been fired."""
471480
return False
472481

482+
def _win_proc(self, hwnd: int, msg: int, wparam: int, lparam: int) -> int:
483+
if self.check(hwnd, msg, wparam, lparam):
484+
self.trigger()
485+
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
486+
473487
def run(self) -> None:
474488
"""Create and start the message listener."""
475-
def wndproc(hwnd: int, msg: int, wparam: int, lparam: int) -> int:
476-
if self.check(hwnd, msg, wparam, lparam):
477-
self.trigger()
478-
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
479-
480489
hinst = kernel32.GetModuleHandleW(None)
481-
wndproc_c = WNDPROCTYPE(wndproc)
490+
wndproc_c = WNDPROCTYPE(self._win_proc)
482491

483492
class_name = type(self).__name__
484493
wc = WNDCLASS()
@@ -510,20 +519,93 @@ def stop(self) -> None:
510519
user32.PostMessageW(self._hwnd, WM_QUIT, 0, 0)
511520

512521

513-
class MonitorEventListener(EventListener):
522+
class _WinEventHookListener(base.EventListener):
523+
"""Listen for Windows event hooks.
524+
525+
Override the `check` method to implement triggers.
526+
"""
527+
528+
EVENTS: tuple[int, ...] = ()
529+
530+
def __init__(self) -> None:
531+
super().__init__()
532+
self._hooks: list[int] = []
533+
534+
def check(self, hWinEventHook: int, event: int, hwnd: int, idObject: int,
535+
idChild: int, dwEventThread: int, dwmsEventTime: int) -> bool:
536+
"""Determine if a specific event has been fired."""
537+
return False
538+
539+
def _win_event_callback(self, hWinEventHook: int, event: int, hwnd: int, idObject: int,
540+
idChild: int, dwEventThread: int, dwmsEventTime: int) -> None:
541+
if self.check(hWinEventHook, event, hwnd, idObject,
542+
idChild, dwEventThread, dwmsEventTime):
543+
self.trigger()
544+
545+
def run(self) -> None:
546+
hook_proc = WINEVENTPROC(self._win_event_callback)
547+
548+
for event_id in self.EVENTS:
549+
hook = user32.SetWinEventHook(
550+
event_id, event_id,
551+
None, hook_proc, 0, 0, WINEVENT_OUTOFCONTEXT
552+
)
553+
if not hook:
554+
raise ctypes.WinError(ctypes.get_last_error())
555+
self._hooks.append(hook)
556+
557+
self.trigger()
558+
559+
msg = MSG()
560+
while user32.GetMessageW(ctypes.byref(msg), None, 0, 0):
561+
user32.TranslateMessage(ctypes.byref(msg))
562+
user32.DispatchMessageW(ctypes.byref(msg))
563+
564+
def stop(self) -> None:
565+
"""Stops the message loop and cleans up the thread."""
566+
for hook in self._hooks:
567+
user32.UnhookWinEvent(hook)
568+
self._hooks.clear()
569+
user32.PostThreadMessageW(kernel32.GetCurrentThreadId(), WM_QUIT, 0, 0)
570+
571+
572+
class MonitorEventListener(_WindowMessageListener):
514573
"""Listen for monitor change events."""
515574

516575
def check(self, hwnd: int, msg: int, wparam: int, lparam: int) -> bool:
517576
return msg == WM_DISPLAYCHANGE
518577

519578

520-
class ControllerEventListener(EventListener):
579+
class ControllerEventListener(_WindowMessageListener):
521580
"""Listen for controller change events."""
522581

523582
def check(self, hwnd: int, msg: int, wparam: int, lparam: int) -> bool:
524583
return msg == WM_DEVICECHANGE and wparam == DBT_DEVNODES_CHANGED
525584

526585

586+
class ForegroundAppListener(_WinEventHookListener):
587+
"""Listen for when the foreground window changes.
588+
This can happen if the user changes, moves or resizes an app.
589+
"""
590+
591+
EVENTS = (
592+
EVENT_SYSTEM_FOREGROUND,
593+
EVENT_OBJECT_LOCATIONCHANGE,
594+
)
595+
596+
def __init__(self) -> None:
597+
self._is_moving = False
598+
super().__init__()
599+
600+
def check(self, hWinEventHook: int, event: int, hwnd: int, idObject: int,
601+
idChild: int, dwEventThread: int, dwmsEventTime: int) -> bool:
602+
if not hwnd or event not in self.EVENTS:
603+
return False
604+
if event == EVENT_OBJECT_LOCATIONCHANGE:
605+
return idObject == OBJID_WINDOW
606+
return True
607+
608+
527609
def prepare_application_icon(icon_path: Path | str) -> None:
528610
"""Register app so that setting an icon is possible."""
529611
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(PACKAGE_IDENTIFIER)

0 commit comments

Comments
 (0)