mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2026-10-27 01:16:06 +03:00
Merge pull request #2416 from HullSeals/enhancement/1805/more-pywin32-handoffs
[#1805/#2389] Fix Hotkey and Reduce Ctypes
This commit is contained in:
6 files changed
+191
-305
No files matched your search
+27
-23
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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" ???
|
||||
|
||||
Reference in new issue
Block a user