diff options
Diffstat (limited to 'ui/main_window.py')
| -rw-r--r-- | ui/main_window.py | 143 |
1 files changed, 140 insertions, 3 deletions
diff --git a/ui/main_window.py b/ui/main_window.py index 0bb62f6..a4b62e9 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -43,6 +43,9 @@ from ui.windows.signals_window import SignalsWindow from ui.windows.plot_window import PlotWindow, build_default_layout from ui.windows.settings_window import SettingsWindow +from plugins.plugin_manager import PluginManager +from plugins.base_plugin import PluginContext + _DARK_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_dark.qss") _LIGHT_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_light.qss") @@ -65,12 +68,20 @@ class MainWindow(QMainWindow): self._win_plot = None self._win_settings = None + _plugins_dir = os.path.join(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))), "plugins") + self._plugin_mgr = PluginManager(_plugins_dir) + self._plugin_mgr.discover() + # {plugin_id: [QAction, ...]} toolbar actions to remove on unload + self._plugin_toolbar_actions: dict = {} + self._wheel_blocker = _WheelBlocker(self) QApplication.instance().installEventFilter(self._wheel_blocker) self._init_demo_devices() self._build_ui() self._connect_signals() + self._init_plugins() # ── Demo ────────────────────────────────────────────────────────────── @@ -89,6 +100,7 @@ class MainWindow(QMainWindow): def _build_ui(self): tb = QToolBar(); tb.setObjectName("mainToolbar"); tb.setMovable(False) self.addToolBar(tb) + self._toolbar = tb def _sep(): s = QFrame(); s.setFrameShape(QFrame.Shape.VLine) @@ -138,11 +150,14 @@ class MainWindow(QMainWindow): tb.addWidget(_sep()) + # Plugin buttons are inserted here at runtime (between this sep and spacer) + self._plugin_sep_action = tb.addWidget(_sep()) + self._plugin_sep_action.setVisible(False) - # Spacer + clock + # Spacer + clock — plugin buttons insert before this action spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) - tb.addWidget(spacer) + self._spacer_action = tb.addWidget(spacer) self._time_lbl = QLabel("00:00:00"); self._time_lbl.setObjectName("timeLabel") tb.addWidget(self._time_lbl) @@ -185,6 +200,108 @@ class MainWindow(QMainWindow): self.engine.log_started.connect(lambda p: self._log_lbl.setText(f"● {p}")) self.engine.log_stopped.connect(lambda p: self._log_lbl.setText(f"✓ {p}")) + # ── Plugin lifecycle ────────────────────────────────────────────────── + + def _make_plugin_context(self) -> PluginContext: + return PluginContext( + registry=self.registry, + engine=self.engine, + processor=self.processor, + main_window=self, + ) + + def _init_plugins(self): + ctx = self._make_plugin_context() + for plugin in self._plugin_mgr.load_enabled(ctx): + self._install_plugin(plugin) + + def _install_plugin(self, plugin): + """Register a loaded plugin's devices, filters, and toolbar buttons.""" + from core.signal_processor import FILTER_CLASSES + + # Devices + for dev in plugin.get_devices(): + try: + dev.connect() + self.registry.add_instance(dev) + self.engine.add_device(dev) + except Exception as exc: + print(f"[Plugin:{plugin.plugin_id}] Device error: {exc}") + + # Custom filter classes + for name, cls in plugin.get_filter_classes().items(): + FILTER_CLASSES[name] = cls + + # Toolbar buttons + actions = plugin.get_toolbar_actions() + tb_actions = [] + if actions: + self._plugin_sep_action.setVisible(True) + for pa in actions: + label = f"{pa.icon} {pa.label}" if pa.icon else pa.label + btn = QPushButton(label) + btn.setObjectName("toolbarSectionBtn") + btn.setCheckable(pa.checkable) + if pa.tooltip: + btn.setToolTip(pa.tooltip) + btn.clicked.connect(pa.callback) + action = self._toolbar.insertWidget(self._spacer_action, btn) + tb_actions.append(action) + self._plugin_toolbar_actions[plugin.plugin_id] = tb_actions + + self._chart.refresh() + if self._win_plot: + self._win_plot.refresh_channels() + self._status.setText(f"Plugin enabled: {plugin.name}") + + def _uninstall_plugin(self, plugin_id: str): + """Remove a plugin's toolbar buttons, devices, and filter classes.""" + from core.signal_processor import FILTER_CLASSES + + plugin = self._plugin_mgr.get_plugin(plugin_id) + + # Remove toolbar buttons first (before unload) + for action in self._plugin_toolbar_actions.pop(plugin_id, []): + self._toolbar.removeAction(action) + + # Hide plugin separator if no plugins remain + if not any(acts for acts in self._plugin_toolbar_actions.values()): + self._plugin_sep_action.setVisible(False) + + if plugin is None: + return + + # Remove custom filter classes + for name in plugin.get_filter_classes(): + FILTER_CLASSES.pop(name, None) + + # Remove devices contributed by this plugin + for dev in plugin.get_devices(): + dev_id = dev.info.device_id + try: + dev.disconnect() + except Exception: + pass + self.registry.remove_instance(dev_id) + self.engine.remove_device(dev_id) + + self._chart.refresh() + if self._win_plot: + self._win_plot.refresh_channels() + self._status.setText(f"Plugin disabled: {plugin.name}") + + def plugin_enable(self, plugin_id: str): + """Called by SettingsWindow when user enables a plugin.""" + ctx = self._make_plugin_context() + plugin = self._plugin_mgr.enable(plugin_id, ctx) + if plugin: + self._install_plugin(plugin) + + def plugin_disable(self, plugin_id: str): + """Called by SettingsWindow when user disables a plugin.""" + self._uninstall_plugin(plugin_id) + self._plugin_mgr.disable(plugin_id) + # ── Window management ───────────────────────────────────────────────── def _toggle_win(self, name: str, checked: bool, btn: QPushButton): @@ -239,9 +356,12 @@ class MainWindow(QMainWindow): def _open_settings(self): if self._win_settings is None: self._win_settings = SettingsWindow(self.registry, self.engine, - self._settings, self) + self._settings, self, + plugin_manager=self._plugin_mgr) self._win_settings.theme_changed.connect(self._apply_theme) self._win_settings.settings_changed.connect(self._on_settings) + self._win_settings.plugin_enable_requested.connect(self.plugin_enable) + self._win_settings.plugin_disable_requested.connect(self.plugin_disable) self._win_settings.closed.connect(lambda: self._btn_settings.setChecked(False)) self._show_win(self._win_settings, "right") @@ -297,10 +417,21 @@ class MainWindow(QMainWindow): control_specs=self._ctrl.get_specs(), settings=self._settings, profile_name=name, + plugin_manager=self._plugin_mgr, ) def _profile_apply(self, profile: Profile): """Restore state from a Profile object.""" + # Reconcile plugin enabled state before the rest of apply runs, + # so plugin devices are present when channels/pipelines are restored. + if profile.plugins_enabled is not None: + wanted = set(profile.plugins_enabled) + current = set(self._plugin_mgr.get_enabled_ids()) + for pid in current - wanted: + self.plugin_disable(pid) + for pid in wanted - current: + self.plugin_enable(pid) + plot_cfg = ProfileManager.apply( profile=profile, registry=self.registry, @@ -308,6 +439,7 @@ class MainWindow(QMainWindow): control_panel=self._ctrl, settings_ref=self._settings, engine=self.engine, + plugin_manager=self._plugin_mgr, ) if plot_cfg: self._chart.apply_layout(plot_cfg) @@ -400,4 +532,9 @@ class MainWindow(QMainWindow): for w in (self._win_devices, self._win_signals, self._win_plot, self._win_settings): if w: w.close() + for plugin in list(self._plugin_mgr.get_loaded()): + try: + plugin.on_unload() + except Exception: + pass self.engine.stop(); event.accept() |
