Files
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

111 lines
3.9 KiB
Python

"""Settings dialog for AR Display Manager — configure exe paths and button position."""
from __future__ import annotations
from pathlib import Path
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLineEdit,
QPushButton,
QVBoxLayout,
QWidget,
)
from display_manager.app_registry import APPS
from display_manager.autostart import disable as autostart_disable
from display_manager.autostart import enable as autostart_enable
from display_manager.autostart import is_enabled as autostart_is_enabled
from display_manager.config import AppExeConfig, DisplayManagerConfig
class SettingsDialog(QDialog):
"""Modal settings editor for the Display Manager."""
def __init__(self, config: DisplayManagerConfig, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setWindowTitle("AR Display Manager — Settings")
self.setMinimumWidth(520)
self._config = config
self._fields: dict[str, QLineEdit] = {}
self._build_ui()
def _build_ui(self) -> None:
layout = QVBoxLayout(self)
# Button position
pos_group = QGroupBox("Floating button")
pos_form = QFormLayout(pos_group)
self._pos_combo = QComboBox()
for label, val in [
("Top-right (default)", "top-right"),
("Top-left", "top-left"),
("Bottom-right", "bottom-right"),
("Bottom-left", "bottom-left"),
]:
self._pos_combo.addItem(label, userData=val)
idx = self._pos_combo.findData(self._config.button_position)
if idx >= 0:
self._pos_combo.setCurrentIndex(idx)
pos_form.addRow("Position on each monitor:", self._pos_combo)
layout.addWidget(pos_group)
# App executables
apps_group = QGroupBox("Application executables")
apps_form = QFormLayout(apps_group)
for app_meta in APPS:
exe_cfg = self._config.apps.get(app_meta.id, AppExeConfig())
row = QHBoxLayout()
field = QLineEdit(exe_cfg.exe)
field.setPlaceholderText("(not configured — click Browse)")
self._fields[app_meta.id] = field
row.addWidget(field, stretch=1)
browse = QPushButton("Browse…")
browse.clicked.connect(lambda checked=False, f=field: self._browse(f))
row.addWidget(browse)
apps_form.addRow(f"{app_meta.icon} {app_meta.name}:", row)
layout.addWidget(apps_group)
# Autostart
sys_group = QGroupBox("System")
sys_form = QFormLayout(sys_group)
self._autostart_chk = QCheckBox("Start AR Display Manager with Windows")
self._autostart_chk.setChecked(autostart_is_enabled())
sys_form.addRow(self._autostart_chk)
layout.addWidget(sys_group)
# Buttons
btns = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
btns.accepted.connect(self._accept)
btns.rejected.connect(self.reject)
layout.addWidget(btns)
def _browse(self, field: QLineEdit) -> None:
path, _ = QFileDialog.getOpenFileName(
self, "Select executable", str(Path.home()), "Executables (*.exe);;All files (*)"
)
if path:
field.setText(path)
def _accept(self) -> None:
self._config.button_position = self._pos_combo.currentData()
for app_meta in APPS:
if app_meta.id not in self._config.apps:
self._config.apps[app_meta.id] = AppExeConfig()
self._config.apps[app_meta.id].exe = self._fields[app_meta.id].text()
self._config.save()
# Autostart
if self._autostart_chk.isChecked():
autostart_enable()
else:
autostart_disable()
self.accept()