From 17a7af959a29aa0c6a02a88aa8cf3c150e8ac8fb Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 10 Jun 2024 23:00:44 -0400 Subject: [PATCH] [#1805] pywin32 Handoff --- config/windows.py | 29 ++++------------ hotkey/windows.py | 83 +++++++++++++++++++++++--------------------- monitor.py | 18 +++------- prefs.py | 10 ++---- requirements-dev.txt | 2 -- requirements.txt | 2 ++ stats.py | 9 ++--- theme.py | 11 +++--- 8 files changed, 68 insertions(+), 96 deletions(-) diff --git a/config/windows.py b/config/windows.py index 75e3f6977..9f3304834 100644 --- a/config/windows.py +++ b/config/windows.py @@ -7,41 +7,23 @@ """ from __future__ import annotations -import ctypes import functools import pathlib import sys import uuid import winreg -from ctypes.wintypes import DWORD, HANDLE from typing import Literal from config import AbstractConfig, applongname, appname, logger +from win32comext.shell import shell assert sys.platform == 'win32' REG_RESERVED_ALWAYS_ZERO = 0 -# This is the only way to do this from python without external deps (which do this anyway). -FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}') -FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}') -FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}') -FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}') - -SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath -SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)] - -CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree -CoTaskMemFree.argtypes = [ctypes.c_void_p] - def known_folder_path(guid: uuid.UUID) -> str | None: """Look up a Windows GUID to actual folder path name.""" - buf = ctypes.c_wchar_p() - if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)): - return None - retval = buf.value # copy data - CoTaskMemFree(buf) # and free original - return retval + return shell.SHGetKnownFolderPath(guid, 0, 0) class WinConfig(AbstractConfig): @@ -49,7 +31,8 @@ class WinConfig(AbstractConfig): def __init__(self) -> None: super().__init__() - self.app_dir_path = pathlib.Path(known_folder_path(FOLDERID_LocalAppData)) / appname # type: ignore + if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData): + self.app_dir_path = pathlib.Path(local_appdata) / appname self.app_dir_path.mkdir(exist_ok=True) self.plugin_dir_path = self.app_dir_path / 'plugins' @@ -65,7 +48,7 @@ def __init__(self) -> None: self.home_path = pathlib.Path.home() journal_dir_path = pathlib.Path( - known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore + known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806 @@ -84,7 +67,7 @@ def __init__(self) -> None: self.identifier = applongname if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir(): - docs = known_folder_path(FOLDERID_Documents) + docs = known_folder_path(shell.FOLDERID_Documents) self.set("outdir", docs if docs is not None else self.home) def __get_regentry(self, key: str) -> None | list | str | int: diff --git a/hotkey/windows.py b/hotkey/windows.py index fa4cb3a65..cbcbd161b 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -8,7 +8,10 @@ import threading import tkinter as tk import winsound -from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD +from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD +import pywintypes +import win32api +import win32gui from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr @@ -17,26 +20,20 @@ logger = get_main_logger() -RegisterHotKey = ctypes.windll.user32.RegisterHotKey -UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey +UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon + MOD_ALT = 0x0001 MOD_CONTROL = 0x0002 MOD_SHIFT = 0x0004 MOD_WIN = 0x0008 MOD_NOREPEAT = 0x4000 -GetMessage = ctypes.windll.user32.GetMessageW -TranslateMessage = ctypes.windll.user32.TranslateMessage -DispatchMessage = ctypes.windll.user32.DispatchMessageW -PostThreadMessage = ctypes.windll.user32.PostThreadMessageW WM_QUIT = 0x0012 WM_HOTKEY = 0x0312 WM_APP = 0x8000 WM_SND_GOOD = WM_APP + 1 WM_SND_BAD = WM_APP + 2 -GetKeyState = ctypes.windll.user32.GetKeyState -MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW VK_BACK = 0x08 VK_CLEAR = 0x0c VK_RETURN = 0x0d @@ -60,10 +57,13 @@ VK_PROCESSKEY = 0xe5 VK_OEM_CLEAR = 0xfe -GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow -GetWindowText = ctypes.windll.user32.GetWindowTextW -GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] -GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW +# VirtualKey mapping values +# +MAPVK_VK_TO_VSC = 0 +MAPVK_VSC_TO_VK = 1 +MAPVK_VK_TO_CHAR = 2 +MAPVK_VSC_TO_VK_EX = 3 +MAPVK_VK_TO_VSC_EX = 4 def window_title(h) -> str: @@ -74,9 +74,9 @@ def window_title(h) -> str: :return: Window title. """ if h: - title_length = GetWindowTextLength(h) + 1 + title_length = win32gui.GetWindowTextLength(h) + 1 buf = ctypes.create_unicode_buffer(title_length) - if GetWindowText(h, buf, title_length): + if win32gui.GetWindowText(h, buf, title_length): return buf.value return '' @@ -197,7 +197,7 @@ def unregister(self) -> None: logger.debug('Thread is/was running') self.thread = None # type: ignore logger.debug('Telling thread WM_QUIT') - PostThreadMessage(thread.ident, WM_QUIT, 0, 0) + win32gui.PostThreadMessage(thread.ident, WM_QUIT, 0, 0) logger.debug('Joining thread') thread.join() # Wait for it to unregister hotkey and quit @@ -210,8 +210,10 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 """Handle hotkeys.""" logger.debug('Begin...') # Hotkey must be registered by the thread that handles it - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We're not the right thread?") + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("We're not the right thread?") self.thread = None # type: ignore return @@ -219,14 +221,14 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 msg = MSG() logger.debug('Entering GetMessage() loop...') - while GetMessage(ctypes.byref(msg), None, 0, 0) != 0: + while win32gui.GetMessage(ctypes.byref(msg), None, 0, 0) != 0: logger.debug('Got message') if msg.message == WM_HOTKEY: logger.debug('WM_HOTKEY') if ( - config.get_int('hotkey_always') - or window_title(GetForegroundWindow()).startswith('Elite - Dangerous') + config.get_int('hotkey_always') + or window_title(win32gui.GetForegroundWindow()).startswith('Elite - Dangerous') ): if not config.shutting_down: logger.debug('Sending event <>') @@ -236,8 +238,10 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 logger.debug('Passing key on') UnregisterHotKey(None, 1) SendInput(1, fake, ctypes.sizeof(INPUT)) - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We aren't registered for this ?") + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("We aren't registered for this ?") break elif msg.message == WM_SND_GOOD: @@ -250,8 +254,8 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 else: logger.debug('Something else') - TranslateMessage(ctypes.byref(msg)) - DispatchMessage(ctypes.byref(msg)) + win32gui.TranslateMessage(ctypes.byref(msg)) + win32gui.DispatchMessage(ctypes.byref(msg)) logger.debug('Exited GetMessage() loop.') UnregisterHotKey(None, 1) @@ -266,7 +270,7 @@ def acquire_stop(self) -> None: """Stop acquiring hotkey state.""" pass - def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 + def fromevent(self, event) -> bool | tuple | None: """ Return configuration (keycode, modifiers) or None=clear or False=retain previous. @@ -277,11 +281,11 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 :param event: tk event ? :return: False to retain previous, None to not use, else (keycode, modifiers) """ - modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ - | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ - | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ - | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ - | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + modifiers = ((win32api.GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ + | ((win32api.GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ + | ((win32api.GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ + | ((win32api.GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ + | ((win32api.GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) keycode = event.keycode if keycode in (VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN): @@ -295,7 +299,7 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 return None if ( - keycode in (VK_RETURN, VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') + keycode in (VK_RETURN, VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') ): # don't allow keys needed for typing in System Map winsound.MessageBeep() return None @@ -305,12 +309,13 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 return 0, modifiers # See if the keycode is usable and available - if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode): + try: + win32gui.RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode) UnregisterHotKey(None, 2) return keycode, modifiers - - winsound.MessageBeep() - return None + except pywintypes.error: + winsound.MessageBeep() + return None def display(self, keycode, modifiers) -> str: """ @@ -346,7 +351,7 @@ def display(self, keycode, modifiers) -> str: text += WindowsHotkeyMgr.DISPLAY[keycode] else: - c = MapVirtualKey(keycode, 2) # printable ? + c = win32api.MapVirtualKey(keycode, MAPVK_VK_TO_CHAR) if not c: # oops not printable text += '⁈' @@ -361,9 +366,9 @@ def display(self, keycode, modifiers) -> str: def play_good(self) -> None: """Play the 'good' sound.""" if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) + win32gui.PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) def play_bad(self) -> None: """Play the 'bad' sound.""" if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) + win32gui.PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) diff --git a/monitor.py b/monitor.py index 9d33f5eb5..ba1a4d39e 100644 --- a/monitor.py +++ b/monitor.py @@ -36,23 +36,15 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR + from ctypes.wintypes import BOOL, HWND, LPARAM + import win32gui from watchdog.events import FileSystemEventHandler, FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver - EnumWindows = ctypes.windll.user32.EnumWindows EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - CloseHandle = ctypes.windll.kernel32.CloseHandle - - GetWindowText = ctypes.windll.user32.GetWindowTextW - GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] - GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW - GetWindowTextLength.argtypes = [ctypes.wintypes.HWND] - GetWindowTextLength.restype = ctypes.c_int - GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd else: @@ -2131,9 +2123,9 @@ def game_running(self) -> bool: # noqa: CCR001 if sys.platform == 'win32': def WindowTitle(h): # noqa: N802 if h: - length = GetWindowTextLength(h) + 1 + length = win32gui.GetWindowTextLength(h) + 1 buf = ctypes.create_unicode_buffer(length) - if GetWindowText(h, buf, length): + if win32gui.GetWindowText(h, buf, length): return buf.value return None @@ -2147,7 +2139,7 @@ def callback(hWnd, lParam): # noqa: N803 return True - return not EnumWindows(EnumWindowsProc(callback), 0) + return not win32gui.EnumWindows(EnumWindowsProc(callback), 0) return False diff --git a/prefs.py b/prefs.py index 9f2062f40..e7ccef18f 100644 --- a/prefs.py +++ b/prefs.py @@ -188,7 +188,8 @@ def __exit__( if sys.platform == 'win32': import ctypes import winreg - from ctypes.wintypes import HINSTANCE, HWND, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT + from ctypes.wintypes import HINSTANCE, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT + import win32gui is_wine = False try: WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine' @@ -219,11 +220,6 @@ def __exit__( ctypes.POINTER(RECT) ] - GetParent = ctypes.windll.user32.GetParent - GetParent.argtypes = [HWND] - GetWindowRect = ctypes.windll.user32.GetWindowRect - GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)] - SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)] @@ -314,7 +310,7 @@ def __init__(self, parent: tk.Tk, callback: Optional[Callable]): # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - GetWindowRect(GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()), position) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), SIZE(position.right - position.left, position.bottom - position.top), # type: ignore diff --git a/requirements-dev.txt b/requirements-dev.txt index 7c8e1ef5f..1e30ff465 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -42,8 +42,6 @@ pytest==8.2.0 pytest-cov==5.0.0 # Pytest code coverage support coverage[toml]==7.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.9.0 -# For manipulating folder permissions and the like. -pywin32==306; sys_platform == 'win32' # All of the normal requirements diff --git a/requirements.txt b/requirements.txt index 22f0a360f..b278cce5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ pillow==10.3.0 watchdog==4.0.0 simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 +# For manipulating folder permissions and the like. +pywin32==306; sys_platform == 'win32' \ No newline at end of file diff --git a/stats.py b/stats.py index db61f7892..965b7fc89 100644 --- a/stats.py +++ b/stats.py @@ -25,17 +25,14 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT + from ctypes.wintypes import POINT, RECT, SIZE, UINT + import win32gui try: CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition CalculatePopupWindowPosition.argtypes = [ ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) ] - GetParent = ctypes.windll.user32.GetParent - GetParent.argtypes = [HWND] - GetWindowRect = ctypes.windll.user32.GetWindowRect - GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)] except Exception: # Not supported under Wine 4.0 CalculatePopupWindowPosition = None # type: ignore @@ -423,7 +420,7 @@ def __init__(self, parent: tk.Tk, data: dict[str, Any]) -> None: # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - GetWindowRect(GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()), position) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), # - is evidently supported on the C side diff --git a/theme.py b/theme.py index 94e99f7a6..dc48499e8 100644 --- a/theme.py +++ b/theme.py @@ -34,6 +34,7 @@ if sys.platform == 'win32': import ctypes from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR + import win32gui AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore FR_PRIVATE = 0x10 @@ -427,8 +428,6 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 GWL_EXSTYLE = -20 # noqa: N806 # ctypes WS_EX_APPWINDOW = 0x00040000 # noqa: N806 # ctypes WS_EX_LAYERED = 0x00080000 # noqa: N806 # ctypes - GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes - SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes # FIXME: Lose the "treat this like a boolean" bullshit if theme == self.THEME_DEFAULT: @@ -445,14 +444,14 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here - hwnd = ctypes.windll.user32.GetParent(root.winfo_id()) - SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize + hwnd = win32gui.GetParent(root.winfo_id()) + win32gui.SetWindowLong(hwnd, GWL_STYLE, win32gui.GetWindowLong(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize if theme == self.THEME_TRANSPARENT: - SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar + win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar else: - SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar + win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar root.deiconify() root.wait_visibility() # need main window to be displayed before returning