|
51 | 51 |
|
52 | 52 | DBT_DEVNODES_CHANGED = 0x0007 |
53 | 53 |
|
| 54 | +EVENT_SYSTEM_FOREGROUND = 0x0003 |
| 55 | + |
| 56 | +EVENT_OBJECT_LOCATIONCHANGE = 0x800B |
| 57 | + |
| 58 | +WINEVENT_OUTOFCONTEXT = 0x0000 |
| 59 | + |
| 60 | +OBJID_WINDOW = 0x00000000 |
| 61 | + |
54 | 62 | BOOL = ctypes.wintypes.BOOL |
55 | 63 |
|
56 | 64 | DWORD = ctypes.wintypes.DWORD |
57 | 65 |
|
| 66 | +LONG = ctypes.wintypes.LONG |
| 67 | + |
58 | 68 | HDC = ctypes.wintypes.HDC |
59 | 69 |
|
60 | 70 | HMONITOR = ctypes.wintypes.HMONITOR |
61 | 71 |
|
62 | 72 | HWND = ctypes.wintypes.HWND |
63 | 73 |
|
| 74 | +HANDLE = ctypes.wintypes.HANDLE |
| 75 | + |
64 | 76 | UINT = ctypes.wintypes.UINT |
65 | 77 |
|
66 | 78 | LPARAM = ctypes.wintypes.LPARAM |
|
83 | 95 |
|
84 | 96 | WNDPROCTYPE = ctypes.WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) |
85 | 97 |
|
| 98 | +WINEVENTPROC = ctypes.WINFUNCTYPE(None, HANDLE, DWORD, HWND, LONG, LONG, DWORD, DWORD) |
| 99 | + |
86 | 100 | HCURSOR = ctypes.wintypes.HANDLE |
87 | 101 |
|
88 | | -HICON = ctypes.wintypes.HANDLE |
| 102 | +HICON = ctypes.wintypes.HICON |
89 | 103 |
|
90 | | -HBRUSH = ctypes.wintypes.HANDLE |
| 104 | +HBRUSH = ctypes.wintypes.HBRUSH |
91 | 105 |
|
92 | 106 | DPI_AWARENESS_CONTEXT_UNAWARE = ctypes.wintypes.HANDLE(-1) |
93 | 107 | DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = ctypes.wintypes.HANDLE(-2) |
|
134 | 148 | user32.EnumDisplayMonitors.restype = BOOL |
135 | 149 |
|
136 | 150 | 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] |
143 | 152 |
|
144 | 153 | user32.SetThreadDpiAwarenessContext.argtypes = [ctypes.wintypes.HANDLE] |
145 | 154 | user32.SetThreadDpiAwarenessContext.restype = ctypes.wintypes.HANDLE |
@@ -456,29 +465,29 @@ def size(self) -> tuple[int, int]: |
456 | 465 | return self._pid.size |
457 | 466 |
|
458 | 467 |
|
459 | | -class EventListener(base.EventListener): |
460 | | - """Base Windows event listener. |
| 468 | +class _WindowMessageListener(base.EventListener): |
| 469 | + """Listen for Windows messages. |
461 | 470 |
|
462 | | - Override the `check` method to implement this. |
| 471 | + Override the `check` method to implement triggers. |
463 | 472 | """ |
464 | 473 |
|
465 | 474 | def __init__(self) -> None: |
466 | 475 | super().__init__() |
467 | | - self._hwnd = None # type: int | None |
| 476 | + self._hwnd: int | None = None |
468 | 477 |
|
469 | 478 | def check(self, hwnd: int, msg: int, wparam: int, lparam: int) -> bool: |
470 | 479 | """Determine if a specific event has been fired.""" |
471 | 480 | return False |
472 | 481 |
|
| 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 | + |
473 | 487 | def run(self) -> None: |
474 | 488 | """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 | | - |
480 | 489 | hinst = kernel32.GetModuleHandleW(None) |
481 | | - wndproc_c = WNDPROCTYPE(wndproc) |
| 490 | + wndproc_c = WNDPROCTYPE(self._win_proc) |
482 | 491 |
|
483 | 492 | class_name = type(self).__name__ |
484 | 493 | wc = WNDCLASS() |
@@ -510,20 +519,93 @@ def stop(self) -> None: |
510 | 519 | user32.PostMessageW(self._hwnd, WM_QUIT, 0, 0) |
511 | 520 |
|
512 | 521 |
|
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): |
514 | 573 | """Listen for monitor change events.""" |
515 | 574 |
|
516 | 575 | def check(self, hwnd: int, msg: int, wparam: int, lparam: int) -> bool: |
517 | 576 | return msg == WM_DISPLAYCHANGE |
518 | 577 |
|
519 | 578 |
|
520 | | -class ControllerEventListener(EventListener): |
| 579 | +class ControllerEventListener(_WindowMessageListener): |
521 | 580 | """Listen for controller change events.""" |
522 | 581 |
|
523 | 582 | def check(self, hwnd: int, msg: int, wparam: int, lparam: int) -> bool: |
524 | 583 | return msg == WM_DEVICECHANGE and wparam == DBT_DEVNODES_CHANGED |
525 | 584 |
|
526 | 585 |
|
| 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 | + |
527 | 609 | def prepare_application_icon(icon_path: Path | str) -> None: |
528 | 610 | """Register app so that setting an icon is possible.""" |
529 | 611 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(PACKAGE_IDENTIFIER) |
|
0 commit comments