summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-03 10:12:34 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-03 10:12:34 -0600
commit3610309b1e5d83049554afe287129c99389d231d (patch)
treedcf2f554aff081381d6ff60074546a1a54732c96
parent0a8d42501c1bf168bd852537bb3cd18490fac7ee (diff)
Fix settings/plugin window bugs; remove example plugin
- Add missing QGroupBox import to settings_window (caused silent crash on open) - Remove Controls tab and _controls_tab() method from settings - Clamp child window positions to screen bounds so they don't go off-screen on Windows when main window is maximized - Apply same screen-clamp fix to Motion Capture plugin window - Delete example plugin; disable it in enabled.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--plugins/enabled.json2
-rw-r--r--plugins/example/__pycache__/plugin.cpython-312.pycbin3549 -> 0 bytes
-rw-r--r--plugins/example/manifest.json8
-rw-r--r--plugins/example/plugin.py105
-rw-r--r--plugins/motion_capture/plugin.py11
-rw-r--r--ui/main_window.py19
-rw-r--r--ui/windows/settings_window.py41
7 files changed, 27 insertions, 159 deletions
diff --git a/plugins/enabled.json b/plugins/enabled.json
index a2216af..a33a40a 100644
--- a/plugins/enabled.json
+++ b/plugins/enabled.json
@@ -1,3 +1,3 @@
{
- "example": true
+ "motion_capture": true
} \ No newline at end of file
diff --git a/plugins/example/__pycache__/plugin.cpython-312.pyc b/plugins/example/__pycache__/plugin.cpython-312.pyc
deleted file mode 100644
index c31b466..0000000
--- a/plugins/example/__pycache__/plugin.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/plugins/example/manifest.json b/plugins/example/manifest.json
deleted file mode 100644
index a5af87d..0000000
--- a/plugins/example/manifest.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "plugin_id": "example",
- "name": "Example Plugin",
- "version": "1.0.0",
- "description": "Skeleton showing all plugin integration points. Safe to delete.",
- "author": "Your Name",
- "entry_point": "plugin.ExamplePlugin"
-}
diff --git a/plugins/example/plugin.py b/plugins/example/plugin.py
deleted file mode 100644
index e5fbb8e..0000000
--- a/plugins/example/plugin.py
+++ /dev/null
@@ -1,105 +0,0 @@
-"""
-plugins/example/plugin.py
-
-Skeleton plugin — shows every integration point.
-Copy this directory, rename it, update manifest.json, and fill in your logic.
-"""
-
-from __future__ import annotations
-from typing import Any, Dict, List, Optional, Type
-
-from PyQt6.QtWidgets import QLabel, QWidget
-
-# LabDAQ imports available at runtime
-from plugins.base_plugin import LabPlugin, PluginAction, PluginContext
-
-
-class ExamplePlugin(LabPlugin):
-
- # ── Metadata ──────────────────────────────────────────────────────────
-
- @property
- def plugin_id(self) -> str:
- return "example"
-
- @property
- def name(self) -> str:
- return "Example Plugin"
-
- @property
- def version(self) -> str:
- return "1.0.0"
-
- @property
- def description(self) -> str:
- return "Skeleton showing all plugin integration points."
-
- @property
- def author(self) -> str:
- return "Your Name"
-
- # ── Lifecycle ─────────────────────────────────────────────────────────
-
- def on_load(self, context: PluginContext) -> None:
- self._ctx = context
- # e.g. start a background thread, open a camera, etc.
-
- def on_unload(self) -> None:
- # Release resources — called when user disables plugin
- pass
-
- # ── Toolbar ───────────────────────────────────────────────────────────
-
- def get_toolbar_actions(self) -> List[PluginAction]:
- return [
- PluginAction(
- label = "Example",
- icon = "🔌",
- tooltip = "Open example plugin window",
- checkable = True,
- callback = self._on_toolbar_click,
- )
- ]
-
- def _on_toolbar_click(self, checked: bool):
- # Open / close your plugin window here
- pass
-
- # ── Devices ───────────────────────────────────────────────────────────
-
- def get_devices(self) -> list:
- # Return BaseDevice instances — they are auto-added to the registry
- # and AcquisitionEngine so their channels appear as normal signals.
- #
- # Example (uncomment and adapt):
- # from devices.analog_input import AnalogInputDevice
- # return [AnalogInputDevice("my_plugin_ai", num_channels=2, simulate=True)]
- return []
-
- # ── Custom filters ────────────────────────────────────────────────────
-
- def get_filter_classes(self) -> Dict[str, Type]:
- # Return {type_name: FilterBase subclass} for custom pipeline filters.
- #
- # Example:
- # from .my_filter import MyFilter
- # return {"my_filter": MyFilter}
- return {}
-
- # ── Settings widget ───────────────────────────────────────────────────
-
- def get_settings_widget(self) -> Optional[QWidget]:
- # Return a QWidget shown in Settings > Plugins when this plugin is enabled.
- lbl = QLabel("No settings for this plugin.")
- lbl.setObjectName("traceSource")
- return lbl
-
- # ── Profile persistence ───────────────────────────────────────────────
-
- def get_save_state(self) -> Dict[str, Any]:
- # Return JSON-serialisable dict saved with .labdaq profiles.
- return {}
-
- def apply_save_state(self, state: Dict[str, Any]) -> None:
- # Restore plugin state when a profile is loaded.
- pass
diff --git a/plugins/motion_capture/plugin.py b/plugins/motion_capture/plugin.py
index e5d37c7..1c655c5 100644
--- a/plugins/motion_capture/plugin.py
+++ b/plugins/motion_capture/plugin.py
@@ -166,9 +166,16 @@ class MotionCapturePlugin(LabPlugin):
self._win.set_px_per_mm(state.get("px_per_mm", 10.0))
if checked:
- geo = self._ctx.main_window.geometry()
if not self._win.isVisible():
- self._win.move(geo.right() + 8, geo.top() + 40)
+ from PyQt6.QtWidgets import QApplication
+ mw = self._ctx.main_window
+ screen = QApplication.screenAt(mw.geometry().center()) or QApplication.primaryScreen()
+ avail = screen.availableGeometry()
+ geo = mw.normalGeometry()
+ self._win.adjustSize()
+ x = max(avail.left(), min(geo.right() + 8, avail.right() - self._win.width()))
+ y = max(avail.top(), min(geo.top() + 40, avail.bottom() - self._win.height()))
+ self._win.move(x, y)
self._win.show()
self._win.raise_()
else:
diff --git a/ui/main_window.py b/ui/main_window.py
index 4372b9f..abfd585 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -368,12 +368,25 @@ class MainWindow(QMainWindow):
self._show_win(self._win_settings, "right")
def _show_win(self, win: QWidget, position: str = "right"):
- geo = self.geometry()
if not win.isVisible():
+ screen = QApplication.screenAt(self.geometry().center())
+ if screen is None:
+ screen = QApplication.primaryScreen()
+ avail = screen.availableGeometry()
+ # Use normalGeometry so maximized windows don't push child off-screen
+ geo = self.normalGeometry()
+ win.adjustSize()
+ w, h = win.width(), win.height()
if position == "right":
- win.move(geo.right() + 8, geo.top() + 40)
+ x = geo.right() + 8
+ y = geo.top() + 40
else:
- win.move(geo.left(), geo.bottom() + 8)
+ x = geo.left()
+ y = geo.bottom() + 8
+ # Clamp to available screen area
+ x = max(avail.left(), min(x, avail.right() - w))
+ y = max(avail.top(), min(y, avail.bottom() - h))
+ win.move(x, y)
win.show(); win.raise_(); win.activateWindow()
# ── Profile callbacks ─────────────────────────────────────────────────
diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py
index 046cfae..422b6c9 100644
--- a/ui/windows/settings_window.py
+++ b/ui/windows/settings_window.py
@@ -13,7 +13,7 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTabWidget, QFrame, QFormLayout, QComboBox,
QSpinBox, QDoubleSpinBox, QCheckBox, QLineEdit,
- QFileDialog, QScrollArea,
+ QFileDialog, QScrollArea, QGroupBox,
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QCloseEvent, QFont
@@ -76,7 +76,6 @@ class SettingsWindow(QWidget):
tabs.addTab(self._general_tab(), " General ")
tabs.addTab(self._acquisition_tab(), " Acquisition ")
tabs.addTab(self._display_tab(), " Display ")
- tabs.addTab(self._controls_tab(), " Controls ")
tabs.addTab(self._plugins_tab(), " Plugins ")
# Bottom bar
@@ -163,44 +162,6 @@ class SettingsWindow(QWidget):
root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll)
return w
- def _controls_tab(self):
- """Output channel assignments — which device channel each control widget drives."""
- w = QWidget()
- scroll = QScrollArea(); scroll.setWidgetResizable(True)
- scroll.setObjectName("deviceScroll")
- cont = QWidget(); lay = QVBoxLayout(cont)
- lay.setContentsMargins(14,12,14,12); lay.setSpacing(8)
-
- info = QLabel(
- "Configure which physical output channels are driven by each control widget.\n"
- "Add output widget mappings below. Changes take effect on next app start."
- )
- info.setObjectName("traceSource"); info.setWordWrap(True)
- lay.addWidget(info)
-
- grp = QGroupBox("Output Assignments")
- g_lay = QFormLayout(grp); g_lay.setSpacing(6)
-
- # Collect digital output channels
- out_channels = ["— none —"]
- for dev in self.registry.all_instances():
- for ch in dev.info.channels:
- if ch.channel_id.startswith("do") or "out" in ch.channel_id.lower():
- out_channels.append(f"{dev.info.device_id} / {ch.channel_id} ({ch.name})")
-
- self._out_combos = {}
- for label in ["Pump Power", "Heater", "Motor Enable", "PWM Ch 1"]:
- cb = QComboBox(); cb.setObjectName("channelPickerCb")
- cb.addItems(out_channels)
- g_lay.addRow(f"{label}:", cb)
- self._out_combos[label] = cb
-
- lay.addWidget(grp)
- lay.addStretch()
- scroll.setWidget(cont)
- root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll)
- return w
-
def _plugins_tab(self):
w = QWidget()
scroll = QScrollArea(); scroll.setWidgetResizable(True)