# Required for the window title name from ctypes import wintypes, windll, create_unicode_buffer, WINFUNCTYPE from typing import Optional, Dict 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 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() if title == "": return foreground_hwnd = windll.user32.GetForegroundWindow() if event != win32con.EVENT_OBJECT_FOCUS and foreground_hwnd != hwnd: return for pattern, code in self.mapping.items(): if pattern == "default": continue if pattern in title: 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 if hwnd == foreground_hwnd: # We are not allowed to display message into the handler, because # the GIL is released in the callback. We can’t modifiy elements # from other threads, as it’s create weird errors : # # Fatal Python error: PyEval_RestoreThread: the function must be # called with the GIL held, but the GIL is released (the current # Python thread state is NULL) print("Mapping '%s' to default" % title) 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)