Files
AR-Autopilot/display_manager/win32_utils.py
T
alro65 42b2eec2e1 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
2026-05-24 21:48:27 -04:00

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))