Merge pull request #2416 from HullSeals/enhancement/1805/more-pywin32-handoffs

[#1805/#2389] Fix Hotkey and Reduce Ctypes
This commit is contained in:
David Sangrey authored and GitHub committed 2025-04-29 22:51:41 -04:00
commit 0a4fe05594
6 files changed
+191 -305

No files matched your search

+27 -23
View File
@@ -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
+19 -21
View File
@@ -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:
+69 -136
View File
@@ -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
# <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapvirtualkeyexa>
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 <<Invoke>>')
self.root.event_generate('<<Invoke>>', 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 <<Invoke>>')
self.root.event_generate('<<Invoke>>', 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()
+8 -49
View File
@@ -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: <n/a> | 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
+50 -47
View File
@@ -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(
+18 -29
View File
@@ -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
# <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew>
# 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" ???