42b2eec2e1
Adds the AR Display Manager daemon (Task #9): - display_manager/ package with 8 modules: - app_registry.py : static metadata for the 4 bridge apps - config.py : JSON-persisted config + per-screen layout store - win32_utils.py : ctypes wrappers (EnumWindows, SetWindowPos, ShowWindow) - process_manager.py: launch + track app processes, HWND lookup - floating_button.py: always-on-top 52×52 px AR button per monitor with draggable placement + custom-painted popup - display_manager.py: orchestrator (QScreens → buttons → app placement + system tray with Settings/Launch/Quit) - settings_dialog.py: modal dialog for exe paths, button position, and Windows autostart toggle - autostart.py : HKCU Run registry read/write helpers - display_manager_main.py at repo root: launcher script - Studio Overview tab: "Lanzar Display Manager" button - pyproject.toml: display-manager optional dependency group (PySide6) Layout persistence: ~/.ar-autopilot/display_manager/layout.json Config: ~/.ar-autopilot/display_manager/config.json Supports up to 4 monitors (2 HDMI native + 2 USB DisplayLink). Double-click tray icon to toggle button visibility. AR_electronics — AR-Autopilot Project
85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
"""Windows API helpers for window management (ctypes, no extra dependencies)."""
|
|
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import ctypes.wintypes as wt
|
|
import sys
|
|
|
|
if sys.platform != "win32":
|
|
# Stub for non-Windows development/testing
|
|
def find_window_by_pid(pid: int) -> int | None:
|
|
return None
|
|
|
|
def move_and_maximize(hwnd: int, x: int, y: int, w: int, h: int) -> None:
|
|
pass
|
|
|
|
def is_window_alive(hwnd: int) -> bool:
|
|
return False
|
|
|
|
else:
|
|
_user32 = ctypes.windll.user32 # type: ignore[attr-defined]
|
|
|
|
# Constants
|
|
_HWND_TOP = 0
|
|
_SW_RESTORE = 9
|
|
_SW_MAXIMIZE = 3
|
|
_SWP_SHOWWINDOW = 0x0040
|
|
_SWP_FRAMECHANGED = 0x0020
|
|
_GW_OWNER = 4
|
|
|
|
# Callback type for EnumWindows
|
|
_WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wt.HWND, wt.LPARAM)
|
|
|
|
def find_window_by_pid(pid: int) -> int | None:
|
|
"""Return the first visible top-level HWND belonging to *pid*."""
|
|
found: list[int] = []
|
|
|
|
def _cb(hwnd: int, _: int) -> bool:
|
|
if not _user32.IsWindowVisible(hwnd):
|
|
return True
|
|
# Skip windows with an owner (child dialogs, etc.)
|
|
if _user32.GetWindow(hwnd, _GW_OWNER):
|
|
return True
|
|
lp_pid = wt.DWORD()
|
|
_user32.GetWindowThreadProcessId(hwnd, ctypes.byref(lp_pid))
|
|
if lp_pid.value == pid:
|
|
found.append(hwnd)
|
|
return False # stop enumeration
|
|
return True
|
|
|
|
_user32.EnumWindows(_WNDENUMPROC(_cb), 0)
|
|
return found[0] if found else None
|
|
|
|
def find_window_by_title_hint(hint: str) -> int | None:
|
|
"""Return the first visible top-level HWND whose title contains *hint*."""
|
|
if not hint:
|
|
return None
|
|
found: list[int] = []
|
|
|
|
def _cb(hwnd: int, _: int) -> bool:
|
|
if not _user32.IsWindowVisible(hwnd):
|
|
return True
|
|
buf = ctypes.create_unicode_buffer(512)
|
|
_user32.GetWindowTextW(hwnd, buf, 512)
|
|
if hint.lower() in buf.value.lower():
|
|
found.append(hwnd)
|
|
return False
|
|
return True
|
|
|
|
_user32.EnumWindows(_WNDENUMPROC(_cb), 0)
|
|
return found[0] if found else None
|
|
|
|
def move_and_maximize(hwnd: int, x: int, y: int, w: int, h: int) -> None:
|
|
"""Move a window to (x, y, w, h) then maximize it on that monitor."""
|
|
_user32.ShowWindow(hwnd, _SW_RESTORE)
|
|
_user32.SetWindowPos(
|
|
hwnd, _HWND_TOP,
|
|
x, y, w, h,
|
|
_SWP_SHOWWINDOW | _SWP_FRAMECHANGED,
|
|
)
|
|
_user32.ShowWindow(hwnd, _SW_MAXIMIZE)
|
|
_user32.SetForegroundWindow(hwnd)
|
|
|
|
def is_window_alive(hwnd: int) -> bool:
|
|
return bool(_user32.IsWindow(hwnd))
|