diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 10:12:34 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 10:12:34 -0600 |
| commit | 3610309b1e5d83049554afe287129c99389d231d (patch) | |
| tree | dcf2f554aff081381d6ff60074546a1a54732c96 | |
| parent | 0a8d42501c1bf168bd852537bb3cd18490fac7ee (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.json | 2 | ||||
| -rw-r--r-- | plugins/example/__pycache__/plugin.cpython-312.pyc | bin | 3549 -> 0 bytes | |||
| -rw-r--r-- | plugins/example/manifest.json | 8 | ||||
| -rw-r--r-- | plugins/example/plugin.py | 105 | ||||
| -rw-r--r-- | plugins/motion_capture/plugin.py | 11 | ||||
| -rw-r--r-- | ui/main_window.py | 19 | ||||
| -rw-r--r-- | ui/windows/settings_window.py | 41 |
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 Binary files differdeleted file mode 100644 index c31b466..0000000 --- a/plugins/example/__pycache__/plugin.cpython-312.pyc +++ /dev/null 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) |
