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,110 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user