summaryrefslogtreecommitdiff
path: root/ui/main_window.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui/main_window.py')
-rw-r--r--ui/main_window.py102
1 files changed, 90 insertions, 12 deletions
diff --git a/ui/main_window.py b/ui/main_window.py
index 1ee8b7f..c7e4953 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -121,7 +121,7 @@ _LIGHT_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_lig
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
- self.setWindowTitle("LabDAQ")
+ self.setWindowTitle("LabUI")
self.setMinimumSize(1000, 640)
self.registry = DeviceRegistry()
@@ -133,9 +133,14 @@ class MainWindow(QMainWindow):
self._win_config = 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)
+ 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 = {}
@@ -290,6 +295,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)
@@ -438,27 +447,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"
@@ -575,12 +612,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)