from ctypes import wintypes, windll, create_unicode_buffer, WINFUNCTYPE from typing import Optional, Dict import sys from zope import interface from interfaces import desktopEvent import win32api import win32gui import win32con from zope import component from interfaces.message import IMessage, Debug from consumer import Mapping # This code tries to hook the Focus changes events with the callback method. # The types of events we want to listen for, and the names we'll use for # them in the log output. Pick from # http://msdn.microsoft.com/en-us/library/windows/desktop/dd318066(v=vs.85).aspx WINEVENT_OUTOFCONTEXT = 0x0000 eventTypes = { #win32con.EVENT_SYSTEM_FOREGROUND: "Foreground", win32con.EVENT_OBJECT_FOCUS: "Focus", win32con.EVENT_OBJECT_NAMECHANGE: "NameChange", win32con.EVENT_OBJECT_SHOW: "Show", #win32con.EVENT_SYSTEM_DIALOGSTART: "Dialog", #win32con.EVENT_SYSTEM_CAPTURESTART: "Capture", #win32con.EVENT_SYSTEM_MINIMIZEEND: "UnMinimize" } WinEventProcType = WINFUNCTYPE( None, wintypes.HANDLE, wintypes.DWORD, wintypes.HWND, wintypes.LONG, wintypes.LONG, wintypes.DWORD, wintypes.DWORD ) def setHook(WinEventProc, eventType): """ Register the hook foo being notified when the window change """ return windll.user32.SetWinEventHook( eventType, eventType, 0, WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT ) @interface.implementer(desktopEvent.IDesktop) class Listener(object): def __init__(self, mapping: Dict[str, str]): self.WinEventProc = WinEventProcType(self.callback) self.mapping = mapping def getForegroundWindowTitle(self) -> Optional[str]: """ Get the window title name. Example found from https://stackoverflow.com/a/58355052 See the function in the winuser librarry : https://learn.microsoft.com/en-us/windows/win32/api/winuser/ """ hWnd = windll.user32.GetForegroundWindow() length = windll.user32.GetWindowTextLengthW(hWnd) buf = create_unicode_buffer(length + 1) windll.user32.GetWindowTextW(hWnd, buf, length + 1) # 1-liner alternative: return buf.value if buf.value else None if buf.value: return buf.value else: return None def callback(self, hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) -> None: if hwnd is None: return match_event = event in (win32con.EVENT_OBJECT_FOCUS, win32con.EVENT_OBJECT_NAMECHANGE) if not match_event: return # Loop until we found the main window parent = windll.user32.GetParent(hwnd) while parent != 0: hwnd = parent parent = windll.user32.GetParent(hwnd) is_foreground = (hwnd == windll.user32.GetForegroundWindow()) if not is_foreground: return length = windll.user32.GetWindowTextLengthW(hwnd) title = create_unicode_buffer(length + 1) windll.user32.GetWindowTextW(hwnd, title, length + 1) if title.value is None: return title = str(title.value).lower().strip() if title == "": return for pattern, code in self.mapping.items(): if pattern == "default": continue if pattern in title: for key in code.keys(): print(f"Mapping '{title}' to {key}") component.handle(Mapping((code, None))) return # Get the default mapping to apply if there is any defined # This only applies when the window raising the event is the main # window print(f"Mapping '{title}' to default") component.handle(Mapping(("default", None))) def start(self) -> None: self.hookIDs = [setHook(self.WinEventProc, et) for et in eventTypes.keys()] if not any(self.hookIDs): print('SetWinEventHook failed') sys.exit(1) def stop(self) -> None: for hook in self.hookIDs: windll.user32.UnhookWinEvent(hook)