diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 72973b1a..1b45225a 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -268,25 +268,36 @@ if __name__ == '__main__': # noqa: C901 # If *this* instance hasn't locked, then another already has and we # now need to do the edmc:// checks for auth callback if locked != JournalLockResult.LOCKED: - from ctypes import windll, create_unicode_buffer, WINFUNCTYPE + from ctypes import WINFUNCTYPE from ctypes.wintypes import BOOL, HWND, LPARAM import win32gui import win32api import win32con + import win32process + import pythoncom - GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 - ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806 + def get_process_handle_from_hwnd(hwnd): + try: + # Get thread and process IDs + _, process_id = win32process.GetWindowThreadProcessId(hwnd) + # Get the process handle + return win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, process_id) + except Exception: + return None - COINIT_MULTITHREADED = 0 # noqa: N806,F841 - COINIT_APARTMENTTHREADED = 0x2 # noqa: N806 - COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806 - CoInitializeEx = windll.ole32.CoInitializeEx # noqa: N806 + def window_title(hwnd: int) -> str | None: + try: + return win32gui.GetWindowText(hwnd) if hwnd else None + except Exception: + return None - def window_title(h: int) -> str | None: - if h: - return win32gui.GetWindowText(h) - return None + def get_window_class(hwnd: int) -> str: + try: + return win32gui.GetClassName(hwnd) + except Exception: + return "" + # We still need WINFUNCTYPE for the callback as win32gui doesn't provide an alternative @WINFUNCTYPE(BOOL, HWND, LPARAM) def enumwindowsproc(window_handle, l_param): # noqa: CCR001 """ @@ -303,29 +314,22 @@ if __name__ == '__main__': # noqa: C901 :param l_param: The second parameter to the EnumWindows() call. :return: False if we found a match, else True to continue iteration """ - # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 - cls = create_unicode_buffer(257) - # This conditional is exploded to make debugging slightly easier - if win32gui.GetClassName(window_handle): - if cls.value == 'TkTopLevel': + if window_class := get_window_class(window_handle): + if window_class == 'TkTopLevel': if window_title(window_handle) == applongname: - if GetProcessHandleFromHwnd(window_handle): + if get_process_handle_from_hwnd(window_handle): # If GetProcessHandleFromHwnd succeeds then the app is already running as this user if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler_redirect): - CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) + pythoncom.CoInitializeEx(0) # Wait for it to be responsive to avoid ShellExecute recursing win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) - else: - ShowWindowAsync(window_handle, win32con.SW_RESTORE) + win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) win32gui.SetForegroundWindow(window_handle) - return False # Indicate window found, so stop iterating - # Indicate that EnumWindows() needs to continue iterating return True # Do not remove, else this function as a callback breaks - # This performs the edmc://auth check and forward # EnumWindows() will iterate through all open windows, calling # enumwindwsproc() on each. When an invocation returns False it diff --git a/common_utils.py b/common_utils.py index 7e3a361e..a6f78356 100644 --- a/common_utils.py +++ b/common_utils.py @@ -18,17 +18,8 @@ logger = get_main_logger() SERVER_RETRY = 5 # retry pause for Companion servers [s] if sys.platform == 'win32': - import ctypes - from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL - import win32gui - try: - CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition - CalculatePopupWindowPosition.argtypes = [ - ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) - ] - CalculatePopupWindowPosition.restype = BOOL - except Exception: # Not supported under Wine 4.0 - CalculatePopupWindowPosition = None # type: ignore + import win32api + import win32con def ensure_on_screen(self, parent: tk.Tk): @@ -38,16 +29,23 @@ def ensure_on_screen(self, parent: tk.Tk): :param self: The calling class instance of tk.TopLevel :param parent: The parent window """ - # Ensure fully on-screen - if sys.platform == 'win32' and CalculatePopupWindowPosition: - position = RECT() - win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) - if CalculatePopupWindowPosition( - POINT(parent.winfo_rootx(), parent.winfo_rooty()), - SIZE(position.right - position.left, position.bottom - position.top), # type: ignore - 0x10000, None, position - ): - self.geometry(f"+{position.left}+{position.top}") + if sys.platform != 'win32': + return + try: + # Get monitor info for the monitor containing the parent window + monitor = win32api.MonitorFromWindow(parent.winfo_id(), win32con.MONITOR_DEFAULTTONEAREST) + monitor_info = win32api.GetMonitorInfo(monitor) + work_area = monitor_info['Work'] # Gets the working area (excludes taskbar) + + # Calculate optimal position + x = max(work_area[0], min(parent.winfo_rootx(), work_area[2] - self.winfo_width())) + y = max(work_area[1], min(parent.winfo_rooty(), work_area[3] - self.winfo_height())) + + # Update window position + self.geometry(f"+{x}+{y}") + + except Exception as e: + logger.debug(f"Failed to ensure window is on screen: {e}") def log_locale(prefix: str) -> None: diff --git a/hotkey/windows.py b/hotkey/windows.py index ad4da9db..b292e7a7 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -2,17 +2,15 @@ from __future__ import annotations import atexit -import ctypes import pathlib import sys import threading import tkinter as tk import winsound -from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD, HWND, BOOL, UINT -import pywintypes import win32api -import win32gui import win32con +import win32gui +import pywintypes from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr @@ -22,22 +20,14 @@ if sys.platform != 'win32': logger = get_main_logger() -UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon -UnregisterHotKey.argtypes = [HWND, ctypes.c_int] -UnregisterHotKey.restype = BOOL - +# Constants MOD_NOREPEAT = 0x4000 WM_SND_GOOD = win32con.WM_APP + 1 WM_SND_BAD = win32con.WM_APP + 2 VK_OEM_MINUS = 0xbd # 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: @@ -52,69 +42,6 @@ def window_title(h) -> str: return '' -class MOUSEINPUT(ctypes.Structure): - """Mouse Input structure.""" - - _fields_ = [ - ('dx', LONG), - ('dy', LONG), - ('mouseData', DWORD), - ('dwFlags', DWORD), - ('time', DWORD), - ('dwExtraInfo', ctypes.POINTER(ULONG)) - ] - - -class KEYBDINPUT(ctypes.Structure): - """Keyboard Input structure.""" - - _fields_ = [ - ('wVk', WORD), - ('wScan', WORD), - ('dwFlags', DWORD), - ('time', DWORD), - ('dwExtraInfo', ctypes.POINTER(ULONG)) - ] - - -class HARDWAREINPUT(ctypes.Structure): - """Hardware Input structure.""" - - _fields_ = [ - ('uMsg', DWORD), - ('wParamL', WORD), - ('wParamH', WORD) - ] - - -class INPUTUNION(ctypes.Union): - """Input union.""" - - _fields_ = [ - ('mi', MOUSEINPUT), - ('ki', KEYBDINPUT), - ('hi', HARDWAREINPUT) - ] - - -class INPUT(ctypes.Structure): - """Input structure.""" - - _fields_ = [ - ('type', DWORD), - ('union', INPUTUNION) - ] - - -SendInput = ctypes.windll.user32.SendInput -SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] -SendInput.restype = UINT - -INPUT_MOUSE = 0 -INPUT_KEYBOARD = 1 -INPUT_HARDWARE = 2 - - class WindowsHotkeyMgr(AbstractHotkeyMgr): """Hot key management.""" @@ -163,7 +90,6 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): def unregister(self) -> None: """Unregister the hotkey handling.""" thread = self.thread - if thread: logger.debug('Thread is/was running') self.thread = None # type: ignore @@ -174,13 +100,11 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): else: logger.debug('No thread') - logger.debug('Done.') def worker(self, keycode, modifiers) -> None: # noqa: CCR001 """Handle hotkeys.""" logger.debug('Begin...') - # Hotkey must be registered by the thread that handles it try: win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) except pywintypes.error: @@ -188,48 +112,66 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): self.thread = None # type: ignore return - fake = INPUT(INPUT_KEYBOARD, INPUTUNION(ki=KEYBDINPUT(keycode, keycode, 0, 0, None))) - - msg = MSG() logger.debug('Entering GetMessage() loop...') - while win32gui.GetMessage(ctypes.byref(msg), None, 0, 0) != 0: - logger.debug('Got message') - if msg.message == win32con.WM_HOTKEY: - logger.debug('WM_HOTKEY') + while True: + try: + result = win32gui.GetMessage(None, 0, 0) + if result[0] == 0: # WM_QUIT + break - if ( - config.get_int('hotkey_always') - or window_title(win32gui.GetForegroundWindow()).startswith('Elite - Dangerous') - ): - if not config.shutting_down: - logger.debug('Sending event <>') - self.root.event_generate('<>', when="tail") + msg_hwnd, msg_id, wparam, lparam, msg_time, msg_point = result[1] + + if msg_id == win32con.WM_HOTKEY: + logger.debug('WM_HOTKEY') + if (config.get_int('hotkey_always') or + window_title(win32gui.GetForegroundWindow()).startswith('Elite - Dangerous')): + if not config.shutting_down: + logger.debug('Sending event <>') + self.root.event_generate('<>', when="tail") + else: + logger.debug('Passing key on') + win32gui.UnregisterHotKey(None, 1) + + # Simulate key press + win32api.keybd_event( + keycode, + win32api.MapVirtualKey(keycode, 0), + 0, + 0 + ) + # Simulate key release + win32api.keybd_event( + keycode, + win32api.MapVirtualKey(keycode, 0), + win32con.KEYEVENTF_KEYUP, + 0 + ) + + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("Failed to re-register hotkey") + break + + elif msg_id == WM_SND_GOOD: + logger.debug('WM_SND_GOOD') + winsound.PlaySound(self.snd_good, winsound.SND_MEMORY) + + elif msg_id == WM_SND_BAD: + logger.debug('WM_SND_BAD') + winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) else: - logger.debug('Passing key on') - UnregisterHotKey(None, 1) - SendInput(1, fake, ctypes.sizeof(INPUT)) - try: - win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) - except pywintypes.error: - logger.exception("We aren't registered for this ?") - break + # Use win32gui for message processing + win32gui.TranslateMessage(result[1]) + win32gui.DispatchMessage(result[1]) - elif msg.message == WM_SND_GOOD: - logger.debug('WM_SND_GOOD') - winsound.PlaySound(self.snd_good, winsound.SND_MEMORY) # synchronous - - elif msg.message == WM_SND_BAD: - logger.debug('WM_SND_BAD') - winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous - - else: - logger.debug('Something else') - win32gui.TranslateMessage(ctypes.byref(msg)) - win32gui.DispatchMessage(ctypes.byref(msg)) + except Exception as e: + logger.exception(f"Error in message loop: {e}") + break logger.debug('Exited GetMessage() loop.') - UnregisterHotKey(None, 1) + win32gui.UnregisterHotKey(None, 1) self.thread = None # type: ignore logger.debug('Done.') @@ -259,31 +201,30 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): | ((win32api.GetKeyState(win32con.VK_RWIN) & 0x8000) and win32con.MOD_WIN) keycode = event.keycode - if keycode in (win32con.VK_SHIFT, win32con.VK_CONTROL, win32con.VK_MENU, win32con.VK_LWIN, win32con.VK_RWIN): + if keycode in (win32con.VK_SHIFT, win32con.VK_CONTROL, win32con.VK_MENU, + win32con.VK_LWIN, win32con.VK_RWIN): return 0, modifiers if not modifiers: if keycode == win32con.VK_ESCAPE: # Esc = retain previous return False - if keycode in (win32con.VK_BACK, win32con.VK_DELETE, - win32con.VK_CLEAR, win32con.VK_OEM_CLEAR): # BkSp, Del, Clear = clear hotkey + if (keycode in (win32con.VK_BACK, win32con.VK_DELETE, win32con.VK_CLEAR, win32con.VK_OEM_CLEAR) or + keycode in (win32con.VK_RETURN, win32con.VK_SPACE, VK_OEM_MINUS) or + ord('A') <= keycode <= ord( + 'Z')): # BkSp, Del, Clear = clear hotkey, or don't allow keys needed for typing + if keycode not in (win32con.VK_BACK, win32con.VK_DELETE, win32con.VK_CLEAR, win32con.VK_OEM_CLEAR): + winsound.MessageBeep() # Only beep for typing keys, not for clear hotkey return None - if ( - keycode in (win32con.VK_RETURN, win32con.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 - - if (keycode in (win32con.VK_NUMLOCK, win32con.VK_SCROLL, win32con.VK_PROCESSKEY) - or win32con.VK_CAPITAL <= keycode <= win32con.VK_MODECHANGE): # ignore unmodified mode switch keys + if (keycode in (win32con.VK_NUMLOCK, win32con.VK_SCROLL, win32con.VK_PROCESSKEY) or + win32con.VK_CAPITAL <= keycode <= win32con.VK_MODECHANGE): # ignore unmodified mode switch keys return 0, modifiers # See if the keycode is usable and available try: win32gui.RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode) - UnregisterHotKey(None, 2) + win32gui.UnregisterHotKey(None, 2) return keycode, modifiers except pywintypes.error: winsound.MessageBeep() @@ -300,13 +241,10 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): text = '' if modifiers & win32con.MOD_WIN: text += '❖+' - if modifiers & win32con.MOD_CONTROL: text += 'Ctrl+' - if modifiers & win32con.MOD_ALT: text += 'Alt+' - if modifiers & win32con.MOD_SHIFT: text += '⇧+' @@ -315,21 +253,16 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): if not keycode: pass - elif win32con.VK_F1 <= keycode <= win32con.VK_F24: text += f'F{keycode + 1 - win32con.VK_F1}' - - elif keycode in WindowsHotkeyMgr.DISPLAY: # specials - text += WindowsHotkeyMgr.DISPLAY[keycode] - + elif keycode in self.DISPLAY: + text += self.DISPLAY[keycode] else: c = win32api.MapVirtualKey(keycode, MAPVK_VK_TO_CHAR) if not c: # oops not printable text += '⁈' - elif c < 0x20: # control keys text += chr(c + 0x40) - else: text += chr(c).upper() diff --git a/l10n.py b/l10n.py index afb6acaf..67b467be 100755 --- a/l10n.py +++ b/l10n.py @@ -18,7 +18,7 @@ import sys import warnings from contextlib import suppress from os import listdir, sep -from typing import TYPE_CHECKING, Iterable, TextIO, cast +from typing import Iterable, TextIO, cast import pathlib from config import config from EDMCLogging import get_main_logger @@ -37,24 +37,7 @@ LANGUAGE_ID = '!Language' LOCALISATION_DIR: pathlib.Path = pathlib.Path('L10n') if sys.platform == 'win32': - import ctypes - from ctypes.wintypes import BOOL, DWORD, LPCVOID, LPCWSTR, LPWSTR - if TYPE_CHECKING: - import ctypes.windll # type: ignore # Magic to make linters not complain that windll is special - - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd318124%28v=vs.85%29.aspx - MUI_LANGUAGE_ID = 4 - MUI_LANGUAGE_NAME = 8 - GetUserPreferredUILanguages = ctypes.windll.kernel32.GetUserPreferredUILanguages - GetUserPreferredUILanguages.argtypes = [ - DWORD, ctypes.POINTER(ctypes.c_ulong), LPCVOID, ctypes.POINTER(ctypes.c_ulong) - ] - GetUserPreferredUILanguages.restype = BOOL - - LOCALE_NAME_USER_DEFAULT = None - GetNumberFormatEx = ctypes.windll.kernel32.GetNumberFormatEx - GetNumberFormatEx.argtypes = [LPCWSTR, DWORD, LPCWSTR, LPCVOID, LPWSTR, ctypes.c_int] - GetNumberFormatEx.restype = ctypes.c_int + import win32api class Translations: @@ -182,14 +165,13 @@ class Translations: if lang: contents: dict[str, str] = self.contents(lang=lang, plugin_path=plugin_path) - if not contents or type(contents) is not dict: + if not contents or not isinstance(contents, dict): logger.debug(f'Failure loading translations for overridden language {lang!r}') return self.translate(x) - elif x not in contents.keys(): + if x not in contents: logger.debug(f'Missing translation: {x!r} for overridden language {lang!r}') return self.translate(x) - else: - return contents.get(x) or self.translate(x) + return contents.get(x) or self.translate(x) if plugin_name: if self.translations[None] and plugin_name not in self.translations: @@ -307,16 +289,6 @@ class _Locale: return None - def wszarray_to_list(self, array): - offset = 0 - while offset < len(array): - sz = ctypes.wstring_at(ctypes.addressof(array) + offset * 2) # type: ignore - if sz: - yield sz - offset += len(sz) + 1 - else: - break - def preferred_languages(self) -> Iterable[str]: """ Return a list of preferred language codes. @@ -333,27 +305,14 @@ class _Locale: if sys.platform != 'win32': # POSIX lang = locale.getlocale()[0] - languages = [lang.replace('_', '-')] if lang else [] - else: - num = ctypes.c_ulong() - size = ctypes.c_ulong(0) - languages = [] - if GetUserPreferredUILanguages( - MUI_LANGUAGE_NAME, ctypes.byref(num), None, ctypes.byref(size) - ) and size.value: - buf = ctypes.create_unicode_buffer(size.value) - - if GetUserPreferredUILanguages( - MUI_LANGUAGE_NAME, ctypes.byref(num), ctypes.byref(buf), ctypes.byref(size) - ): - languages = self.wszarray_to_list(buf) - + current_locale = win32api.GetUserDefaultLangID() + lang = locale.windows_locale[current_locale] # HACK: | 2021-12-11: OneSky calls "Chinese Simplified" "zh-Hans" # in the name of the file, but that will be zh-CN in terms of # locale. So map zh-CN -> zh-Hans + languages = [lang.replace('_', '-')] if lang else [] languages = ['zh-Hans' if lang == 'zh-CN' else lang for lang in languages] - return languages diff --git a/prefs.py b/prefs.py index 690e32b6..51525bd4 100644 --- a/prefs.py +++ b/prefs.py @@ -4,7 +4,7 @@ from __future__ import annotations import contextlib import logging -from os.path import expandvars, join, normpath +from os.path import join, normpath from pathlib import Path import subprocess import sys @@ -185,10 +185,10 @@ class AutoInc(contextlib.AbstractContextManager): if sys.platform == 'win32': - import ctypes import winreg - from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL import win32api + from win32com.shell import shell, shellcon + import pythoncom is_wine = False try: WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine' @@ -199,30 +199,51 @@ if sys.platform == 'win32': except OSError: # Assumed to be 'path not found', i.e. not-wine pass - CalculatePopupWindowPosition = None - if not is_wine: - try: - CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition - CalculatePopupWindowPosition.argtypes = [POINT, SIZE, UINT, RECT, RECT] - CalculatePopupWindowPosition.restype = BOOL + def get_localized_path(path: str, config_home: str) -> str: + """ + Get the localized display path for a Windows file system path. + Shows only drive letters without labels. - except AttributeError as e: - logger.error( - 'win32 and not is_wine, but ctypes.windll.user32.CalculatePopupWindowPosition invalid', - exc_info=e - ) + Args: + path: The path to localize + config_home: The home directory path + Returns: + Localized path as string + """ + if not path: + return path - else: - CalculatePopupWindowPosition.argtypes = [ - ctypes.POINTER(POINT), - ctypes.POINTER(SIZE), - UINT, - ctypes.POINTER(RECT), - ctypes.POINTER(RECT) - ] + # Determine start index based on if path starts with home directory + start_idx = len(config_home.split('\\')) if path.lower().startswith(config_home.lower()) else 0 - SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName - SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)] + # Split path into components + components = normpath(path).split('\\') + display_components = [] + + # Process each path component + for i in range(start_idx, len(components)): + current_path = '\\'.join(components[:i + 1]) + + # Handle drive letter specially + if i == 0 and ':' in components[i]: + display_components.append(components[i]) + continue + + try: + # Get localized name info using shell API + pidl = shell.SHParseDisplayName(current_path, 0)[0] + name_info = shell.SHGetNameFromIDList(pidl, shellcon.SIGDN_NORMALDISPLAY) + + if name_info: + display_components.append(name_info) + else: + display_components.append(components[i]) + + except (pythoncom.com_error, win32api.error): + # Fall back to original component name if localization fails + display_components.append(components[i]) + + return '\\'.join(display_components) class PreferencesDialog(tk.Toplevel): @@ -1099,27 +1120,9 @@ class PreferencesDialog(tk.Toplevel): entryfield['state'] = tk.NORMAL # must be writable to update entryfield.delete(0, tk.END) if sys.platform == 'win32': - start = len(config.home.split('\\')) if pathvar.get().lower().startswith(config.home.lower()) else 0 - display = [] - components = normpath(pathvar.get()).split('\\') - buf = ctypes.create_unicode_buffer(MAX_PATH) - pidsRes = ctypes.c_int() # noqa: N806 # Windows convention - for i in range(start, len(components)): - try: - if (not SHGetLocalizedName('\\'.join(components[:i+1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and - win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, - pidsRes.value, buf, MAX_PATH)): - display.append(buf.value) - - else: - display.append(components[i]) - - except Exception: - display.append(components[i]) - - entryfield.insert(0, '\\'.join(display)) - - # None if path doesn't exist + localized_path = get_localized_path(pathvar.get(), config.home) + entryfield.insert(0, localized_path) + # None if path doesn't exist else: if pathvar.get().startswith(config.home): entryfield.insert(0, '~' + pathvar.get()[len(config.home):]) @@ -1290,14 +1293,14 @@ class PreferencesDialog(tk.Toplevel): config.set('ui_transparency', self.transparency.get()) config.set('always_ontop', self.always_ontop.get()) config.set('minimize_system_tray', self.minimize_system_tray.get()) - if self.disable_system_tray.get() != config.get('no_systray'): + if self.disable_system_tray.get() != config.get_int('no_systray'): self.req_restart = True config.set('no_systray', self.disable_system_tray.get()) config.set('theme', self.theme.get()) config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) theme.apply(self.parent) - if self.plugdir.get() != config.get('plugin_dir'): + if self.plugdir.get() != config.get_str('plugin_dir'): config.set( 'plugin_dir', join(config.home_path, self.plugdir.get()[2:]) if self.plugdir.get().startswith( diff --git a/protocol.py b/protocol.py index 6f2bf406..58ee9fa3 100644 --- a/protocol.py +++ b/protocol.py @@ -12,7 +12,6 @@ import sys import threading from urllib import parse from typing import TYPE_CHECKING, Type - from config import config from constants import appname, protocolhandler_redirect from EDMCLogging import get_main_logger @@ -25,13 +24,7 @@ logger = get_main_logger() is_wine = False if sys.platform == 'win32': - from ctypes import windll # type: ignore - - try: - if windll.ntdll.wine_get_version: - is_wine = True - except Exception: - pass + is_wine = bool(os.getenv('WINEPREFIX')) class GenericProtocolHandler: @@ -74,10 +67,12 @@ if (config.auth_force_edmc_protocol # noqa: C901 windll, POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at ) from ctypes.wintypes import ( - ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR, + ATOM, BOOL, HBRUSH, HGLOBAL, HICON, HINSTANCE, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR, MSG, UINT, WPARAM ) import win32gui + import win32api + import win32con class WNDCLASS(Structure): """ @@ -102,15 +97,9 @@ if (config.auth_force_edmc_protocol # noqa: C901 CW_USEDEFAULT = 0x80000000 - CreateWindowExW = windll.user32.CreateWindowExW - CreateWindowExW.argtypes = [DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID] - CreateWindowExW.restype = HWND RegisterClassW = windll.user32.RegisterClassW RegisterClassW.argtypes = [POINTER(WNDCLASS)] - GetParent = windll.user32.GetParent - SetForegroundWindow = windll.user32.SetForegroundWindow - # # NB: Despite 'BOOL' return type, it *can* be >0, 0 or -1, so is actually # c_long @@ -120,7 +109,6 @@ if (config.auth_force_edmc_protocol # noqa: C901 TranslateMessage = windll.user32.TranslateMessage DispatchMessageW = windll.user32.DispatchMessageW - PostThreadMessageW = windll.user32.PostThreadMessageW SendMessageW = windll.user32.SendMessageW SendMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM] PostMessageW = windll.user32.PostMessageW @@ -223,7 +211,7 @@ if (config.auth_force_edmc_protocol # noqa: C901 thread = self.thread if thread: self.thread = None - PostThreadMessageW(thread.ident, WM_QUIT, 0, 0) + win32gui.PostThreadMessage(thread.ident, WM_QUIT, 0, 0) thread.join() # Wait for it to quit def worker(self) -> None: @@ -233,7 +221,7 @@ if (config.auth_force_edmc_protocol # noqa: C901 wndclass.lpfnWndProc = WndProc wndclass.cbClsExtra = 0 wndclass.cbWndExtra = 0 - wndclass.hInstance = windll.kernel32.GetModuleHandleW(0) + wndclass.hInstance = win32api.GetModuleHandle(None) wndclass.hIcon = None wndclass.hCursor = None wndclass.hbrBackground = None @@ -245,16 +233,17 @@ if (config.auth_force_edmc_protocol # noqa: C901 return # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw - hwnd = CreateWindowExW( + hwnd = win32gui.CreateWindowEx( 0, # dwExStyle - wndclass.lpszClassName, # lpClassName - "DDE Server", # lpWindowName - 0, # dwStyle - CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, # X, Y, nWidth, nHeight - self.master.winfo_id(), # hWndParent # Don't use HWND_MESSAGE since the window won't get DDE broadcasts - None, # hMenu - wndclass.hInstance, # hInstance - None # lpParam + 'DDEServer', # lpClassName (use string directly as win32gui expects it) + "DDE Server", # lpWindowName + 0, # dwStyle + win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, # X, Y, nWidth, nHeight + self.master.winfo_id(), # hWndParent + 0, # hMenu (use 0 instead of None for win32gui) + wndclass.hInstance, # hInstance + None # lpParam ) msg = MSG() @@ -285,8 +274,8 @@ if (config.auth_force_edmc_protocol # noqa: C901 logger.debug(f'Message starts with {self.redirect}') self.event(url) - SetForegroundWindow(GetParent(self.master.winfo_id())) # raise app window # Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE + win32gui.SetForegroundWindow(win32gui.GetParent(self.master.winfo_id())) PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam)) else: @@ -294,7 +283,7 @@ if (config.auth_force_edmc_protocol # noqa: C901 PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam)) elif msg.message == WM_DDE_TERMINATE: - PostMessageW(msg.wParam, WM_DDE_TERMINATE, hwnd, 0) + win32gui.PostMessage(msg.wParam, WM_DDE_TERMINATE, hwnd, 0) else: TranslateMessage(byref(msg)) # "Translates virtual key messages into character messages" ???