diff --git a/main.py b/main.py index 7dea30f..1de2d47 100644 --- a/main.py +++ b/main.py @@ -1337,7 +1337,7 @@ class MainApplication(QMainWindow): def terminal_gui(self): if self.terminal is None or not self.terminal.isVisible(): - self.terminal = TerminalWindow(self) + self.terminal = TerminalWindow(self, self.plugin_manager) self.terminal.show() def update_optode_positions(self): diff --git a/plugin_manager.py b/plugin_manager.py index 2c43948..d1c81af 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -1,6 +1,7 @@ """ Filename: plugin_manager.py Description: Manager file for anything plugin related +Note: Compliant with pylance strict type checking Author: Tyler de Zeeuw License: GPL-3.0 @@ -117,7 +118,7 @@ class PluginManager(QObject): print(f"[PluginManager] {summary_msg}") - parent_widget = getattr(self, "main_window", None) or self + parent_widget = getattr(self, "main_window", None) QMessageBox.warning( parent_widget, "Plugin Load Failures", @@ -166,7 +167,9 @@ class PluginManager(QObject): for action in menubar.actions(): clean_text = action.text().replace("&", "").strip().lower() if clean_text == "plugins": - plugins_menu = action.menu() + menu = action.menu() + if isinstance(menu, QMenu): + plugins_menu = menu break # If it doesn't exist yet, create it @@ -191,8 +194,9 @@ class PluginManager(QObject): plugins_menu.addSeparator() manager_action = plugins_menu.addAction("Manage Plugins...") - if hasattr(self.main_window, "plugins_gui") and callable(self.main_window.plugins_gui): - manager_action.triggered.connect(self.main_window.plugins_gui) + plugins_gui_func = getattr(self.main_window, "plugins_gui", None) + if callable(plugins_gui_func): + manager_action.triggered.connect(plugins_gui_func) def unload_plugins(self) -> None: """Clears current active plugin instances.""" diff --git a/pylance_progress b/pylance_progress index 96d8148..fda4f96 100644 --- a/pylance_progress +++ b/pylance_progress @@ -1,4 +1,4 @@ -src\analysis\participantfoldchannels.py 158 +src\analysis\participantfoldchannels.py 84 src\shared\flaresbasewidget.py 1155 flares.py 2900 main_unit_tests.py 152 diff --git a/src/analysis/participantfoldchannels.py b/src/analysis/participantfoldchannels.py index 6b6c6cc..d10c5be 100644 --- a/src/analysis/participantfoldchannels.py +++ b/src/analysis/participantfoldchannels.py @@ -8,29 +8,31 @@ License: GPL-3.0 # Built-in Imports import os -from pathlib import Path import time import traceback + +from pathlib import Path from multiprocessing import Process, current_process, Manager -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union # External library imports -from matplotlib.backend_bases import Event import numpy as np +from pandas import DataFrame import matplotlib.pyplot as plt import matplotlib.image as mpimg from matplotlib.figure import Figure +from matplotlib.backend_bases import Event from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas from PySide6.QtWidgets import QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QProgressBar, QPushButton, QScrollArea, QSizePolicy, QWidget, QDialog, QVBoxLayout from PySide6.QtCore import QThread, Qt, QSize, QTimer, QObject, Signal from PySide6.QtGui import QCloseEvent, QMouseEvent, QPixmap, QImage -from pandas import DataFrame from mne.io.base import BaseRaw from src.shared.flaresbasewidget import FlaresBaseWidget from src.shared.shareddata import APP_NAME, resource_path +from flares import fold_channels class MultiProgressDialog(QDialog): @@ -82,7 +84,6 @@ def single_participant_worker( """ Runs inside its own dedicated process """ p_name = os.path.basename(file_path) try: - from flares import fold_channels # Perform the heavy fold_channels logic channel_results = fold_channels(raw=raw_data, p_name=p_name, progress_queue=progress_queue) @@ -284,7 +285,7 @@ class StaticChannelCanvas(FigureCanvas): class StandaloneLegendDialog(QWidget): - def __init__(self, canvas_engine, title_prefix, parent=None): + def __init__(self, canvas_engine: Any, title_prefix: str, parent: QWidget | None = None) -> None: super().__init__(None) self.setWindowTitle("Full View - Brodmann Legend") self.setMinimumSize(500, 600) @@ -302,7 +303,14 @@ class InteractiveParticipantGridCanvas(FigureCanvas): """The Big Grid Canvas. Dynamically scales row and column configurations to maintain a crisp 16:9 layout orientation. """ - def __init__(self, channels_data, color_map, is_fullscreen_copy=False, parent=None): + def __init__( + self, + channels_data: dict[str, list[dict[str, Any]]], + color_map: dict[str, str], + is_fullscreen_copy: bool = False, + parent: QWidget | None = None + ) -> None: + self.channels_data = channels_data self.color_map = color_map self.is_fullscreen_copy = is_fullscreen_copy @@ -494,7 +502,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas): self._fullscreen_refs.append(fullscreen_window) - def _calculate_total_brodmann_profile(self, channels_data: Dict[str, Any]): + def _calculate_total_brodmann_profile(self, channels_data: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: """Sums and normalizes the specificity profile across all channels.""" totals = {} num_channels = len(channels_data) @@ -503,7 +511,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas): return [] # Sum up specificities across all channels - for channel_name, data_list in channels_data.items(): + for _, data_list in channels_data.items(): for entry in data_list: landmark = entry['Landmark'] specificity = entry['Specificity'] @@ -532,7 +540,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas): return normalized_data_list - def _open_expanded_view(self, channel_name, data_list): + def _open_expanded_view(self, channel_name: str, data_list: list[dict[str, Any]]) -> None: # 1. Create a plain QWidget with NO parent (None) # This instantly makes it a top-level desktop window popup = QWidget(None) @@ -746,11 +754,12 @@ class ProcessOrchestrator(QObject): setup_finished = Signal(object, object, object, list) setup_failed = Signal(str) - def __init__(self, - selected_files, - haemo_dict: dict[str, BaseRaw], - worker_func - ): + def __init__( + self, + selected_files: list[str], + haemo_dict: dict[str, Any], + worker_func: Callable[..., Any] + ) -> None: super().__init__() self.selected_files = selected_files @@ -810,9 +819,6 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget): self.index_texts = [ "0 (Fold Channels)", - # "1 (second image)", - # "2 (third image)", - # "3 (fourth image)", ] self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts) @@ -888,13 +894,8 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget): self.multi_progress.add_participant(os.path.basename(file_path), total_channels) - from datetime import datetime - - print(f"Before: {datetime.now()}") self.multi_progress.show() - print(f"After 1: {datetime.now()}") - if current_process().name == 'MainProcess': # Create a clean background thread worker execution channel self.orchestrator_thread = QThread() @@ -914,7 +915,6 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget): self.orchestrator.setup_failed.connect(self.orchestrator.deleteLater) self.orchestrator_thread.start() - print(f"After 4: {datetime.now()}") def on_orchestration_success( self, @@ -1102,7 +1102,7 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget): vbox = QVBoxLayout(container) title = QLabel("Brodmann Area Legend") - title.setAlignment(Qt.AlignCenter) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) vbox.addWidget(title) pixmap = self._bytes_to_pixmap(legend_bytes) @@ -1110,10 +1110,10 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget): # Legends are usually tall, so we scale it differently or keep it smaller legend_label.setPixmap(pixmap.scaled( self.thumb_size, - Qt.KeepAspectRatio, - Qt.SmoothTransformation + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation )) - legend_label.setAlignment(Qt.AlignCenter) + legend_label.setAlignment(Qt.AlignmentFlag.AlignCenter) legend_label.mousePressEvent = lambda e, p=pixmap: self._open_full_size(p, "Brodmann Legend") vbox.addWidget(legend_label) diff --git a/src/window/plugins.py b/src/window/plugins.py index a7dd65c..82a8314 100644 --- a/src/window/plugins.py +++ b/src/window/plugins.py @@ -1,13 +1,15 @@ """ Filename: plugins.py Description: Plugins window +Note: Compliant with pylance strict type checking Author: Tyler de Zeeuw License: GPL-3.0 """ # Built-in imports -from typing import Any +from pathlib import Path +from typing import Any, cast # External library imports from PySide6.QtCore import Qt, QThread, Signal @@ -104,12 +106,14 @@ class PluginsWindow(QWidget): self._clear_details_panel() return - name = info.get("name", "Unknown") - version = info.get("version", "1.0.0") - author = info.get("author", "Unknown") - desc = info.get("description", "No description provided.") - is_disabled = info.get("is_disabled", False) - path = info.get("path", "") + plugin_info = cast(dict[str, Any], info) + + name = str(plugin_info.get("name", "Unknown")) + version = str(plugin_info.get("version", "1.0.0")) + author = str(plugin_info.get("author", "Unknown")) + desc = str(plugin_info.get("description", "No description provided.")) + is_disabled = bool(plugin_info.get("is_disabled", False)) + path = str(plugin_info.get("path", "")) self.lbl_plugin_title.setText(name) self.lbl_plugin_meta.setText(f"Version: {version}  |  Author: {author}") @@ -134,25 +138,37 @@ class PluginsWindow(QWidget): selected = self.installed_list.currentItem() if not selected: return - info = selected.data(Qt.ItemDataRole.UserRole) - if isinstance(info, dict) and "path" in info: - self.manager.toggle_plugin_state(info["path"]) + raw_info = selected.data(Qt.ItemDataRole.UserRole) + if isinstance(raw_info, dict): + info = cast(dict[str, Any], raw_info) + plugin_path = info.get("path") + if isinstance(plugin_path, str) and plugin_path: + self.manager.toggle_plugin_state(Path(plugin_path)) def uninstall_plugin(self) -> None: selected = self.installed_list.currentItem() if not selected: return - info = selected.data(Qt.ItemDataRole.UserRole) - if not isinstance(info, dict) or "path" not in info: + + raw_info = selected.data(Qt.ItemDataRole.UserRole) + if not isinstance(raw_info, dict): + return + + info = cast(dict[str, Any], raw_info) + plugin_path = info.get("path") + plugin_name = info.get("name", "this plugin") + + if not isinstance(plugin_path, str) or not plugin_path: return reply = QMessageBox.question( - self, "Confirm Uninstall", - f"Are you sure you want to delete '{info['name']}'?", + self, + "Confirm Uninstall", + f"Are you sure you want to delete '{plugin_name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: - self.manager.uninstall_plugin(info["path"]) + self.manager.uninstall_plugin(Path(plugin_path)) def fetch_remote_plugins(self) -> None: """Asynchronously fetches remote plugins on a background thread.""" @@ -197,7 +213,7 @@ class PluginsWindow(QWidget): p_id = plugin.get("id", "") version = plugin.get("version", "v0.0") desc = plugin.get("description", "") - platforms = plugin.get("platforms", []) + platforms = str(plugin.get("platforms", [])) min_v_str = plugin.get("min_app_version", "0.0.0") is_platform_ok = not platforms or self.manager.current_platform in platforms diff --git a/src/window/terminal.py b/src/window/terminal.py index a0c3b73..66b2775 100644 --- a/src/window/terminal.py +++ b/src/window/terminal.py @@ -17,8 +17,10 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit, QMainW from PySide6.QtCore import QProcess, Qt, QThread, Signal from file_ext_registration import register_file_association, is_windows_admin +from plugin_manager import PluginManager from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PLATFORM_NAME from src.window.about import AboutWindow +from src.window.plugins import PluginsWindow from updater import UpdateManager @@ -42,7 +44,7 @@ class _AssocWorker(QThread): class TerminalWindow(QWidget): - def __init__(self, parent: QWidget | None = None) -> None: + def __init__(self, parent: QWidget | None, plugin_manager: PluginManager) -> None: super().__init__(parent, Qt.WindowType.Window) self.setWindowTitle(f"Terminal - {APP_NAME.upper()}") self.resize(320, 180) @@ -58,6 +60,7 @@ class TerminalWindow(QWidget): self.setLayout(layout) self._process: QProcess | None = None + self.plugin_manager = plugin_manager self.commands: dict[str, Callable[..., Any]] = { "hello": self.cmd_hello, @@ -66,6 +69,7 @@ class TerminalWindow(QWidget): "about": self.cmd_about, "assoc": self.cmd_assoc, "update": self.cmd_update, + "plugins": self.cmd_plugins, "utest": self.cmd_utest, } @@ -122,6 +126,10 @@ class TerminalWindow(QWidget): self.about = AboutWindow(self) self.about.show() + def cmd_plugins(self, *args: Any) -> None: + self.about = PluginsWindow(self, self.plugin_manager) + self.about.show() + def cmd_update(self, *args: Any) -> str: main_win = self.parent() if not isinstance(main_win, QMainWindow):