Skip to content

Commit

Permalink
[EDCD#1805] pywin32 Handoff
Browse files Browse the repository at this point in the history
  • Loading branch information
Rixxan committed Jun 11, 2024
1 parent b10548d commit 17a7af9
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 96 deletions.
29 changes: 6 additions & 23 deletions config/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,32 @@
"""
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):
"""Implementation of AbstractConfig for Windows."""

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'
Expand All @@ -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
Expand All @@ -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:
Expand Down
83 changes: 44 additions & 39 deletions hotkey/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
# <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:
Expand All @@ -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 ''
Expand Down Expand Up @@ -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

Expand All @@ -210,23 +210,25 @@ 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

fake = INPUT(INPUT_KEYBOARD, INPUTUNION(ki=KEYBDINPUT(keycode, keycode, 0, 0, None)))

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 <<Invoke>>')
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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 += '⁈'

Expand All @@ -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)
18 changes: 5 additions & 13 deletions monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
10 changes: 3 additions & 7 deletions prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)]

Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading

0 comments on commit 17a7af9

Please sign in to comment.