feat: AR Display Manager — multi-monitor floating app switcher
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
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user