diff options
Diffstat (limited to 'ui/main_window.py')
| -rw-r--r-- | ui/main_window.py | 103 |
1 files changed, 90 insertions, 13 deletions
diff --git a/ui/main_window.py b/ui/main_window.py index 9090d0f..efbb486 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -55,8 +55,7 @@ _LIGHT_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_lig class MainWindow(QMainWindow): def __init__(self): super().__init__() - from core.version import __version__ - self.setWindowTitle(f"LabDAQ v{__version__}") + self.setWindowTitle("LabUI") self.setMinimumSize(1000, 640) self.registry = DeviceRegistry() @@ -71,9 +70,14 @@ class MainWindow(QMainWindow): self._win_settings = None self._win_debug = None - _plugins_dir = os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), "plugins") - self._plugin_mgr = PluginManager(_plugins_dir) + import sys as _sys + _user_plugins = os.path.join(os.path.expanduser("~"), ".labui", "plugins") + _extra_dirs = [] + if not getattr(_sys, "frozen", False): + _project_plugins = os.path.join(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))), "plugins") + _extra_dirs = [_project_plugins] + self._plugin_mgr = PluginManager(_user_plugins, extra_scan_dirs=_extra_dirs) self._plugin_mgr.discover() # {plugin_id: [QAction, ...]} toolbar actions to remove on unload self._plugin_toolbar_actions: dict = {} @@ -200,6 +204,10 @@ class MainWindow(QMainWindow): sb = QStatusBar(); self.setStatusBar(sb) self._status = QLabel("Ready"); sb.addWidget(self._status) self._log_lbl = QLabel(""); sb.addPermanentWidget(self._log_lbl) + from core.version import __version__ + _ver_lbl = QLabel(f"v{__version__}") + _ver_lbl.setObjectName("versionLabel") + sb.addPermanentWidget(_ver_lbl) self._clock = QTimer(self); self._clock.setInterval(1000) self._clock.timeout.connect(self._tick) @@ -333,27 +341,55 @@ class MainWindow(QMainWindow): self._install_plugin(plugin) def _pip_install(self, requirements: list) -> bool: - """Blocking `pip install` of the given requirement strings. - Returns True on success; shows a result dialog either way.""" + """Blocking pip install of the given requirement strings. + + In a frozen (PyInstaller) app sys.executable is the exe itself, so we + locate a real Python interpreter and install into ~/.labui/plugin_packages/ + which is added to sys.path at startup (see main.py). In dev mode the + normal sys.executable + site-packages path is used instead. + Returns True on success; shows a result dialog either way. + """ + import shutil import subprocess - import sys + import sys as _sys from PyQt6.QtWidgets import QMessageBox + frozen = getattr(_sys, "frozen", False) + + if frozen: + python = shutil.which("python3") or shutil.which("python") + if not python: + QMessageBox.critical( + self, "Install Failed", + "Could not find a Python interpreter on PATH.\n" + "Install the required packages manually:\n\n" + + "\n".join(f" pip install {r}" for r in requirements) + ) + return False + pkg_dir = os.path.join(os.path.expanduser("~"), ".labui", "plugin_packages") + os.makedirs(pkg_dir, exist_ok=True) + cmd = [python, "-m", "pip", "install", "--target", pkg_dir, *requirements] + else: + python = _sys.executable + cmd = [python, "-m", "pip", "install", *requirements] + QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) try: - result = subprocess.run( - [sys.executable, "-m", "pip", "install", *requirements], - capture_output=True, text=True, - ) + result = subprocess.run(cmd, capture_output=True, text=True) finally: QApplication.restoreOverrideCursor() if result.returncode == 0: + if frozen: + pkg_dir = os.path.join(os.path.expanduser("~"), ".labui", "plugin_packages") + if pkg_dir not in _sys.path: + _sys.path.insert(0, pkg_dir) QMessageBox.information( self, "Install Complete", f"Installed: {', '.join(requirements)}" ) return True + QMessageBox.critical( self, "Install Failed", f"pip install failed for: {', '.join(requirements)}\n\n" @@ -516,12 +552,53 @@ class MainWindow(QMainWindow): plugin_manager=self._plugin_mgr, ) + def _check_profile_plugins(self, profile: Profile) -> bool: + """Detect missing/broken plugins and prompt user to fix them. + Returns False if the user cancels the profile load. + + Two cases are caught: + 'plugin' — plugin not installed at all + 'deps' — plugin installed but its dependencies are absent + """ + if not profile.plugins_enabled: + return True + + installed_ids = {m.plugin_id for m in self._plugin_mgr.get_manifests()} + missing = [] + for pid in profile.plugins_enabled: + pm_info = next( + (m for m in profile.plugins_manifest if m.get("plugin_id") == pid), + {"plugin_id": pid, "name": pid, "version": "unknown", + "description": "", "requires": []}, + ) + if pid not in installed_ids: + missing.append({**pm_info, "kind": "plugin"}) + else: + absent_deps = self._plugin_mgr.get_missing_dependencies(pid) + if absent_deps: + missing.append({**pm_info, "kind": "deps", + "missing_deps": absent_deps}) + + if not missing: + return True + + from PyQt6.QtWidgets import QDialog + from ui.windows.missing_plugins_dialog import MissingPluginsDialog + dlg = MissingPluginsDialog(missing, self._plugin_mgr, self) + return dlg.exec() == QDialog.DialogCode.Accepted + def _profile_apply(self, profile: Profile): """Restore state from a Profile object.""" + if not self._check_profile_plugins(profile): + return + # Reconcile plugin enabled state before the rest of apply runs, # so plugin devices are present when channels/pipelines are restored. + # Only enable plugins that are actually installed — missing ones were + # handled (or skipped) by _check_profile_plugins. if profile.plugins_enabled is not None: - wanted = set(profile.plugins_enabled) + installed_ids = {m.plugin_id for m in self._plugin_mgr.get_manifests()} + wanted = set(profile.plugins_enabled) & installed_ids current = set(self._plugin_mgr.get_enabled_ids()) for pid in current - wanted: self.plugin_disable(pid) |
