Source code for populse_mia.user_interface.pop_ups

"""
Module that defines all the pop-ups used across the Mia software.

:Contains:
    :Class:
        - ClickableLabel
        - DefaultValueListCreation
        - DefaultValueQLineEdit
        - PopUpAddPath
        - PopUpAddTag
        - PopUpCloneTag
        - PopUpClosePipeline
        - PopUpDataBrowserCurrentSelection
        - PopUpDeletedProject
        - PopUpDeleteProject
        - PopUpFilterSelection
        - PopUpInformation
        - PopUpInheritanceDict
        - PopUpMultipleSort
        - PopUpNewProject
        - PopUpOpenProject
        - PopUpPreferences
        - PopUpProperties
        - PopUpQuit
        - PopUpRemoveScan
        - PopUpRemoveTag
        - PopUpSaveProjectAs
        - PopUpSeeAllProjects
        - PopUpSelectFilter
        - PopUpSelectIteration
        - PopUpTagSelection  (must precede PopUpSelectTag)
        - PopUpSelectTag
        - PopUpSelectTagCountTable
        - PopUpShowHistory
        - PopUpVisualizedTags
        - QLabel_clickable

"""

##########################################################################
# Populse_mia - Copyright (C) IRMaGe/CEA, 2018
# Distributed under the terms of the CeCILL license, as published by
# the CEA-CNRS-INRIA. Refer to the LICENSE file or to
# http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.html
# for details.
##########################################################################

import ast
import glob
import hashlib
import logging
import os
import platform
import shutil
import subprocess
from datetime import datetime
from functools import partial
from typing import get_origin

import yaml

# Capsul imports
from capsul.api import capsul_engine
from capsul.pipeline.pipeline_nodes import PipelineNode
from capsul.qt_gui.widgets.pipeline_developer_view import PipelineDeveloperView
from capsul.qt_gui.widgets.settings_editor import SettingsEditor

# PyQt5 imports
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import (
    QAbstractItemView,
    QApplication,
    QCheckBox,
    QComboBox,
    QDialog,
    QDialogButtonBox,
    QFileDialog,
    QFormLayout,
    QGroupBox,
    QHBoxLayout,
    QHeaderView,
    QInputDialog,
    QLabel,
    QLineEdit,
    QListWidget,
    QListWidgetItem,
    QMessageBox,
    QPlainTextEdit,
    QPushButton,
    QRadioButton,
    QScrollArea,
    QSpinBox,
    QSplitter,
    QTableWidget,
    QTableWidgetItem,
    QTabWidget,
    QTreeWidget,
    QTreeWidgetItem,
    QVBoxLayout,
    QWidget,
)

# Populse_mia imports
from populse_mia.data_manager import (
    BRICK_EXEC,
    BRICK_EXEC_TIME,
    BRICK_INIT,
    BRICK_INIT_TIME,
    BRICK_INPUTS,
    BRICK_NAME,
    BRICK_OUTPUTS,
    COLLECTION_BRICK,
    COLLECTION_CURRENT,
    COLLECTION_HISTORY,
    COLLECTION_INITIAL,
    FIELD_TYPE_BOOLEAN,
    FIELD_TYPE_DATE,
    FIELD_TYPE_DATETIME,
    FIELD_TYPE_FLOAT,
    FIELD_TYPE_INTEGER,
    FIELD_TYPE_LIST_BOOLEAN,
    FIELD_TYPE_LIST_DATE,
    FIELD_TYPE_LIST_DATETIME,
    FIELD_TYPE_LIST_FLOAT,
    FIELD_TYPE_LIST_INTEGER,
    FIELD_TYPE_LIST_STRING,
    FIELD_TYPE_LIST_TIME,
    FIELD_TYPE_STRING,
    FIELD_TYPE_TIME,
    HISTORY_BRICKS,
    HISTORY_PIPELINE,
    TAG_CHECKSUM,
    TAG_FILENAME,
    TAG_HISTORY,
    TAG_ORIGIN_USER,
    TAG_TYPE,
    TAG_UNIT_DEGREE,
    TAG_UNIT_HZPIXEL,
    TAG_UNIT_MHZ,
    TAG_UNIT_MM,
    TAG_UNIT_MS,
    TYPE_MAT,
    TYPE_NII,
    TYPE_TXT,
    TYPE_UNKNOWN,
)
from populse_mia.data_manager.project import Project
from populse_mia.software_properties import Config
from populse_mia.user_interface.data_browser import data_browser

logger = logging.getLogger(__name__)


[docs] class ClickableLabel(QLabel): """ A QLabel subclass that emits a signal when clicked. .. Methods: - mousePressEvent: overrides the mousePressEvent method by emitting the clicked signal .. Signals: - clicked(): Emitted when the label is clicked. """ clicked = QtCore.pyqtSignal()
[docs] def __init__(self): """Initializes the ClickableLabel.""" super().__init__()
[docs] def mousePressEvent(self, event): """ Handles the mouse press event by emitting the `clicked` signal. :param event (QMouseEvent): The mouse event triggering the signal. """ self.clicked.emit()
[docs] class DefaultValueListCreation(QDialog): """ A dialog for creating or editing a list's default value. This widget allows users to input and manage a list of values based on a specified type (e.g., integers, floats, booleans, etc.). .. Methods: - add_element(): Adds one more element to the list. - default_init_table(): Initializes the table with a default value when no previous value exists. - remove_element(): Removes the last element from the list. - resize_table(): Adjusts the popup size based on the table content. - update_default_value(): Validates user input and updates the parent's default value. """
[docs] def __init__(self, parent, type): """ Initializes the DefaultValueListCreation dialog. :param parent (DefaultValueQLineEdit): The parent object. :param type (Type): The type of the list elements (e.g., int, float, str). """ super().__init__() self.setModal(True) # Current type chosen self.type = type self.parent = parent self.setWindowTitle( f"Adding a list of {self.type.__args__[0].__name__}" ) # The table that will be filled self.table = QTableWidget() self.table.setRowCount(1) value = self.parent.text() if value: try: list_value = ast.literal_eval(value) if isinstance(list_value, list): # If the previous value was already a list, we fill it self.table.setColumnCount(len(list_value)) for i, val in enumerate(list_value): self.table.setItem(0, i, QTableWidgetItem(str(val))) else: self.default_init_table() except Exception: self.default_init_table() else: self.default_init_table() self.resize_table() # Ok button self.ok_button = QPushButton("Ok") self.ok_button.clicked.connect(self.update_default_value) # Cancel button cancel_button = QPushButton("Cancel") cancel_button.clicked.connect(self.close) # Button to add an element to the list sources_images_dir = Config().getSourceImageDir() self.add_element_label = ClickableLabel() self.add_element_label.setObjectName("plus") self.add_element_label.setPixmap( QtGui.QPixmap( os.path.join(sources_images_dir, "green_plus.png") ).scaledToHeight(15) ) self.add_element_label.clicked.connect(self.add_element) # Button to remove the last element of the list self.remove_element_label = ClickableLabel() self.remove_element_label.setObjectName("minus") self.remove_element_label.setPixmap( QtGui.QPixmap( os.path.join(sources_images_dir, "red_minus.png") ).scaledToHeight(20) ) self.remove_element_label.clicked.connect(self.remove_element) # Layouts self.list_layout = QHBoxLayout() self.list_layout.addWidget(self.table) self.list_layout.addWidget(self.remove_element_label) self.list_layout.addWidget(self.add_element_label) self.h_box_final = QHBoxLayout() self.h_box_final.addWidget(self.ok_button) self.h_box_final.addWidget(cancel_button) self.v_box_final = QVBoxLayout() self.v_box_final.addLayout(self.list_layout) self.v_box_final.addLayout(self.h_box_final) self.setLayout(self.v_box_final)
[docs] def add_element(self): """ Adds a new empty element to the list. Increases the number of columns in the table by one and adds an empty `QTableWidgetItem` for user input. """ self.table.setColumnCount(self.table.columnCount() + 1) self.table.setItem(0, self.table.columnCount() - 1, QTableWidgetItem()) self.resize_table()
[docs] def default_init_table(self): """ Initializes the table with a default value. If no previous value exists, the table is set up with a single empty column to allow user input. """ # Table filled with a single element at the beginning if no value self.table.setColumnCount(1) self.table.setItem(0, 0, QTableWidgetItem())
[docs] def remove_element(self): """ Removes the last element from the list. Ensures that at least one column remains to prevent an empty table. Adjusts the table size accordingly. """ if self.table.columnCount() <= 1: return self.table.setColumnCount(self.table.columnCount() - 1) self.resize_table() self.adjustSize()
[docs] def resize_table(self): """ Adjusts the size of the popup window based on the table content. Dynamically resizes the table width and height based on the number of columns and rows, with a maximum width limit of 900 pixels. """ self.table.resizeColumnsToContents() total_width = sum( self.table.columnWidth(i) for i in range(self.table.columnCount()) ) total_height = sum( self.table.rowHeight(i) for i in range(self.table.rowCount()) ) max_width = 900 padding = 20 if total_width + 20 < max_width else 40 self.table.setFixedWidth(min(total_width + 20, max_width)) self.table.setFixedHeight(total_height + padding)
[docs] def update_default_value(self): """ Validates user input and updates the parent's default value. Converts table values to the specified list type, ensuring that each entry is valid. If any value is invalid, a warning message is displayed, and the update is aborted. If all values are valid, they are stored in the parent widget, and the dialog is closed. """ type_parsers = { FIELD_TYPE_LIST_INTEGER: int, FIELD_TYPE_LIST_FLOAT: float, FIELD_TYPE_LIST_BOOLEAN: lambda x: {"True": True, "False": False}[ x ], FIELD_TYPE_LIST_STRING: str, FIELD_TYPE_LIST_DATE: lambda x: datetime.strptime( x, "%d/%m/%Y" ).date(), FIELD_TYPE_LIST_DATETIME: lambda x: datetime.strptime( x, "%d/%m/%Y %H:%M:%S.%f" ), FIELD_TYPE_LIST_TIME: lambda x: datetime.strptime( x, "%H:%M:%S.%f" ).time(), } database_value = [] for i in range(self.table.columnCount()): item = self.table.item(0, i) text = item.text() if item else "" try: database_value.append(type_parsers[self.type](text)) except (ValueError, KeyError): msg = QMessageBox(self) msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("Warning") msg.setText("Invalid value") msg.setInformativeText( f"The value '{text}' is invalid for type {self.type}." ) msg.setStandardButtons(QMessageBox.Ok) msg.exec() return self.parent.setText(str(database_value)) self.close()
[docs] class DefaultValueQLineEdit(QLineEdit): """ A QLineEdit override for handling default values. This class customizes QLineEdit to handle list-type default values by displaying a popup when clicked. .. Methods: - mousePressEvent: Mouse pressed on the QLineEdit """
[docs] def __init__(self, parent): """ :param parent: The parent widget, expected to have a `type` attribute. """ super().__init__() self.parent = parent
[docs] def mousePressEvent(self, event): """ Handles mouse press events. If the parent's type is a list, displays a popup for list creation. :param event (QMouseEvent): The mouse press event (unused). """ if get_origin(self.parent.type) is list: # We display the pop up to create the list if the checkbox is # checked, otherwise we do nothing self.list_creation = DefaultValueListCreation( self, self.parent.type ) self.list_creation.show()
[docs] class PopUpAddPath(QDialog): """ Dialog for adding a document to the project without using the MRI Files Manager (File > Import). .. Methods: - file_to_choose: Opens a file dialog to choose a document. - find_type: Determines the document type when the file path changes. - save_path: Adds the file path to the database and updates the UI. """
[docs] def __init__(self, project, databrowser): """ Initializes the pop-up for adding a document. :param project (Project): The current project instance. :param databrowser (DataBrowser): The application's data browser. """ super().__init__() self.project = project self.databrowser = databrowser self.setWindowTitle("Add a document") self.setModal(True) vbox_layout = QVBoxLayout() # File selection layout file_layout = QHBoxLayout() file_label = QLabel("File: ") self.file_line_edit = QLineEdit() self.file_line_edit.setFixedWidth(300) self.file_line_edit.textChanged.connect(self.find_type) file_button = QPushButton("Choose a document") file_button.clicked.connect(self.file_to_choose) file_layout.addWidget(file_label) file_layout.addWidget(self.file_line_edit) file_layout.addWidget(file_button) vbox_layout.addLayout(file_layout) # File type layout type_layout = QHBoxLayout() type_label = QLabel("Type: ") self.type_line_edit = QLineEdit() type_layout.addWidget(type_label) type_layout.addWidget(self.type_line_edit) vbox_layout.addLayout(type_layout) # Action buttons layout button_layout = QHBoxLayout() self.ok_button = QPushButton("Ok") self.ok_button.clicked.connect(self.save_path) cancel_button = QPushButton("Cancel") cancel_button.clicked.connect(self.close) button_layout.addWidget(self.ok_button) button_layout.addWidget(cancel_button) vbox_layout.addLayout(button_layout) self.setLayout(vbox_layout)
[docs] def file_to_choose(self): """ Opens a file dialog for selecting documents. """ fname, _ = QFileDialog.getOpenFileNames( self, "Choose a document to import", os.path.expanduser("~") ) if fname: self.file_line_edit.setText(str(fname))
[docs] def find_type(self): """ Determines the document type when the file path changes. """ file_paths = self.file_line_edit.text() # If the input is empty, clear the type field if not file_paths: self.type_line_edit.clear() return try: file_list = ast.literal_eval(file_paths) except (SyntaxError, ValueError): self.type_line_edit.setText(str([TYPE_UNKNOWN] * len(file_paths))) return type_mapping = {".nii": TYPE_NII, ".mat": TYPE_MAT, ".txt": TYPE_TXT} file_types = [ type_mapping.get(os.path.splitext(f)[1], TYPE_UNKNOWN) for f in file_list ] self.type_line_edit.setText(str(file_types))
[docs] def save_path(self): """ Adds the selected document paths to the database and updates the UI. """ try: path_list = ( ast.literal_eval(self.file_line_edit.text()) if self.file_line_edit.text() else [""] ) path_type_list = ( ast.literal_eval(self.type_line_edit.text()) if self.type_line_edit.text() else [""] ) except (SyntaxError, ValueError): self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Warning) self.msg.setText("- Invalid Data! -") self.msg.setInformativeText( f"Invalid file(s) in:\n" f"{path_list}\n" f" or type format(s) in:\n" f"{path_type_list}" ) self.msg.setWindowTitle("Warning: Invalid data!") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show() return with self.project.database.data() as database_data: doc_in_db = { os.path.basename(doc) for doc in database_data.get_document_names(COLLECTION_CURRENT) } self.project.unsavedModifications = True for path, path_type in zip(path_list, path_type_list): filename = os.path.basename(path) if filename in doc_in_db: self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Warning) self.msg.setText(f"- {os.path.basename(path)} -") self.msg.setInformativeText( f"The document '{path}' \n " f"already exists in the Data Browser!" ) self.msg.setWindowTitle("Warning: existing data!") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show() continue if not path or not os.path.exists(path) or not path_type: self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Warning) self.msg.setText("Invalid arguments") self.msg.setInformativeText( "The path must exist.\nThe path type can't be empty." ) self.msg.setWindowTitle("Warning") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show() continue # Prepare history tracking history_maker = ["add_scans"] values_added = [] # Copy file to project directory rel_path = os.path.join("data", "downloaded_data", filename) copy_path = os.path.join(self.project.folder, rel_path) shutil.copy(path, copy_path) # Compute file checksum with open(path, "rb") as scan_file: checksum = hashlib.md5(scan_file.read()).hexdigest() # Add document to database with self.project.database.data(write=True) as database_data: for collection in (COLLECTION_INITIAL, COLLECTION_CURRENT): database_data.add_document(collection, rel_path) database_data.set_value( collection_name=collection, primary_key=rel_path, values_dict={TAG_TYPE: path_type}, ) database_data.set_value( collection_name=collection, primary_key=rel_path, values_dict={TAG_CHECKSUM: checksum}, ) values_added.extend( [ [rel_path, TAG_TYPE, path_type, path_type], [rel_path, TAG_CHECKSUM, checksum, checksum], ] ) # Update history history_maker.extend([[rel_path], values_added]) self.project.undos.append(history_maker) self.project.redos.clear() # Update data browser with self.project.database.data() as database_data: scans = database_data.get_document_names(COLLECTION_CURRENT) table_data = self.databrowser.table_data table_data.scans_to_visualize = scans table_data.scans_to_search = scans table_data.add_columns() table_data.fill_headers() table_data.add_rows([rel_path]) self.databrowser.reset_search_bar() self.databrowser.frame_advanced_search.setHidden(True) self.databrowser.advanced_search.rows = [] self.close()
[docs] class PopUpAddTag(QDialog): """ Dialog for adding a new tag to the project. This dialog allows users to create a new tag by specifying its name, default value, description, unit, and type. Attributes: signal_add_tag: Signal emitted when a new tag is successfully added Methods: - _connect_signals: Connect signals to slots - _setup_layouts: Set up the layout of UI elements - _setup_ui: Set up the dialog UI elements - _show_error: Display an error message box - ok_action: Validates input fields and adds the new tag if valid - on_activated: Updates form fields when tag type is changed """ # Signal emitted when tag is successfully added signal_add_tag = QtCore.pyqtSignal()
[docs] def __init__(self, databrowser, project): """ Initialize the dialog for adding a new tag. :param databrowser: The data browser instance :param project: The current project in the software """ super().__init__() self.project = project self.databrowser = databrowser # Default tag type is string self.type = FIELD_TYPE_STRING self.setObjectName("Add a tag") self.setWindowTitle("Add a tag") self.setMinimumWidth(700) self.setModal(True) self._setup_ui() self._connect_signals()
def _connect_signals(self): """Connect signals to slots.""" self.push_button_ok.clicked.connect(self.ok_action) self.combo_box_type.currentTextChanged.connect(self.on_activated) def _setup_layouts(self): """Set up the layout of UI elements.""" # Labels column v_box_labels = QVBoxLayout() v_box_labels.addWidget(self.label_tag_name) v_box_labels.addWidget(self.label_default_value) v_box_labels.addWidget(self.label_description_value) v_box_labels.addWidget(self.label_unit_value) v_box_labels.addWidget(self.label_type) # Edit fields column v_box_edits = QVBoxLayout() v_box_edits.addWidget(self.text_edit_tag_name) default_layout = QHBoxLayout() default_layout.addWidget(self.text_edit_default_value) v_box_edits.addLayout(default_layout) v_box_edits.addWidget(self.text_edit_description_value) v_box_edits.addWidget(self.combo_box_unit) v_box_edits.addWidget(self.combo_box_type) # Main content layout h_box_top = QHBoxLayout() h_box_top.addLayout(v_box_labels) h_box_top.addSpacing(50) h_box_top.addLayout(v_box_edits) # OK button layout h_box_ok = QHBoxLayout() h_box_ok.addStretch(1) h_box_ok.addWidget(self.push_button_ok) # Main dialog layout v_box_total = QVBoxLayout() v_box_total.addLayout(h_box_top) v_box_total.addLayout(h_box_ok) self.setLayout(v_box_total) def _setup_ui(self): """Set up the dialog UI elements.""" _translate = QtCore.QCoreApplication.translate # Create UI components self.push_button_ok = QPushButton(self) self.push_button_ok.setObjectName("push_button_ok") self.push_button_ok.setText(_translate("Add a tag", "OK")) # Tag name components self.label_tag_name = QLabel( _translate("Add a tag", "Tag name:"), self ) self.label_tag_name.setTextFormat(QtCore.Qt.AutoText) self.label_tag_name.setObjectName("tag_name") self.text_edit_tag_name = QLineEdit(self) self.text_edit_tag_name.setObjectName("textEdit_tag_name") # Default value components self.label_default_value = QLabel( _translate("Add a tag", "Default value:"), self ) self.label_default_value.setTextFormat(QtCore.Qt.AutoText) self.label_default_value.setObjectName("default_value") self.text_edit_default_value = DefaultValueQLineEdit(self) self.text_edit_default_value.setObjectName("textEdit_default_value") # Default value for string type self.text_edit_default_value.setText("Undefined") # Description components self.label_description_value = QLabel( _translate("Add a tag", "Description:"), self ) self.label_description_value.setTextFormat(QtCore.Qt.AutoText) self.label_description_value.setObjectName("description_value") self.text_edit_description_value = QLineEdit(self) self.text_edit_description_value.setObjectName( "textEdit_description_value" ) # Unit components self.label_unit_value = QLabel(_translate("Add a tag", "Unit:"), self) self.label_unit_value.setTextFormat(QtCore.Qt.AutoText) self.label_unit_value.setObjectName("unit_value") self.combo_box_unit = QComboBox(self) self.combo_box_unit.setObjectName("combo_box_unit") self.combo_box_unit.addItems( [ None, TAG_UNIT_MS, TAG_UNIT_MM, TAG_UNIT_MHZ, TAG_UNIT_HZPIXEL, TAG_UNIT_DEGREE, ] ) # Type components self.label_type = QLabel(_translate("Add a tag", "Tag type:"), self) self.label_type.setTextFormat(QtCore.Qt.AutoText) self.label_type.setObjectName("type") self.combo_box_type = QComboBox(self) self.combo_box_type.setObjectName("combo_box_type") self.combo_box_type.addItems( [ "String", "Integer", "Float", "Boolean", "Date", "Datetime", "Time", "String List", "Integer List", "Float List", "Boolean List", "Date List", "Datetime List", "Time List", ] ) # Set up layouts self._setup_layouts() def _show_error(self, text, informative_text): """ Display an error message box. :param text: The main error message text :param informative_text: The additional informative text """ self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Critical) self.msg.setText(text) self.msg.setInformativeText(informative_text) self.msg.setWindowTitle("Error") self.msg.setStandardButtons(QMessageBox.Close) self.msg.exec()
[docs] def ok_action(self): """Validate inputs and add the new tag if all fields are correct. Performs validation on the tag name, type and default value to ensure they are valid before adding the tag to the project. """ # Import check_value_type here to prevent circular import issues from populse_mia.utils import check_value_type # Get existing tag names with self.project.database.data() as database_data: existing_tags = database_data.get_field_names(COLLECTION_CURRENT) tag_name = self.text_edit_tag_name.text() default_value = self.text_edit_default_value.text() # Validation checks # Tag name can't be empty if not tag_name: self._show_error( "The tag name cannot be empty", "Please enter a tag name" ) return # Tag name can't exist already if tag_name in existing_tags: self._show_error( "This tag name already exists", "Please select another tag name", ) return # Special validation for PatientName tag if tag_name == "PatientName": if self.type is not FIELD_TYPE_STRING: self._show_error( "PatientName is a special tag that must be a character " "string containing no spaces!", "Please select string type.", ) return # Remove spaces from PatientName default value self.text_edit_default_value.setText( default_value.replace(" ", "") ) default_value = self.text_edit_default_value.text() # Validate default value type if not check_value_type(default_value, self.type, False): self._show_error( "Invalid default value", f"The default value '{default_value}' is invalid " f"with the '{self.type}' type!", ) return # All validations passed, add the tag self.accept() # Store values for databrowser self.new_tag_name = tag_name self.new_default_value = default_value self.new_tag_description = self.text_edit_description_value.text() self.new_tag_unit = self.combo_box_unit.currentText() or None # Add tag to project self.databrowser.add_tag_infos( self.new_tag_name, self.new_default_value, self.type, self.new_tag_description, self.new_tag_unit, ) self.close()
[docs] def on_activated(self, text): """ Update the default value when the tag type changes. :param text: The new type selected from combo box """ type_mapping = { "String": (FIELD_TYPE_STRING, "Undefined"), "Integer": (FIELD_TYPE_INTEGER, "0"), "Float": (FIELD_TYPE_FLOAT, "0.0"), "Boolean": (FIELD_TYPE_BOOLEAN, "True"), "Date": (FIELD_TYPE_DATE, datetime.now().strftime("%d/%m/%Y")), "Datetime": ( FIELD_TYPE_DATETIME, datetime.now().strftime("%d/%m/%Y %H:%M:%S.%f"), ), "Time": (FIELD_TYPE_TIME, datetime.now().strftime("%H:%M:%S.%f")), } # Handle list types separately to avoid repetition list_types = { "String List": ( FIELD_TYPE_LIST_STRING, "['Undefined', 'Undefined']", ), "Integer List": (FIELD_TYPE_LIST_INTEGER, "[0, 0]"), "Float List": (FIELD_TYPE_LIST_FLOAT, "[0.0, 0.0]"), "Boolean List": (FIELD_TYPE_LIST_BOOLEAN, "[True, True]"), } # Handle date-based list types which need current time formatting now = datetime.now() date_list_types = { "Date List": ( FIELD_TYPE_LIST_DATE, f"['{now.strftime('%d/%m/%Y')}'," f" '{now.strftime('%d/%m/%Y')}']", ), "Datetime List": ( FIELD_TYPE_LIST_DATETIME, f"['{now.strftime('%d/%m/%Y %H:%M:%S.%f')}'," f" '{now.strftime('%d/%m/%Y %H:%M:%S.%f')}']", ), "Time List": ( FIELD_TYPE_LIST_TIME, f"['{now.strftime('%H:%M:%S.%f')}'," f" '{now.strftime('%H:%M:%S.%f')}']", ), } # Combine all type mappings all_types = {**type_mapping, **list_types, **date_list_types} if text in all_types: self.type, default_value = all_types[text] self.text_edit_default_value.setText(default_value)
[docs] class PopUpCloneTag(QDialog): """ Dialog for cloning an existing tag with a new name. This dialog allows users to select an existing tag from the project and clone it with a new name. Attributes: signal_clone_tag: Signal emitted when a tag is successfully cloned Methods: - _connect_signals: Connect signals to slots - _populate_tag_list: Populate the tag list with available tags - _setup_ui: Set up the dialog UI elements - _show_error: Display an error message box - ok_action: Validates the new tag name and clones the selected tag - search_str: Filters the tag list based on a search string """ # Signal emitted when tag is successfully cloned signal_clone_tag = QtCore.pyqtSignal()
[docs] def __init__(self, databrowser, project): """ Initialize the dialog for cloning a tag. :param databrowser: The data browser instance :param project: The current project in the software """ super().__init__() self.databrowser = databrowser self.project = project self.setWindowTitle("Clone a tag") self.setObjectName("Clone a tag") self.setModal(True) self._setup_ui() self._populate_tag_list(project) self._connect_signals(project)
def _connect_signals(self, project): """Connect signals to slots. :param project: The current project """ self.push_button_ok.clicked.connect(lambda: self.ok_action(project)) self.search_bar.textChanged.connect(partial(self.search_str, project)) def _populate_tag_list(self, project): """ Populate the tag list with available tags from the project. :param project: The current project """ _translate = QtCore.QCoreApplication.translate # Get all tags except system tags with project.database.data() as database_data: tags_list = database_data.get_field_names(COLLECTION_CURRENT) tags_list.remove(TAG_CHECKSUM) tags_list.remove(TAG_HISTORY) # Add tags to list widget for tag in tags_list: item = QListWidgetItem(_translate("Dialog", tag)) self.list_widget_tags.addItem(item) self.list_widget_tags.sortItems() def _setup_ui(self): """Set up the dialog UI elements.""" _translate = QtCore.QCoreApplication.translate # New tag name components self.label_new_tag_name = QLabel( _translate("Clone a tag", "New tag name:"), self ) self.label_new_tag_name.setTextFormat(QtCore.Qt.AutoText) self.label_new_tag_name.setObjectName("label_new_tag_name") self.line_edit_new_tag_name = QLineEdit(self) self.line_edit_new_tag_name.setObjectName("lineEdit_new_tag_name") # OK button self.push_button_ok = QPushButton( _translate("Clone a tag", "OK"), self ) self.push_button_ok.setObjectName("push_button_ok") # Bottom row layout hbox_buttons = QHBoxLayout() hbox_buttons.addWidget(self.label_new_tag_name) hbox_buttons.addWidget(self.line_edit_new_tag_name) hbox_buttons.addStretch(1) hbox_buttons.addWidget(self.push_button_ok) # Tag list components self.label_tag_list = QLabel( _translate("Clone a tag", "Available tags:"), self ) self.label_tag_list.setTextFormat(QtCore.Qt.AutoText) self.label_tag_list.setObjectName("label_tag_list") self.search_bar = QLineEdit(self) self.search_bar.setObjectName("lineEdit_search_bar") self.search_bar.setPlaceholderText("Search") # Top row layout hbox_top = QHBoxLayout() hbox_top.addWidget(self.label_tag_list) hbox_top.addStretch(1) hbox_top.addWidget(self.search_bar) # Tag list widget self.list_widget_tags = QListWidget(self) self.list_widget_tags.setObjectName("listWidget_tags") self.list_widget_tags.setSelectionMode( QAbstractItemView.SingleSelection ) # Main dialog layout vbox = QVBoxLayout() vbox.addLayout(hbox_top) vbox.addWidget(self.list_widget_tags) vbox.addLayout(hbox_buttons) self.setLayout(vbox) def _show_error(self, text, informative_text): """Display an error message box. :param text: The main error message text :param informative_text: The additional informative text """ self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Critical) self.msg.setText(text) self.msg.setInformativeText(informative_text) self.msg.setWindowTitle("Error") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show()
[docs] def ok_action(self, project): """ Validate new tag name and clone the selected tag if valid. :param project: The current project """ new_tag_name = self.line_edit_new_tag_name.text() selected_items = self.list_widget_tags.selectedItems() # Check if tag name already exists with project.database.data() as database_data: existing_tags = [ tag["index"].split("|")[1] for tag in database_data.get_field_attributes( COLLECTION_CURRENT ) ] if new_tag_name in existing_tags: self._show_error( "This tag name already exists", "Please select another tag name", ) elif not new_tag_name: self._show_error( "The tag name can't be empty", "Please select a tag name" ) elif not selected_items: self._show_error( "The tag to clone must be selected", "Please select a tag to clone", ) else: self.accept() self.tag_to_replace = selected_items[0].text() self.new_tag_name = new_tag_name self.databrowser.clone_tag_infos( self.tag_to_replace, self.new_tag_name ) self.close()
[docs] def search_str(self, project, search_text): """Filter the tag list based on the search string. :param project: The current project :param search_text: The search string to filter by """ _translate = QtCore.QCoreApplication.translate # Get all tags except system tags with project.database.data() as database_data: all_tags = database_data.get_field_names(COLLECTION_CURRENT) all_tags.remove(TAG_CHECKSUM) all_tags.remove(TAG_HISTORY) # Filter tags by search text (case-insensitive) if search_text: search_text = search_text.upper() filtered_tags = [ tag for tag in all_tags if search_text in tag.upper() ] else: filtered_tags = all_tags # Update the list widget self.list_widget_tags.clear() for tag_name in filtered_tags: self.list_widget_tags.addItem( QListWidgetItem(_translate("Dialog", tag_name)) ) self.list_widget_tags.sortItems()
[docs] class PopUpClosePipeline(QDialog): """ Dialog displayed when closing a modified pipeline editor. This dialog asks the user whether they want to save changes before closing the pipeline editor. It provides three options: save, don't save, or cancel. Signals: save_as_signal: Emitted when the user chooses to save the pipeline. do_not_save_signal: Emitted when the user chooses not to save the pipeline. cancel_signal: Emitted when the user cancels the closing action. Attributes: bool_save_as (bool): Indicates if the pipeline should be saved under a new name. bool_exit (bool): Indicates if the editor can be closed. pipeline_name (str): Name of the pipeline being edited. .. Methods: - _connect_signals: Connect button signals to their respective slots - _setup_ui: Set up the dialog's user interface - can_exit: returns the value of bool_exit - cancel_clicked: makes the actions to cancel the action - do_not_save_clicked: makes the actions not to save the pipeline - save_as_clicked: makes the actions to save the pipeline """ save_as_signal = QtCore.pyqtSignal() do_not_save_signal = QtCore.pyqtSignal() cancel_signal = QtCore.pyqtSignal()
[docs] def __init__(self, pipeline_name): """ Initialize the dialog with the pipeline name. :param pipeline_name (str): Name of the pipeline (basename). """ super().__init__() self.pipeline_name = pipeline_name self.bool_exit = False self.bool_save_as = False self._setup_ui() self._connect_signals()
def _connect_signals(self): """ Connect button signals to their respective slots. """ self.push_button_save_as.clicked.connect(self.save_as_clicked) self.push_button_do_not_save.clicked.connect(self.do_not_save_clicked) self.push_button_cancel.clicked.connect(self.cancel_clicked) def _setup_ui(self): """ Set up the dialog's user interface. """ self.setWindowTitle("Confirm pipeline closing") label = QLabel( f"Do you want to close the pipeline without " f"saving {self.pipeline_name}?", self, ) self.push_button_save_as = QPushButton("Save", self) self.push_button_do_not_save = QPushButton("Do not save", self) self.push_button_cancel = QPushButton("Cancel", self) # Create button layout button_layout = QHBoxLayout() button_layout.addStretch(1) button_layout.addWidget(self.push_button_save_as) button_layout.addWidget(self.push_button_do_not_save) button_layout.addWidget(self.push_button_cancel) button_layout.addStretch(1) # Create main layout main_layout = QVBoxLayout() main_layout.addWidget(label) main_layout.addLayout(button_layout) self.setLayout(main_layout)
[docs] def can_exit(self): """Check if the editor can be closed. Return (bool): True if the editor can be closed, False otherwise. """ return self.bool_exit
[docs] def cancel_clicked(self): """Handle the cancel button click. Sets bool_exit to False, emits cancel_signal, and closes the dialog. """ self.bool_exit = False self.cancel_signal.emit() self.close()
[docs] def do_not_save_clicked(self): """Handle the 'Do not save' button click. Sets bool_exit to True, emits do_not_save_signal, and closes the dialog. """ self.bool_exit = True self.do_not_save_signal.emit() self.close()
[docs] def save_as_clicked(self): """Handle the 'Save' button click. Sets bool_save_as and bool_exit to True, emits save_as_signal, and closes the dialog. """ self.bool_save_as = True self.bool_exit = True self.save_as_signal.emit() self.close()
[docs] class PopUpDataBrowserCurrentSelection(QDialog): """ Dialog to display and confirm the current data browser selection. This dialog shows a table of the currently selected document from the data browser and allows the user to confirm or cancel the selection. When confirmed, it updates the scan_list attribute of relevant components in the main window. Attributes: project: Current project in the software. databrowser: Data browser instance of the software. filter: List of the current documents in the data browser. main_window: Main window of the software. .. Methods: - _set_dialog_size: Set the dialog size based on screen resolution - _setup_ui: Set up the dialog's user interface - ok_clicked: updates the "scan_list" attribute of several widgets """
[docs] def __init__(self, project, databrowser, filter, main_window): """ Initialize the dialog with the current project and selection data. :param project: Current project in the software :param databrowser: Data browser instance of the software :param filter (list): List of the current documents in the data browser :param main_window: Main window of the software """ super().__init__() self.project = project self.databrowser = databrowser self.filter = filter self.main_window = main_window self._setup_ui()
def _set_dialog_size(self): """Set the dialog size based on screen resolution.""" screen_resolution = QApplication.instance().desktop().screenGeometry() width, height = screen_resolution.width(), screen_resolution.height() self.setMinimumWidth(round(0.5 * width)) self.setMinimumHeight(round(0.8 * height)) def _setup_ui(self): """Set up the dialog's user interface.""" self.setWindowTitle("Confirm the selection") self.setModal(True) # Create layout layout = QVBoxLayout() # Create and configure data browser table databrowser_table = data_browser.TableDataBrowser( self.project, self.databrowser, [TAG_FILENAME], False, False ) old_scan_list = databrowser_table.scans_to_visualize databrowser_table.scans_to_visualize = self.filter databrowser_table.update_visualized_rows(old_scan_list) # Create buttons buttons = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) buttons.accepted.connect(self.ok_clicked) buttons.rejected.connect(self.close) # Add widgets to layout layout.addWidget(databrowser_table) layout.addWidget(buttons) self.setLayout(layout) # Set dialog size based on screen resolution self._set_dialog_size()
[docs] def ok_clicked(self): """ Update the scan_list attribute of components when OK is clicked. This method propagates the current filter (selected documents) to various components in the main window's pipeline manager, and marks the data as sent in the data browser before closing the dialog. """ # Update scan_list in all required components pipeline_manager = self.main_window.pipeline_manager components = [ pipeline_manager, pipeline_manager.nodeController, pipeline_manager.pipelineEditorTabs, pipeline_manager.iterationTable, ] for component in components: component.scan_list = self.filter self.databrowser.data_sent = True self.close()
[docs] class PopUpDeletedProject(QMessageBox): """ Message box that displays a list of deleted, renamed, or moved projects. This dialog appears when the software starts and detects that previously available projects are no longer accessible at their expected locations. Attributes: deleted_projects (list): List of project names that are no longer accessible. .. Methods: - _connect_signals: Connect button signals to their respective slots - _setup_message_box: Configure the message box appearance and content """
[docs] def __init__(self, deleted_projects): """ Initialize the message box with a list of inaccessible projects. :param deleted_projects (list): List of project names that are no longer accessible (deleted, renamed, or moved). """ super().__init__() self.deleted_projects = deleted_projects self._setup_message_box() self._connect_signals() self.exec()
def _connect_signals(self): """Connect button signals to their respective slots.""" self.buttonClicked.connect(self.close) def _setup_message_box(self): """Configure the message box appearance and content.""" # Create the message with the list of deleted projects project_list = "\n".join( f"- {project}" for project in self.deleted_projects ) message = ( f"These projects have been renamed, " f"moved or deleted:\n{project_list}" ) # Set message box properties self.setIcon(QMessageBox.Warning) self.setText("Deleted projects") self.setInformativeText(message) self.setWindowTitle("Warning") self.setStandardButtons(QMessageBox.Ok)
[docs] class PopUpDeleteProject(QDialog): """ Dialog for deleting selected projects. Allows the user to select and delete one or more projects from the projects directory after confirmation. .. Methods: - _delete_project: Handles project deletion - _setup_buttons: Creates and configures the dialog buttons - _setup_layout: Configures the main layout of the dialog - _setup_ui: Sets up the user interface components - ok_clicked: delete the selected projects after confirmation """
[docs] def __init__(self, main_window): """ Initializes the delete project dialog. :param main_window (QMainWindow): The main application window """ super().__init__() self.setWindowTitle("Delete project") config = Config() self.project_path = config.get_projects_save_path() self.main_window = main_window self._setup_ui()
def _delete_project(self, project_path, opened_projects): """ Handles project deletion, updating application state accordingly. :param project_path (str): The path of the project to delete :param opened_projects (list): The list of currently opened projects """ if os.path.abspath(self.main_window.project.folder) == os.path.abspath( project_path ): self.main_window.project = Project(None, True) self.main_window.update_project("") project_rel_path = os.path.relpath(project_path) if project_rel_path in self.main_window.saved_projects.pathsList: self.main_window.saved_projects.removeSavedProject( project_rel_path ) self.main_window.update_recent_projects_actions() if project_rel_path in opened_projects: opened_projects.remove(project_rel_path) shutil.rmtree(project_path) def _setup_buttons(self): """Creates and configures the dialog buttons.""" self.h_box_bottom = QHBoxLayout() self.h_box_bottom.addStretch(1) self.push_button_ok = QPushButton("OK") self.push_button_ok.setObjectName("pushButton_ok") self.push_button_ok.clicked.connect(self.ok_clicked) self.h_box_bottom.addWidget(self.push_button_ok) self.push_button_cancel = QPushButton("Cancel") self.push_button_cancel.setObjectName("pushButton_cancel") self.push_button_cancel.clicked.connect(self.close) self.h_box_bottom.addWidget(self.push_button_cancel) def _setup_layout(self): """Configures the main layout of the dialog.""" self.scroll = QScrollArea() self.widget = QWidget() self.widget.setLayout(self.v_box) self.scroll.setWidget(self.widget) self.final = QVBoxLayout() self.final.addWidget(self.scroll) self.final_layout = QVBoxLayout() self.final_layout.addLayout(self.final) self.final_layout.addLayout(self.h_box_bottom) self.setLayout(self.final_layout) def _setup_ui(self): """Sets up the user interface components.""" self.v_box = QVBoxLayout() self.v_box.addWidget(QLabel("Select projects to delete:")) self.check_boxes = [ QCheckBox(project) for project in os.listdir(self.project_path) if os.path.isdir(os.path.join(self.project_path, project)) ] for check_box in self.check_boxes: self.v_box.addWidget(check_box) self._setup_buttons() self._setup_layout()
[docs] def ok_clicked(self): """ Deletes the selected projects after user confirmation. """ selected_projects = [ cb.text() for cb in self.check_boxes if cb.isChecked() ] if not selected_projects: self.accept() self.close() return config = Config() opened_projects = config.get_opened_projects() reply = None for name in selected_projects: project_path = os.path.join(self.project_path, name) if reply not in (QMessageBox.YesToAll, QMessageBox.NoToAll): reply = QMessageBox.warning( self, "populse_mia - Warning: Delete project", f"Do you really want to delete the {name} project?", QMessageBox.Yes | QMessageBox.No | QMessageBox.YesToAll | QMessageBox.NoToAll, ) if reply in (QMessageBox.Yes, QMessageBox.YesToAll): self._delete_project(project_path, opened_projects) config.set_opened_projects(opened_projects) config.saveConfig() self.accept() self.close()
[docs] class PopUpFilterSelection(QDialog): """ Dialog for selecting a previously saved filter. :Methods: - cancel_clicked: Closes the pop-up. - ok_clicked: Handles actions when the "OK" button is clicked. - search_str: Filters the list based on the search input. """
[docs] def __init__(self, project): """ Initializes the pop-up dialog. :param project (object): The current project containing saved filters """ super().__init__() self.project = project self.setModal(True) _translate = QtCore.QCoreApplication.translate # Label for the filter list self.label_filter_list = QLabel( _translate("main_window", "Available filters:"), self ) self.label_filter_list.setTextFormat(QtCore.Qt.AutoText) self.label_filter_list.setObjectName("label_filter_list") # Search bar for filtering the list self.search_bar = QLineEdit(self) self.search_bar.setObjectName("lineEdit_search_bar") self.search_bar.setPlaceholderText("Search") self.search_bar.textChanged.connect(self.search_str) # List widget for displaying filters self.list_widget_filters = QListWidget(self) self.list_widget_filters.setObjectName("listWidget_tags") self.list_widget_filters.setSelectionMode( QAbstractItemView.SingleSelection ) # OK and Cancel buttons self.push_button_ok = QPushButton("OK", self) self.push_button_ok.setObjectName("pushButton_ok") self.push_button_ok.clicked.connect(self.ok_clicked) self.push_button_cancel = QPushButton("Cancel", self) self.push_button_cancel.setObjectName("pushButton_cancel") self.push_button_cancel.clicked.connect(self.cancel_clicked) # Layout setup hbox_top_left = QHBoxLayout() hbox_top_left.addWidget(self.label_filter_list) hbox_top_left.addWidget(self.search_bar) vbox_top_left = QVBoxLayout() vbox_top_left.addLayout(hbox_top_left) vbox_top_left.addWidget(self.list_widget_filters) hbox_buttons = QHBoxLayout() hbox_buttons.addStretch(1) hbox_buttons.addWidget(self.push_button_ok) hbox_buttons.addWidget(self.push_button_cancel) vbox_final = QVBoxLayout() vbox_final.addLayout(vbox_top_left) vbox_final.addLayout(hbox_buttons) self.setLayout(vbox_final)
[docs] def cancel_clicked(self): """Closes the pop-up dialog.""" self.close()
[docs] def ok_clicked(self): """ Handles actions when the "OK" button is clicked. This method should be overridden in subclasses to implement specific behavior. """ pass
[docs] def search_str(self, search_text): """ Filters the list of saved filters based on the search input. :param search_text (str): The text pattern to search for """ search_text = search_text.strip().upper() if search_text: matching_filters = [ f.name for f in self.project.filters if search_text in f.name.upper() ] else: matching_filters = [f.name for f in self.project.filters] for idx in range(self.list_widget_filters.count()): item = self.list_widget_filters.item(idx) item.setHidden(item.text() not in matching_filters)
[docs] class PopUpInformation(QWidget): """ Popup window displaying the current project's information. """ # Signal emitted when project preferences change signal_preferences_change = QtCore.pyqtSignal()
[docs] def __init__(self, project): """ Initializes the popup window with project details. :param project (Project): The current project instance """ super().__init__() # UI Elements name_label = QLabel("Name: ") self.name_value = QLineEdit(project.getName()) folder_label = QLabel(f"Root folder: {project.folder}") date_label = QLabel(f"Date of creation: {project.getDate()}") # Layout Setup box = QVBoxLayout() row = QHBoxLayout() row.addWidget(name_label) row.addWidget(self.name_value) box.addLayout(row) box.addWidget(folder_label) box.addWidget(date_label) box.addStretch(1) self.setLayout(box)
[docs] class PopUpInheritanceDict(QDialog): """ Dialog for selecting tag inheritance between input and output plugs. This dialog allows users to select which input plug should pass its tags to a specific output plug, or to choose to ignore tag inheritance altogether. :Methods: - _setup_buttons: Set up action buttons for the dialog - _setup_radio_buttons: Set up radio buttons for each input option - ok_clicked: Event when ok button is clicked - okall_clicked: Event when Ok all button is clicked - on_clicked: Event when radiobutton is clicked - ignoreall_clicked: Event when ignore all plugs button is clicked - ignore_clicked: Event when ignore button is clicked - ignore_node_clicked: Event when ignore all nodes button is clicked """
[docs] def __init__(self, values, node_name, plug_name, iterate): """ Initialize the inheritance selection dialog. :param values (dict): Dict mapping input names (keys) to their paths (values) :param node_name (str): Name of the current node :param plug_name (str): Name of the current output plug :param iterate (bool): Boolean indicating if the choice applies to iterations """ super().__init__() self.setModal(True) self.setObjectName("Dialog") self.setWindowTitle(f"Plug inherited in {node_name}") # Initialize state flags self.ignore = False self.all = False self.everything = False # Create the main layout v_box_values = QVBoxLayout() label = ( f"In the node <b><i>{node_name}</i></b>, from which input plug, " f"the output plug <b><i>{plug_name}</i></b> should inherit " f"the tags?:" ) v_box_values.addWidget(QLabel(label)) # Add radio buttons for each input option self._setup_radio_buttons(values, v_box_values) # Add button row h_box_buttons = QHBoxLayout() self._setup_buttons(node_name, plug_name, h_box_buttons) v_box_values.addLayout(h_box_buttons) # Add ignore node button self.push_button_ignore_node = QPushButton( "Ignore for all nodes in the pipeline", self ) self.push_button_ignore_node.clicked.connect(self.ignore_node_clicked) self.push_button_ignore_node.setToolTip( "No tags will be inherited for the whole pipeline." ) v_box_values.addWidget(self.push_button_ignore_node) # Add iteration notice if needed if iterate: label = "<i>These choices will be valid for each iteration.</i>" v_box_values.addWidget(QLabel(label)) self.setLayout(v_box_values)
def _setup_buttons(self, node_name, plug_name, layout): """Set up action buttons for the dialog. :param node_name: Name of the current node :param plug_name: Name of the current output plug :param layout: Layout to add the buttons to """ # OK button self.push_button_ok = QPushButton("OK", self) self.push_button_ok.clicked.connect(self.ok_clicked) self.push_button_ok.setToolTip( f"<i>{plug_name}</i> will inherit tags from {self.key}." ) layout.addWidget(self.push_button_ok) # Ignore button self.push_button_ignore = QPushButton("Ignore", self) self.push_button_ignore.clicked.connect(self.ignore_clicked) self.push_button_ignore.setToolTip( f"<i>{plug_name}</i> will not inherit any tags." ) layout.addWidget(self.push_button_ignore) # OK all button self.push_button_okall = QPushButton("OK for all output plugs", self) self.push_button_okall.clicked.connect(self.okall_clicked) self.push_button_okall.setToolTip( f"All the output plugs from <i>{node_name}</i> " f"will inherit tags from {self.key}." ) layout.addWidget(self.push_button_okall) # Ignore all button self.push_button_ignoreall = QPushButton( "Ignore for all output plugs", self ) self.push_button_ignoreall.clicked.connect(self.ignoreall_clicked) self.push_button_ignoreall.setToolTip( f"All the output plugs from <i>{node_name}</i> will not " f"inherit any tags." ) layout.addWidget(self.push_button_ignoreall) def _setup_radio_buttons(self, values, layout): """Set up radio buttons for each input option. :param values: Dict mapping input names to their paths :param layout: Layout to add the radio buttons to """ first_button = True for key, value in values.items(): radiobutton = QRadioButton(key, self) radiobutton.value = value radiobutton.key = key radiobutton.setChecked(first_button) if first_button: self.value = value self.key = key first_button = False radiobutton.toggled.connect(self.on_clicked) layout.addWidget(radiobutton) layout.addStretch(1)
[docs] def ok_clicked(self): """ Handle OK button click. Accepts the dialog with current selection applied to current plug. """ self.accept() self.close()
[docs] def okall_clicked(self): """ Handle 'OK for all output plugs' button click. Accepts the dialog with current selection applied to all output plugs. """ self.all = True self.accept() self.close()
[docs] def on_clicked(self): """ Handle radio button selection event. Updates the currently selected input value and key. """ radiobutton = self.sender() self.value = radiobutton.value self.key = radiobutton.key # Update OK button tooltips with new selection self.push_button_ok.setToolTip( f"Output plug will inherit tags from {self.key}." ) self.push_button_okall.setToolTip( f"All output plugs will inherit tags from {self.key}." )
[docs] def ignoreall_clicked(self): """ Handle 'Ignore for all output plugs' button click. Accepts the dialog with tag inheritance ignored for all output plugs. """ self.ignore = True self.all = True self.accept() self.close()
[docs] def ignore_clicked(self): """ Handle Ignore button click. Accepts the dialog with tag inheritance ignored for current plug. """ self.ignore = True self.accept() self.close()
[docs] def ignore_node_clicked(self): """ Handle 'Ignore for all nodes in the pipeline' button click. Accepts the dialog with tag inheritance ignored for the entire pipeline. """ self.ignore = True self.all = True self.everything = True self.accept() self.close()
[docs] class PopUpMultipleSort(QDialog): """ Dialog for sorting the data browser's table based on multiple tags. This dialog allows users to select multiple tags (columns) for sorting table data in either ascending or descending order. Users can dynamically add or remove sort criteria. .. Methods: - _setup_ui_elements: Create and configure all UI elements - add_tag: Adds a push button - fill_values: Fills the values list when a tag is added or removed - refresh_layout: Updates the layouts (especially when a tag push button is added or removed) - remove_tag: Removes a push buttons and makes the changes in the list of values - select_tag: Calls a pop-up to choose a tag - sort_scans: Collects the information and send them to the data browser """
[docs] def __init__(self, project, table_data_browser): """ Initialize the multiple sort dialog. :param project: Current project in the software :param table_data_browser: Data browser's table to be sorted """ super().__init__() self.project = project self.table_data_browser = table_data_browser self.setModal(True) self.setWindowTitle("Multiple Sort") # Initialize data structures # Contains unique values for each selected tag self.values_list = [[], []] # Will hold the final list of tags for sorting self.list_tags = [] # Buttons for tag selection self.push_buttons = [] # Create UI elements self._setup_ui_elements() # Create and set layout self.v_box_final = QVBoxLayout() self.setLayout(self.v_box_final) self.refresh_layout()
def _setup_ui_elements(self): """Create and configure all UI elements.""" # Tag selection section self.label_tags = QLabel("Tags: ") # Create initial tag buttons push_button_tag_1 = QPushButton("Tag n°1") push_button_tag_1.clicked.connect(lambda: self.select_tag(0)) push_button_tag_2 = QPushButton("Tag n°2") push_button_tag_2.clicked.connect(lambda: self.select_tag(1)) self.push_buttons = [push_button_tag_1, push_button_tag_2] # Add/remove tag controls sources_images_dir = Config().getSourceImageDir() # Remove tag button self.remove_tag_label = ClickableLabel() remove_tag_picture = QtGui.QPixmap( os.path.relpath(os.path.join(sources_images_dir, "red_minus.png")) ).scaledToHeight(20) self.remove_tag_label.setPixmap(remove_tag_picture) self.remove_tag_label.clicked.connect(self.remove_tag) # Add tag button self.add_tag_label = ClickableLabel() self.add_tag_label.setObjectName("plus") add_tag_picture = QtGui.QPixmap( os.path.relpath(os.path.join(sources_images_dir, "green_plus.png")) ).scaledToHeight(15) self.add_tag_label.setPixmap(add_tag_picture) self.add_tag_label.clicked.connect(self.add_tag) # Sort order selection self.combo_box = QComboBox() self.combo_box.addItems(["Ascending", "Descending"]) # Sort button self.push_button_sort = QPushButton("Sort Scans") self.push_button_sort.clicked.connect(self.sort_scans)
[docs] def add_tag(self): """Add a new tag button to the sort criteria.""" button_index = len(self.push_buttons) push_button = QPushButton(f"Tag n°{button_index + 1}") push_button.clicked.connect(lambda: self.select_tag(button_index - 1)) self.push_buttons.insert(button_index, push_button) self.refresh_layout()
[docs] def fill_values(self, idx): """ Collect unique values for the selected tag. :param idx: Index of the tag button in the push_buttons list """ tag_name = self.push_buttons[idx].text() # Ensure values_list has enough slots while len(self.values_list) <= idx: self.values_list.append([]) # Clear existing values for this tag self.values_list[idx] = [] with self.project.database.data() as database_data: for scan in database_data.get_document_names(COLLECTION_CURRENT): current_value = database_data.get_value( collection_name=COLLECTION_CURRENT, primary_key=scan, field=tag_name, ) if current_value not in self.values_list[idx]: self.values_list[idx].append(current_value)
[docs] def refresh_layout(self): """ Update the dialog layout to reflect current tag buttons. """ self.h_box_top = QHBoxLayout() self.h_box_top.setSpacing(10) self.h_box_top.addWidget(self.label_tags) # Add all tag buttons for tag_button in self.push_buttons: self.h_box_top.addWidget(tag_button) # Add control buttons self.h_box_top.addWidget(self.add_tag_label) self.h_box_top.addWidget(self.remove_tag_label) self.h_box_top.addWidget(self.combo_box) self.h_box_top.addWidget(self.push_button_sort) self.h_box_top.addStretch(1) # Clear the previous layout if it exists if self.v_box_final.count() > 0: # Remove the previous h_box_top item = self.v_box_final.takeAt(0) if item.layout(): while item.layout().count(): item.layout().takeAt(0) self.v_box_final.addLayout(self.h_box_top)
[docs] def remove_tag(self): """ Remove the last tag button from the sort criteria. """ if not self.push_buttons: return # Remove the last button push_button = self.push_buttons.pop() push_button.deleteLater() push_button = None # Remove corresponding values if self.values_list: self.values_list.pop() self.refresh_layout()
[docs] def select_tag(self, idx): """ Open a pop-up dialog to choose a tag for the specified button. :param idx: Index of the button in the push_buttons list """ with self.project.database.data() as database_data: pop_up = PopUpSelectTagCountTable( self.project, database_data.get_shown_tags(), self.push_buttons[idx].text(), ) if pop_up.exec_(): self.push_buttons[idx].setText(pop_up.selected_tag) self.fill_values(idx)
[docs] def sort_scans(self): """Collect sorting parameters and send them to the data browser.""" self.order = self.combo_box.itemText(self.combo_box.currentIndex()) # Clear previous tag list self.list_tags = [] # Collect valid tag names with self.project.database.data() as database_data: field_names = database_data.get_field_names(COLLECTION_CURRENT) for push_button in self.push_buttons: tag_name = push_button.text() if tag_name in field_names: self.list_tags.append(tag_name) # Close dialog and trigger sorting self.accept() self.table_data_browser.multiple_sort_infos(self.list_tags, self.order)
[docs] class PopUpNewProject(QFileDialog): """ Dialog for creating a new project. This dialog is displayed when the user wants to create a new project. It manages file selection and handles the creation process. .. Method: - get_filename: Sets the widget's attributes depending on the selected file name .. Signals: - signal_create_project: Emitted when a project has been successfully created """ # Signal emitted when project creation is successful signal_create_project = QtCore.pyqtSignal()
[docs] def __init__(self): """Initialize the new project dialog with appropriate settings.""" # Import here to prevent circular import issues from populse_mia.utils import set_projects_directory_as_default super().__init__() self.setLabelText(QFileDialog.Accept, "Create") self.setAcceptMode(QFileDialog.AcceptSave) # Set the projects directory as default set_projects_directory_as_default(self)
[docs] def get_filename(self, file_name_tuple): """ Process the selected filename and set up project attributes. :param file_name_tuple (tuple): Tuple containing the selected filename(s), obtained from the selectedFiles method Note: If the file already exists, displays an error message. Otherwise, closes the dialog and emits signal_create_project. """ # Import here to prevent circular import issues from populse_mia.utils import message_already_exists # Handle the case where file_name_tuple might be empty if not file_name_tuple: return "" file_name = file_name_tuple[0] if not file_name: return "" entire_path = os.path.abspath(file_name) self.path, self.name = os.path.split(entire_path) self.relative_path = os.path.relpath(file_name) self.relative_subpath = os.path.relpath(self.path) # Check if project already exists if os.path.exists(self.relative_path): message_already_exists() else: self.close() # Emit signal to notify that project has been created self.signal_create_project.emit() return file_name
[docs] class PopUpOpenProject(QFileDialog): """ Dialog for opening an existing MIA project. This dialog allows users to select an existing project directory from the filesystem. It uses the default projects directory as the starting location and emits a signal when a valid project is selected. .. Method: - get_filename: Sets the widget's attributes depending on the selected file name .. Signals: - signal_create_project: Signal emitted when a valid project is selected """ # Signal emitted when a valid project is selected signal_create_project = QtCore.pyqtSignal()
[docs] def __init__(self): # Import here to prevent circular imports from populse_mia.utils import set_projects_directory_as_default super().__init__() self.setOption(QFileDialog.DontUseNativeDialog, True) self.setFileMode(QFileDialog.Directory) # Set the projects directory as default location set_projects_directory_as_default(self)
[docs] def get_filename(self, file_name_tuple): """ Process the selected directory and emit signal if valid. Sets the path, name, and relative_path attributes based on the selected directory. If the directory exists, emits signal_create_project. :param file_name_tuple (tuple): Tuple containing selected directory path(s). Typically obtained from selectedFiles() method. """ # Import here to prevent circular imports from populse_mia.utils import message_already_exists if not file_name_tuple: return file_name = file_name_tuple[0] if not file_name: return entire_path = os.path.abspath(file_name) self.path, self.name = os.path.split(entire_path) self.relative_path = os.path.relpath(file_name) # Check if directory exists and emit signal if os.path.exists(entire_path): self.close() self.signal_create_project.emit() else: message_already_exists()
[docs] class PopUpPreferences(QDialog): """ Dialog for changing software preferences. This class manages the preferences dialog for the software, allowing users to configure various settings related to tools, projects, and appearance. .. Methods: - admin_mode_switch: Called when the admin mode checkbox is clicked - browse_afni: Called when afni browse button is clicked - browse_ants: Called when ants browse button is clicked - browse_freesurfer: Browse for the FreeSurfer env file - browse_fsl: Called when fsl browse button is clicked - browse_matlab: Called when matlab browse button is clicked - browse_matlab_standalone: Called when matlab browse button is clicked - browse_mri_conv_path: Called when "MRIManager.jar" browse button is clicked - browse_mrtrix: Called when mrtrix browse button is clicked - browse_projects_save_path: Called when "Projects folder" browse button is clicked - browse_resources_path: Called when "resources" browse button is clicked - browse_spm: Called when spm browse button is clicked - browse_spm_standalone: Called when spm standalone browse button is clicked - change_admin_psswd: Method to change the admin password - control_checkbox_toggled: Check before changing controller version - create_afni_group: Create the AFNI group box - create_ants_group: Create the ANTS group box - create_appearance_tab: Create the 'Appearance' tab - create_capsul_group: Create the CAPSUL group box - create_freesurfer_group: Create the FreeSurfer group box - create_fsl_group: Create the FSL group box - create_global_preferences: Create the global preferences group box - create_horizontal_box: Create a horizontal box layout with the given widgets - create_matlab_group: Create the Matlab group box - create_mrtrix_group: Create the mrtrix group box - create_pipeline_tab: Create the 'Pipeline' tab - create_projects_preferences: Create the projects preferences group box - create_populse_preferences: Create the Populse third party preferences group box - create_resources_preferences: Create the external resources preferences group box - create_spm_group: Create the SPM group box - create_tools_tab: Create the 'Tools' tab - edit_capsul_config: Capsul engine edition - edit_config_file: Create a window to view, edit the mia configuration file - findChar: Highlights characters in red when using the Find button when editing configuration - load_config: Load the configuration settings - ok_clicked: Saves the modifications to the config file and apply them - save_minimal_config: Save minimal configuration for CAPSUL config sync purposes - save_full_config: Save the full configuration and validate settings - setup_ui: Set up the user interface components - show_error_message: Show an error message dialog - show_warning_message: Show a warning message dialog - update_gui_from_capsul_config: Update the GUI based on the CAPSUL configuration - use_afni_changed: Called when the use_afni checkbox is changed - use_ants_changed: Called when the use_ants checkbox is changed - use_current_mainwindow_size: Use the current main window size - use_freesurfer_changed: Handle the use_freesurfer checkbox change event - use_fsl_changed: Called when the use_fsl checkbox is changed - use_matlab_changed: Called when the use_matlab checkbox is changed - use_matlab_standalone_changed: Called when the use_matlab_standalone checkbox is changed - use_mrtrix_changed: Called when the use_mrtrix checkbox is changed - use_spm_changed: Called when the use_spm checkbox is changed - use_spm_standalone_changed: Called when the use_spm_standalone checkbox is changed - validate_and_save: Validate and save the preferences - validate_matlab_path: Validate the Matlab path - validate_matlab_standalone_path: Validate the Matlab standalone path - validate_paths: Validate the paths and settings - validate_spm_path: Validate the SPM path - validate_spm_standalone_path: Validate the SPM standalone path - validate_tool_path: Validate the tool path - wrong_path: Show a wrong path message .. Signals: - signal_preferences_change: Signal emitted when the preferences are successfully validated and saved - use_clinical_mode_signal: Signal emitted when the clinical mode is enabled - not_use_clinical_mode_signal: Signal emitted when the clinical mode is disabled """ signal_preferences_change = QtCore.pyqtSignal() use_clinical_mode_signal = QtCore.pyqtSignal() not_use_clinical_mode_signal = QtCore.pyqtSignal()
[docs] def __init__(self, main_window): """ Initialize the preferences dialog. :param main_window: The main window object of the software """ super().__init__() self.setModal(True) self.main_window = main_window self.clicked = 0 self.salt = "P0pulseM1@" self.setup_ui() self.load_config() # Disabling widgets self.use_spm_changed() self.use_matlab_changed() self.use_matlab_standalone_changed() self.use_spm_standalone_changed() self.use_fsl_changed() self.use_afni_changed() self.use_ants_changed() self.use_freesurfer_changed() self.use_mrtrix_changed() # Connects the change of state event for a checkbox self.use_matlab_checkbox.stateChanged.connect(self.use_matlab_changed) self.use_matlab_standalone_checkbox.stateChanged.connect( self.use_matlab_standalone_changed ) self.use_spm_checkbox.stateChanged.connect(self.use_spm_changed) self.use_spm_standalone_checkbox.stateChanged.connect( self.use_spm_standalone_changed ) self.use_fsl_checkbox.stateChanged.connect(self.use_fsl_changed) self.use_afni_checkbox.stateChanged.connect(self.use_afni_changed) self.use_ants_checkbox.stateChanged.connect(self.use_ants_changed) self.use_mrtrix_checkbox.stateChanged.connect(self.use_mrtrix_changed) self.use_freesurfer_checkbox.stateChanged.connect( self.use_freesurfer_changed )
[docs] def admin_mode_switch(self): """Handle the admin mode checkbox click event.""" config = Config() if self.admin_mode_checkbox.isChecked(): psswd, ok = QInputDialog.getText( self, "Password Input Dialog", "Enter the admin password:", QLineEdit.Password, ) if ok: salt_psswd = f"{self.salt}{psswd}" hash_psswd = hashlib.sha256(salt_psswd.encode()).hexdigest() if hash_psswd != config.get_admin_hash(): self.admin_mode_checkbox.setChecked(False) self.status_label.setText( "<i style='color:red'>Wrong password.</i>" ) else: self.change_psswd.setVisible(True) self.edit_config.setVisible(True) self.status_label.clear() else: self.admin_mode_checkbox.setChecked(False) else: self.change_psswd.setVisible(False) self.edit_config.setVisible(False)
[docs] def browse_afni(self): """Browse for the AFNI directory.""" fname = QFileDialog.getExistingDirectory( self, "Choose AFNI directory", os.path.expanduser("~") ) if fname: self.afni_choice.setText(fname)
[docs] def browse_ants(self): """Browse for the ANTS directory.""" fname = QFileDialog.getExistingDirectory( self, "Choose ANTS directory", os.path.expanduser("~") ) if fname: self.ants_choice.setText(fname)
[docs] def browse_freesurfer(self): """Browse for the FreeSurfer env file.""" fname = QFileDialog.getOpenFileName( self, "Choose freesurfer env file", os.path.expanduser("~") )[0] if fname: self.freesurfer_choice.setText(fname)
[docs] def browse_fsl(self): """Browse for the FSL config file.""" fname = QFileDialog.getOpenFileName( self, "Choose FSL config file", os.path.expanduser("~") )[0] if fname: self.fsl_choice.setText(fname)
[docs] def browse_matlab(self): """Browse for the Matlab file.""" fname = QFileDialog.getOpenFileName( self, "Choose Matlab file", os.path.expanduser("~") )[0] if fname: self.matlab_choice.setText(fname)
[docs] def browse_matlab_standalone(self): """Browse for the Matlab standalone directory.""" fname = QFileDialog.getExistingDirectory( self, "Choose MCR directory", os.path.expanduser("~") ) if fname: self.matlab_standalone_choice.setText(fname)
[docs] def browse_mri_conv_path(self): """Browse for the MRIFileManager.jar file.""" fname = QFileDialog.getOpenFileName( self, "Select the location of the MRIManager.jar file", os.path.expanduser("~"), )[0] if fname: self.mri_conv_path_line_edit.setText(fname)
[docs] def browse_mrtrix(self): """Browse for the mrtrix directory.""" fname = QFileDialog.getExistingDirectory( self, "Choose mrtrix directory", os.path.expanduser("~") ) if fname: self.mrtrix_choice.setText(fname)
[docs] def browse_projects_save_path(self): """Browse for the projects folder.""" fname = QFileDialog.getExistingDirectory( self, "Select a folder where to save the projects", os.path.expanduser("~"), ) if fname: self.projects_save_path_line_edit.setText(fname) with open(os.path.join(fname, ".gitignore"), "w") as myFile: myFile.write("/*")
[docs] def browse_resources_path(self): """Browse for the resources folder.""" fname = QFileDialog.getExistingDirectory( self, "Select the location of the external resources folder", os.path.expanduser("~"), ) if fname: self.resources_path_line_edit.setText(fname)
[docs] def browse_spm(self): """Browse for the SPM directory.""" fname = QFileDialog.getExistingDirectory( self, "Choose SPM directory", os.path.expanduser("~") ) if fname: self.spm_choice.setText(fname)
[docs] def browse_spm_standalone(self): """Browse for the SPM standalone directory.""" fname = QFileDialog.getExistingDirectory( self, "Choose SPM standalone directory", os.path.expanduser("~") ) if fname: self.spm_standalone_choice.setText(fname)
[docs] def change_admin_psswd(self, status): """ Open a dialog to change the admin password with validation checks. :param status (str): Initial status message to display in the dialog. """ change = QDialog() change.old_psswd = QLineEdit() change.new_psswd = QLineEdit() change.new_psswd_conf = QLineEdit() status = f"<i>{status}</i>" change.status = QLabel(status) change.status.setStyleSheet("color:red") change.old_psswd.setEchoMode(QLineEdit.Password) change.new_psswd.setEchoMode(QLineEdit.Password) change.new_psswd_conf.setEchoMode(QLineEdit.Password) buttonBox = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self ) layout = QFormLayout() layout.addRow("Old password", change.old_psswd) layout.addRow("New password", change.new_psswd) layout.addRow("Confirm new password", change.new_psswd_conf) layout.addRow(change.status) layout.addWidget(buttonBox) buttonBox.accepted.connect(change.accept) buttonBox.rejected.connect(change.reject) change.setLayout(layout) event = change.exec() if not event: change.close() else: config = Config() old_psswd = f"{self.salt}{change.old_psswd.text()}" hash_psswd = hashlib.sha256(old_psswd.encode()).hexdigest() if ( hash_psswd == config.get_admin_hash() and change.new_psswd.text() == change.new_psswd_conf.text() and len(change.new_psswd.text()) > 6 ): new_psswd = f"{self.salt}{change.new_psswd.text()}" config.set_admin_hash( hashlib.sha256(new_psswd.encode()).hexdigest() ) elif hash_psswd != config.get_admin_hash(): self.change_admin_psswd("The old password is incorrect.") elif len(change.new_psswd.text()) <= 6: self.change_admin_psswd( "Your password must have more than 6 characters" ) elif change.new_psswd.text() != change.new_psswd_conf.text(): self.change_admin_psswd("The new passwords are not the same.")
[docs] def control_checkbox_toggled(self): """Check if the user really wants to change the controller version.""" self.control_checkbox.toggle() self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Warning) self.msg.setText("Controller version change") self.msg.setWindowTitle("Warning") self.msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) config = Config() if not self.control_checkbox_changed: self.msg.setInformativeText( f"To change the controller from " f"{'V1' if config.isControlV1() else 'V2'} to " f"{'V2' if config.isControlV1() else 'V1'}, MIA must be " f"restarted. Would you like to plan this change for next " f"start-up?" ) else: self.msg.setInformativeText( f"Change of controller from " f"{'V1' if config.isControlV1() else 'V2'} to " f"{'V2' if config.isControlV1() else 'V1'} is already " f"planned for next start-up. Would you like to cancel " f"this change?" ) return_value = self.msg.exec() if return_value == QMessageBox.Yes: self.control_checkbox_changed = not self.control_checkbox_changed self.main_window.set_controller_version() QApplication.restoreOverrideCursor()
[docs] def create_afni_group(self): """Create the AFNI group box.""" self.use_afni_label = QLabel("Use AFNI") self.use_afni_checkbox = QCheckBox("", self) self.afni_label = QLabel("AFNI path (e.g. dir_containing_abin/abin):") self.afni_choice = QLineEdit() self.afni_browse = QPushButton("Browse") self.afni_browse.clicked.connect(self.browse_afni) v_box_afni_path = QVBoxLayout() v_box_afni_path.addWidget(self.afni_label) v_box_afni_path.addLayout( self.create_horizontal_box( self.afni_choice, self.afni_browse, add_stretch=False ) ) v_box_afni = QVBoxLayout() v_box_afni.addLayout( self.create_horizontal_box( self.use_afni_checkbox, self.use_afni_label ) ) v_box_afni.addLayout(v_box_afni_path) self.groupbox_afni.setLayout(v_box_afni)
[docs] def create_ants_group(self): """Create the ANTS group box.""" self.use_ants_label = QLabel("Use ANTS") self.use_ants_checkbox = QCheckBox("", self) self.ants_label = QLabel("ANTS path (e.g. ANTs_dir/bin):") self.ants_choice = QLineEdit() self.ants_browse = QPushButton("Browse") self.ants_browse.clicked.connect(self.browse_ants) v_box_ants_path = QVBoxLayout() v_box_ants_path.addWidget(self.ants_label) v_box_ants_path.addLayout( self.create_horizontal_box( self.ants_choice, self.ants_browse, add_stretch=False ) ) v_box_ants = QVBoxLayout() v_box_ants.addLayout( self.create_horizontal_box( self.use_ants_checkbox, self.use_ants_label ) ) v_box_ants.addLayout(v_box_ants_path) self.groupbox_ants.setLayout(v_box_ants)
[docs] def create_appearance_tab(self, _translate): """ Create and configure the 'Appearance' tab with color and display settings. :param _translate (callable): Function used to translate UI text (translate method of QCoreApplication in the Qt framework.) """ self.tab_appearance = QWidget() self.tab_appearance.setObjectName("tab_appearance") self.tab_widget.addTab( self.tab_appearance, _translate("Dialog", "Appearance") ) colors = [ "Black", "Blue", "Green", "Grey", "Orange", "Red", "Yellow", "White", ] self.appearance_layout = QVBoxLayout() self.label_background_color = QLabel("Background color") self.background_color_combo = QComboBox(self) self.label_text_color = QLabel("Text color") self.text_color_combo = QComboBox(self) self.background_color_combo.addItem("") self.text_color_combo.addItem("") for color in colors: self.background_color_combo.addItem(color) self.text_color_combo.addItem(color) self.fullscreen_cbox = QCheckBox("Use full screen") self.mainwindow_size_x_spinbox = QSpinBox() self.mainwindow_size_x_spinbox.setMaximum( QApplication.instance().desktop().width() ) self.mainwindow_size_y_spinbox = QSpinBox() self.mainwindow_size_y_spinbox.setMaximum( QApplication.instance().desktop().height() ) self.mainwindow_size_button = QPushButton("use current size") self.mainwindow_size_button.clicked.connect( self.use_current_mainwindow_size ) mainwindow_size_lay = QHBoxLayout() mainwindow_size_lay.addWidget(QLabel("Main window size")) mainwindow_size_lay.addWidget(self.mainwindow_size_x_spinbox) mainwindow_size_lay.addWidget(QLabel(" x ")) mainwindow_size_lay.addWidget(self.mainwindow_size_y_spinbox) mainwindow_size_lay.addWidget(self.mainwindow_size_button) self.appearance_layout.addWidget(self.label_background_color) self.appearance_layout.addWidget(self.background_color_combo) self.appearance_layout.addWidget(self.label_text_color) self.appearance_layout.addWidget(self.text_color_combo) self.appearance_layout.addWidget(self.fullscreen_cbox) self.appearance_layout.addLayout(mainwindow_size_lay) self.appearance_layout.addStretch(1) self.tab_appearance.setLayout(self.appearance_layout)
[docs] def create_capsul_group(self, groupbox_capsul): """ Create and configure the CAPSUL group box with a configuration button. :param groupbox_capsul (QGroupBox): The group box to be configured for CAPSUL settings. """ capsul_config_button = QPushButton( "Edit CAPSUL config", default=False, autoDefault=False ) capsul_config_button.clicked.connect(self.edit_capsul_config) h_box_capsul = QHBoxLayout() h_box_capsul.addWidget(capsul_config_button) h_box_capsul.addStretch(1) v_box_capsul = QVBoxLayout() v_box_capsul.addLayout(h_box_capsul) groupbox_capsul.setLayout(v_box_capsul)
[docs] def create_freesurfer_group(self): """Create the FreeSurfer group box.""" self.use_freesurfer_label = QLabel("Use FreeSurfer") self.use_freesurfer_checkbox = QCheckBox("", self) self.freesurfer_label = QLabel( "FreeSurfer path (e.g. FreeSurfer_dir/FreeSurferEnv.sh):" ) self.freesurfer_choice = QLineEdit() self.freesurfer_browse = QPushButton("Browse") self.freesurfer_browse.clicked.connect(self.browse_freesurfer) v_box_freesurfer_path = QVBoxLayout() v_box_freesurfer_path.addWidget(self.freesurfer_label) v_box_freesurfer_path.addLayout( self.create_horizontal_box( self.freesurfer_choice, self.freesurfer_browse, add_stretch=False, ) ) v_box_freesurfer = QVBoxLayout() v_box_freesurfer.addLayout( self.create_horizontal_box( self.use_freesurfer_checkbox, self.use_freesurfer_label ) ) v_box_freesurfer.addLayout(v_box_freesurfer_path) self.groupbox_freesurfer.setLayout(v_box_freesurfer)
[docs] def create_fsl_group(self): """Create the FSL group box.""" self.use_fsl_label = QLabel("Use FSL") self.use_fsl_checkbox = QCheckBox("", self) self.fsl_label = QLabel( "FSL config file (e.g., fsl_dir/etc/fslconf/fsl.sh):" ) self.fsl_choice = QLineEdit() self.fsl_browse = QPushButton("Browse") self.fsl_browse.clicked.connect(self.browse_fsl) v_box_fsl_path = QVBoxLayout() v_box_fsl_path.addWidget(self.fsl_label) v_box_fsl_path.addLayout( self.create_horizontal_box( self.fsl_choice, self.fsl_browse, add_stretch=False ) ) v_box_fsl = QVBoxLayout() v_box_fsl.addLayout( self.create_horizontal_box( self.use_fsl_checkbox, self.use_fsl_label ) ) v_box_fsl.addLayout(v_box_fsl_path) self.groupbox_fsl.setLayout(v_box_fsl)
[docs] def create_global_preferences(self): """Create the global preferences group box.""" self.save_checkbox = QCheckBox("", self) self.save_label = QLabel("Auto save") self.clinical_mode_checkbox = QCheckBox("", self) self.clinical_mode_label = QLabel("Clinical mode") self.admin_mode_checkbox = QCheckBox("", self) self.admin_mode_checkbox.clicked.connect(self.admin_mode_switch) self.admin_mode_label = QLabel("Admin mode") self.change_psswd = QPushButton( "Change password", default=False, autoDefault=False ) self.change_psswd.clicked.connect(partial(self.change_admin_psswd, "")) self.edit_config = QPushButton( "Edit config", default=False, autoDefault=False ) self.edit_config.clicked.connect(self.edit_config_file) self.control_checkbox = QCheckBox("", self) self.control_label = QLabel("Version 1 controller") self.control_checkbox_changed = ( self.main_window.get_controller_version() ) self.control_checkbox.clicked.connect(self.control_checkbox_toggled) self.max_thumbnails_label = QLabel( "Number of thumbnails in Data Browser:" ) self.max_thumbnails_box = QSpinBox() self.max_thumbnails_box.setMinimum(1) self.max_thumbnails_box.setMaximum(15) self.max_thumbnails_box.setSingleStep(1) self.radioView_checkbox = QCheckBox("", self) self.radioView_label = QLabel( "Radiological orientation in miniviewer (data browser)" ) v_box_global = QVBoxLayout() v_box_global.addLayout( self.create_horizontal_box(self.save_checkbox, self.save_label) ) v_box_global.addLayout( self.create_horizontal_box( self.clinical_mode_checkbox, self.clinical_mode_label ) ) v_box_global.addLayout( self.create_horizontal_box( self.admin_mode_checkbox, self.admin_mode_label ) ) v_box_global.addLayout(self.create_horizontal_box(self.change_psswd)) v_box_global.addLayout(self.create_horizontal_box(self.edit_config)) v_box_global.addLayout( self.create_horizontal_box( self.control_checkbox, self.control_label ) ) v_box_global.addWidget(self.max_thumbnails_label) v_box_global.addLayout( self.create_horizontal_box(self.max_thumbnails_box) ) v_box_global.addLayout( self.create_horizontal_box( self.radioView_checkbox, self.radioView_label ) ) self.groupbox_global.setLayout(v_box_global)
[docs] def create_horizontal_box(self, *widgets, add_stretch=True): """ Create a horizontal box layout containing the specified widgets. :param widgets (tuple[QtWidgets.QWidget, ...]): The widgets to add to the layout. :param add_stretch (bool): Whether to add stretch at the end to push the widgets to the left. Defaults to True. :return (QHBoxLayout): The created horizontal box layout. """ h_box = QHBoxLayout() for widget in widgets: h_box.addWidget(widget) if add_stretch: h_box.addStretch(1) return h_box
[docs] def create_matlab_group(self): """Create the Matlab group box.""" self.use_matlab_label = QLabel("Use Matlab") self.use_matlab_checkbox = QCheckBox("", self) self.matlab_label = QLabel( "Matlab path (e.g., matlab_dir/bin/matlab):" ) self.matlab_choice = QLineEdit() self.matlab_browse = QPushButton("Browse") self.matlab_browse.clicked.connect(self.browse_matlab) self.use_matlab_standalone_label = QLabel("Use Matlab standalone") self.use_matlab_standalone_checkbox = QCheckBox("", self) self.matlab_standalone_label = QLabel( "Matlab standalone path (e.g., MCR_dir/v95):" ) self.matlab_standalone_choice = QLineEdit() self.matlab_standalone_browse = QPushButton("Browse") self.matlab_standalone_browse.clicked.connect( self.browse_matlab_standalone ) v_box_matlab_path = QVBoxLayout() v_box_matlab_path.addWidget(self.matlab_label) v_box_matlab_path.addLayout( self.create_horizontal_box( self.matlab_choice, self.matlab_browse, add_stretch=False ) ) v_box_matlab_standalone_path = QVBoxLayout() v_box_matlab_standalone_path.addLayout( self.create_horizontal_box( self.use_matlab_standalone_checkbox, self.use_matlab_standalone_label, ) ) v_box_matlab_standalone_path.addWidget(self.matlab_standalone_label) v_box_matlab_standalone_path.addLayout( self.create_horizontal_box( self.matlab_standalone_choice, self.matlab_standalone_browse, add_stretch=False, ) ) v_box_matlab = QVBoxLayout() v_box_matlab.addLayout( self.create_horizontal_box( self.use_matlab_checkbox, self.use_matlab_label ) ) v_box_matlab.addLayout(v_box_matlab_path) v_box_matlab.addLayout(v_box_matlab_standalone_path) self.groupbox_matlab.setLayout(v_box_matlab)
[docs] def create_mrtrix_group(self): """Create the mrtrix group box.""" self.use_mrtrix_label = QLabel("Use mrtrix") self.use_mrtrix_checkbox = QCheckBox("", self) self.mrtrix_label = QLabel("mrtrix path (e.g. mrtrix_dir/bin):") self.mrtrix_choice = QLineEdit() self.mrtrix_browse = QPushButton("Browse") self.mrtrix_browse.clicked.connect(self.browse_mrtrix) v_box_mrtrix_path = QVBoxLayout() v_box_mrtrix_path.addWidget(self.mrtrix_label) v_box_mrtrix_path.addLayout( self.create_horizontal_box( self.mrtrix_choice, self.mrtrix_browse, add_stretch=False ) ) v_box_mrtrix = QVBoxLayout() v_box_mrtrix.addLayout( self.create_horizontal_box( self.use_mrtrix_checkbox, self.use_mrtrix_label ) ) v_box_mrtrix.addLayout(v_box_mrtrix_path) self.groupbox_mrtrix.setLayout(v_box_mrtrix)
[docs] def create_pipeline_tab(self, _translate): """ Create the 'Pipeline' tab in the settings interface. This tab allows configuring various neuroimaging tool settings, including Matlab, SPM, FSL, AFNI, ANTS, FreeSurfer, MRtrix, and CAPSUL. :param _translate (Callable): Function used to translate UI text (translate method of QCoreApplication in the Qt framework.) """ self.tab_pipeline = QWidget() self.tab_pipeline.setObjectName("tab_pipeline") self.tab_widget.addTab( self.tab_pipeline, _translate("Dialog", "Pipeline") ) self.groupbox_matlab = QGroupBox("Matlab") self.create_matlab_group() self.groupbox_spm = QGroupBox("SPM") self.create_spm_group() self.groupbox_fsl = QGroupBox("FSL") self.create_fsl_group() self.groupbox_afni = QGroupBox("AFNI") self.create_afni_group() self.groupbox_ants = QGroupBox("ANTS") self.create_ants_group() self.groupbox_freesurfer = QGroupBox("FreeSurfer") self.create_freesurfer_group() self.groupbox_mrtrix = QGroupBox("mrtrix") self.create_mrtrix_group() groupbox_capsul = QGroupBox("CAPSUL") self.create_capsul_group(groupbox_capsul) # general layout self.tab_pipeline_layout = QVBoxLayout() self.tab_pipeline_layout.addWidget(self.groupbox_matlab) self.tab_pipeline_layout.addWidget(self.groupbox_spm) self.tab_pipeline_layout.addWidget(self.groupbox_fsl) self.tab_pipeline_layout.addWidget(self.groupbox_afni) self.tab_pipeline_layout.addWidget(self.groupbox_ants) self.tab_pipeline_layout.addWidget(self.groupbox_freesurfer) self.tab_pipeline_layout.addWidget(self.groupbox_mrtrix) self.tab_pipeline_layout.addWidget(groupbox_capsul) self.tab_pipeline_layout.addStretch(1) self.tab_pipeline.setLayout(self.tab_pipeline_layout)
[docs] def create_projects_preferences(self): """Create the projects preferences group box.""" self.projects_save_path_label = QLabel("Projects folder:") self.projects_save_path_line_edit = QLineEdit() self.projects_save_path_browse = QPushButton("Browse") self.projects_save_path_browse.clicked.connect( self.browse_projects_save_path ) self.max_projects_label = QLabel( 'Number of projects in "Saved projects":' ) self.max_projects_box = QSpinBox() self.max_projects_box.setMinimum(1) self.max_projects_box.setMaximum(20) self.max_projects_box.setSingleStep(1) v_box_projects_save = QVBoxLayout() v_box_projects_save.addWidget(self.projects_save_path_label) v_box_projects_save.addLayout( self.create_horizontal_box( self.projects_save_path_line_edit, self.projects_save_path_browse, add_stretch=False, ) ) v_box_max_projects = QVBoxLayout() v_box_max_projects.addWidget(self.max_projects_label) v_box_max_projects.addLayout( self.create_horizontal_box(self.max_projects_box) ) projects_layout = QVBoxLayout() projects_layout.addLayout(v_box_projects_save) projects_layout.addLayout(v_box_max_projects) self.groupbox_projects.setLayout(projects_layout)
[docs] def create_populse_preferences(self): """Create the POPULSE third party preferences group box.""" self.mri_conv_path_label = QLabel( "Absolute path to MRIManager.jar file: (e.g., mri_conv_dir/" "MRIFileManager/MRIManager.jar)" ) self.mri_conv_path_line_edit = QLineEdit() self.mri_conv_path_browse = QPushButton("Browse") self.mri_conv_path_browse.clicked.connect(self.browse_mri_conv_path) v_box_mri_conv = QVBoxLayout() v_box_mri_conv.addWidget(self.mri_conv_path_label) v_box_mri_conv.addLayout( self.create_horizontal_box( self.mri_conv_path_line_edit, self.mri_conv_path_browse, add_stretch=False, ) ) populse_layout = QVBoxLayout() populse_layout.addLayout(v_box_mri_conv) self.groupbox_populse.setLayout(populse_layout)
[docs] def create_resources_preferences(self): """Create the external resources preferences group box.""" self.resources_path_label = QLabel( "Absolute path to the external resource data (required by " "certain processes for proper functionality):" ) self.resources_path_line_edit = QLineEdit() self.resources_path_browse = QPushButton("Browse") self.resources_path_browse.clicked.connect(self.browse_resources_path) v_box_resources = QVBoxLayout() v_box_resources.addWidget(self.resources_path_label) v_box_resources.addLayout( self.create_horizontal_box( self.resources_path_line_edit, self.resources_path_browse, add_stretch=False, ) ) resources_layout = QVBoxLayout() resources_layout.addLayout(v_box_resources) self.groupbox_resources.setLayout(resources_layout)
[docs] def create_spm_group(self): """Create the SPM group box.""" self.use_spm_label = QLabel("Use SPM") self.use_spm_checkbox = QCheckBox("", self) self.spm_label = QLabel("SPM path (e.g., spm_dir/spm12):") self.spm_choice = QLineEdit() self.spm_browse = QPushButton("Browse") self.spm_browse.clicked.connect(self.browse_spm) self.use_spm_standalone_label = QLabel("Use SPM standalone") self.use_spm_standalone_checkbox = QCheckBox("", self) self.spm_standalone_label = QLabel( "SPM standalone path (e.g., the directory hosting the " "run_spm12.sh file):" ) self.spm_standalone_choice = QLineEdit() self.spm_standalone_browse = QPushButton("Browse") self.spm_standalone_browse.clicked.connect(self.browse_spm_standalone) v_box_spm_path = QVBoxLayout() v_box_spm_path.addWidget(self.spm_label) v_box_spm_path.addLayout( self.create_horizontal_box( self.spm_choice, self.spm_browse, add_stretch=False ) ) v_box_spm_standalone_path = QVBoxLayout() v_box_spm_standalone_path.addWidget(self.spm_standalone_label) v_box_spm_standalone_path.addLayout( self.create_horizontal_box( self.spm_standalone_choice, self.spm_standalone_browse, add_stretch=False, ) ) v_box_spm = QVBoxLayout() v_box_spm.addLayout( self.create_horizontal_box( self.use_spm_checkbox, self.use_spm_label ) ) v_box_spm.addLayout(v_box_spm_path) v_box_spm.addLayout( self.create_horizontal_box( self.use_spm_standalone_checkbox, self.use_spm_standalone_label ) ) v_box_spm.addLayout(v_box_spm_standalone_path) self.groupbox_spm.setLayout(v_box_spm)
[docs] def create_tools_tab(self, _translate): """ Create the 'Tools' tab in the settings interface. This tab contains various sections for configuring global preferences, project-specific settings, third-party tool integration, and external resources. :param _translate (Callable): Function used to translate UI text (translate method of QCoreApplication in the Qt framework.) """ self.tab_tools = QWidget() self.tab_tools.setObjectName("tab_tools") self.tab_widget.addTab(self.tab_tools, _translate("Dialog", "Tools")) self.groupbox_global = QGroupBox("Global preferences") self.create_global_preferences() self.groupbox_projects = QGroupBox("Projects preferences") self.create_projects_preferences() self.groupbox_populse = QGroupBox("POPULSE third party preference") self.create_populse_preferences() self.groupbox_resources = QGroupBox("External resources preferences") self.create_resources_preferences() self.tab_tools_layout = QVBoxLayout() self.tab_tools_layout.addLayout( self.create_horizontal_box(self.groupbox_global) ) self.tab_tools_layout.addWidget(self.groupbox_projects) self.tab_tools_layout.addWidget(self.groupbox_populse) self.tab_tools_layout.addWidget(self.groupbox_resources) self.tab_tools_layout.addStretch(1) self.tab_tools.setLayout(self.tab_tools_layout)
[docs] def edit_capsul_config(self): """ Capsul engine edition. This method is used when user hit the Edit CAPSUL config button (File > MIA preferences > Pipeline tab). """ # Validate the current Mia config first if not self.validate_and_save(): return config = Config() capsul_config = config.get_capsul_config(sync_from_engine=False) modules = capsul_config.get("engine_modules", []) # Build a temporary new engine (because it may not be validated) engine = capsul_engine() for module in modules + [ "fom", "axon", "python", "fsl", "freesurfer", "nipype", "afni", "ants", "mrtrix", "somaworkflow", ]: engine.load_module(module) envs = capsul_config.get("engine", {}) for env, conf in envs.items(): c = dict(conf) if "capsul_engine" not in c or "uses" not in c["capsul_engine"]: c["capsul_engine"] = { "uses": { engine.settings.module_name(m): "ALL" for m in conf.keys() } } engine.settings.import_configs(env, c, cont_on_error=True) dialog = SettingsEditor(engine) try: result = dialog.exec() except Exception as e: logger.warning(e) return if result: settings = engine.settings.export_config_dict() capsul_config["engine"] = settings capsul_config["engine_modules"] = list(engine._loaded_modules) try: config.set_capsul_config(capsul_config) except Exception as e: logger.warning(e) return # Update Mia preferences GUI which might have changed self.update_gui_from_capsul_config(capsul_config) del dialog del engine
[docs] def edit_config_file(self): """Create a window to view, edit the mia configuration file.""" # import verCmp only here to prevent circular import issue from populse_mia.utils import verCmp config = Config() self.editConf = QDialog() self.editConf.setWindowTitle( os.path.join( config.get_properties_path(), "properties", "config.yml" ) ) self.editConf.txt = QPlainTextEdit() stream = yaml.dump( config.config, default_flow_style=False, allow_unicode=True ) self.editConf.txt.insertPlainText(str(stream)) textWidth = self.editConf.txt.width() + 100 textHeight = self.editConf.txt.height() + 200 self.editConf.txt.setMinimumSize(textWidth, textHeight) self.editConf.txt.resize(textWidth, textHeight) buttonBox = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self ) buttonBox.button(QDialogButtonBox.Ok).setDefault(False) buttonBox.button(QDialogButtonBox.Cancel).setDefault(False) self.findChar_line_edit = QLineEdit() findChar_button = QPushButton("Find") findChar_button.setDefault(True) h_box_find = QHBoxLayout() h_box_find.addWidget(self.findChar_line_edit) h_box_find.addWidget(findChar_button) findChar_button.clicked.connect(self.findChar) layout = QFormLayout() layout.addWidget(self.editConf.txt) layout.addRow(h_box_find) layout.addWidget(buttonBox) buttonBox.accepted.connect(self.editConf.accept) buttonBox.rejected.connect(self.editConf.reject) self.editConf.setLayout(layout) event = self.editConf.exec() if not event: self.editConf.close() else: stream = self.editConf.txt.toPlainText() if verCmp(yaml.__version__, "5.1", "sup"): config.config = yaml.load(stream, Loader=yaml.FullLoader) else: config.config = yaml.load(stream) config.saveConfig() self.editConf.close()
[docs] def findChar(self): """ Highlights characters in red when using the Find button in the configuration editor. This method searches for a pattern entered in the find field and highlights all matching occurrences in the text editor. """ cursor = self.editConf.txt.textCursor() cursor.select(QtGui.QTextCursor.Document) cursor.setCharFormat(QtGui.QTextCharFormat()) cursor.clearSelection() self.editConf.txt.setTextCursor(cursor) pattern = self.findChar_line_edit.text() if pattern == "": return cursor = self.editConf.txt.textCursor() format = QtGui.QTextCharFormat() format.setBackground(QtGui.QBrush(QtGui.QColor(255, 0, 0, 50))) regex = QtCore.QRegExp(pattern) pos = 0 index = regex.indexIn(self.editConf.txt.toPlainText(), pos) while index != -1: cursor.setPosition(index) for _ in pattern: cursor.movePosition(QtGui.QTextCursor.Right, 1) cursor.mergeCharFormat(format) pos = index + regex.matchedLength() index = regex.indexIn(self.editConf.txt.toPlainText(), pos)
[docs] def load_config(self): """Load the configuration settings.""" config = Config() self.save_checkbox.setChecked(config.isAutoSave()) self.clinical_mode_checkbox.setChecked(config.get_use_clinical()) self.admin_mode_checkbox.setChecked(not config.get_user_mode()) self.change_psswd.setVisible(not config.get_user_mode()) self.edit_config.setVisible(not config.get_user_mode()) self.control_checkbox.setChecked(config.isControlV1()) self.max_thumbnails_box.setValue(config.get_max_thumbnails()) self.radioView_checkbox.setChecked(config.isRadioView()) self.projects_save_path_line_edit.setText( config.get_projects_save_path() ) self.max_projects_box.setValue(config.get_max_projects()) self.mri_conv_path_line_edit.setText(config.get_mri_conv_path()) self.resources_path_line_edit.setText(config.get_resources_path()) self.use_afni_checkbox.setChecked(config.get_use_afni()) self.afni_choice.setText(config.get_afni_path()) self.use_ants_checkbox.setChecked(config.get_use_ants()) self.ants_choice.setText(config.get_ants_path()) self.use_freesurfer_checkbox.setChecked(config.get_use_freesurfer()) self.freesurfer_choice.setText(config.get_freesurfer_setup()) self.use_fsl_checkbox.setChecked(config.get_use_fsl()) self.fsl_choice.setText(config.get_fsl_config()) self.matlab_choice.setText(config.get_matlab_path()) self.matlab_standalone_choice.setText( config.get_matlab_standalone_path() ) self.use_mrtrix_checkbox.setChecked(config.get_use_mrtrix()) self.mrtrix_choice.setText(config.get_mrtrix_path()) self.spm_choice.setText(config.get_spm_path()) if config.get_use_spm_standalone(): archi = platform.architecture() if "Windows" in archi[1]: self.use_matlab_standalone_checkbox.setChecked(False) else: self.use_matlab_standalone_checkbox.setChecked(True) self.use_spm_standalone_checkbox.setChecked(True) self.use_matlab_checkbox.setChecked(False) self.use_spm_checkbox.setChecked(False) elif config.get_use_spm(): self.use_spm_standalone_checkbox.setChecked(False) self.use_matlab_checkbox.setChecked(True) self.use_spm_checkbox.setChecked(True) self.use_matlab_standalone_checkbox.setChecked(False) elif config.get_use_matlab(): self.use_spm_standalone_checkbox.setChecked(False) self.use_matlab_checkbox.setChecked(True) self.use_spm_checkbox.setChecked(False) self.use_matlab_standalone_checkbox.setChecked(False) elif config.get_use_matlab_standalone(): self.use_spm_standalone_checkbox.setChecked(False) self.use_matlab_checkbox.setChecked(False) self.use_spm_checkbox.setChecked(False) self.use_matlab_standalone_checkbox.setChecked(True) self.spm_standalone_choice.setText(config.get_spm_standalone_path()) self.background_color_combo.setCurrentText( config.getBackgroundColor() or "White" ) self.text_color_combo.setCurrentText(config.getTextColor() or "Black") self.fullscreen_cbox.setChecked(config.get_mainwindow_maximized()) wsize = config.get_mainwindow_size() if isinstance(wsize, list) and len(wsize) >= 2: self.mainwindow_size_x_spinbox.setValue(wsize[0]) self.mainwindow_size_y_spinbox.setValue(wsize[1])
[docs] def ok_clicked(self): """Handle the OK button click event.""" if self.validate_and_save(OK_clicked=True): self.accept() self.close()
[docs] def save_minimal_config(self, config): """ Saves a minimal configuration for CAPSUL config synchronization. :param config (Config): The configuration object to update and save. """ config.set_afni_path(self.afni_choice.text()) config.set_use_afni(self.use_afni_checkbox.isChecked()) config.set_ants_path(self.ants_choice.text()) config.set_use_ants(self.use_ants_checkbox.isChecked()) config.set_freesurfer_setup(self.freesurfer_choice.text()) config.set_use_freesurfer(self.use_freesurfer_checkbox.isChecked()) config.set_fsl_config(self.fsl_choice.text()) config.set_use_fsl(self.use_fsl_checkbox.isChecked()) config.set_matlab_path(self.matlab_choice.text()) config.set_use_matlab(self.use_matlab_checkbox.isChecked()) config.set_matlab_standalone_path(self.matlab_standalone_choice.text()) config.set_use_matlab_standalone( self.use_matlab_standalone_checkbox.isChecked() ) config.set_mrtrix_path(self.mrtrix_choice.text()) config.set_use_mrtrix(self.use_mrtrix_checkbox.isChecked()) config.set_spm_path(self.spm_choice.text()) config.set_use_spm(self.use_spm_checkbox.isChecked()) config.set_spm_standalone_path(self.spm_standalone_choice.text()) config.set_use_spm_standalone( self.use_spm_standalone_checkbox.isChecked() )
[docs] def save_full_config(self, config): """ Saves the full configuration and validates settings. :param config (Config): The configuration object to update and save. :return (bool): True if the configuration is valid and successfully saved, False otherwise. """ config.setAutoSave(self.save_checkbox.isChecked()) config.set_radioView(self.radioView_checkbox.isChecked()) config.setControlV1(self.control_checkbox.isChecked()) config.set_max_thumbnails( min(max(self.max_thumbnails_box.value(), 1), 15) ) config.set_max_projects(min(max(self.max_projects_box.value(), 1), 20)) self.main_window.windowName = "MIA - Multiparametric Image Analysis" if self.admin_mode_checkbox.isChecked(): config.set_user_mode(False) self.main_window.windowName = ( f"{self.main_window.windowName} (Admin mode)" ) else: config.set_user_mode(True) # Final Window name self.main_window.windowName = f"{self.main_window.windowName} - " self.main_window.setWindowTitle( f"{self.main_window.windowName}{self.main_window.projectName}" ) if self.clinical_mode_checkbox.isChecked(): config.set_clinical_mode(True) self.use_clinical_mode_signal.emit() else: config.set_clinical_mode(False) self.not_use_clinical_mode_signal.emit() fullscreen = self.fullscreen_cbox.isChecked() config.set_mainwindow_maximized(fullscreen) if fullscreen: self.main_window.showMaximized() else: self.main_window.showNormal() config.set_mainwindow_size( [ self.mainwindow_size_x_spinbox.value(), self.mainwindow_size_y_spinbox.value(), ] ) config.setBackgroundColor(self.background_color_combo.currentText()) config.setTextColor(self.text_color_combo.currentText()) self.main_window.setStyleSheet( f"background-color:{self.background_color_combo.currentText()};" f"color:{self.text_color_combo.currentText()};" ) if not self.validate_paths(config): return False self.signal_preferences_change.emit() return True
[docs] def setup_ui(self): """Set up the user interface components.""" self.setObjectName("Dialog") self.setWindowTitle("MIA preferences") _translate = QtCore.QCoreApplication.translate self.tab_widget = QTabWidget(self) self.tab_widget.setEnabled(True) # Create tabs self.create_tools_tab(_translate) self.create_pipeline_tab(_translate) self.create_appearance_tab(_translate) # Buttons layout self.push_button_ok = QPushButton("OK") self.push_button_ok.setObjectName("pushButton_ok") self.push_button_ok.clicked.connect(self.ok_clicked) self.push_button_cancel = QPushButton("Cancel") self.push_button_cancel.setObjectName("pushButton_cancel") self.push_button_cancel.clicked.connect(self.close) self.status_label = QLabel("") hbox_buttons = QHBoxLayout() hbox_buttons.addWidget(self.status_label) hbox_buttons.addStretch(1) hbox_buttons.addWidget(self.push_button_ok) hbox_buttons.addWidget(self.push_button_cancel) vbox = QVBoxLayout() vbox.addWidget(self.tab_widget) vbox.addLayout(hbox_buttons) # Global layout - scrollable global window self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.widget = QWidget() self.widget.setLayout(vbox) self.scroll.setWidget(self.widget) self.final_layout = QVBoxLayout() self.final_layout.addWidget(self.scroll) self.setLayout(self.final_layout)
[docs] def show_error_message(self, title, message): """ Displays an error message dialog. :param title (str): The title of the error message dialog. :param message (str): The detailed error message to display. """ self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Critical) self.msg.setText(title) self.msg.setInformativeText(message) self.msg.setWindowTitle("Error") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show()
[docs] def show_warning_message(self, title, message): """ Displays a warning message dialog. :param title (str): The title of the warning message dialog. :param message (str): The detailed warning message to display. """ self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Warning) self.msg.setText(title) self.msg.setInformativeText(message) self.msg.setWindowTitle("Warning") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show()
[docs] def update_gui_from_capsul_config(self, conf): """ Updates the GUI elements based on the CAPSUL configuration. This method retrieves the configuration settings for various neuroimaging tools (e.g., AFNI, ANTs, FreeSurfer, FSL, Matlab, SPM, etc.) and updates the corresponding GUI fields, including checkboxes and text fields. :param conf (CapsulConfig): The CAPSUL configuration object containing the paths and usage states of different tools. """ # afni use_afni = conf.get_use_afni() if use_afni: self.afni_choice.setText(conf.get_afni_path()) self.use_afni_checkbox.setChecked(use_afni) # ants use_ants = conf.get_use_ants() if use_ants: self.ants_choice.setText(conf.get_ants_path()) self.use_ants_checkbox.setChecked(use_ants) # freesurfer use_freesurfer = conf.get_use_freesurfer() if use_freesurfer: self.freesurfer_choice.setText(conf.get_freesurfer_setup()) self.use_freesurfer_checkbox.setChecked(use_freesurfer) # fsl use_fsl = conf.get_use_fsl() if use_fsl: self.fsl_choice.setText(conf.get_fsl_config()) self.use_fsl_checkbox.setChecked(use_fsl) # matlab use_matlab = conf.get_use_matlab() self.use_matlab_checkbox.setChecked(use_matlab) self.matlab_choice.setText(conf.get_matlab_path()) # matlab - MCR use_matlab_sa = conf.get_use_matlab_standalone() self.use_matlab_standalone_checkbox.setChecked(use_matlab_sa) self.matlab_standalone_choice.setText( conf.get_matlab_standalone_path() ) # mrtrix use_mrtrix = conf.get_use_mrtrix() if use_mrtrix: self.mrtrix_choice.setText(conf.get_mrtrix_path()) self.use_mrtrix_checkbox.setChecked(use_mrtrix) # spm use_spm = conf.get_use_spm() self.use_spm_checkbox.setChecked(use_spm) self.spm_choice.setText(conf.get_spm_path()) # spm - Standalone use_spm_sa = conf.get_use_spm_standalone() self.use_spm_standalone_checkbox.setChecked(use_spm_sa) self.spm_standalone_choice.setText(conf.get_spm_standalone_path())
[docs] def use_afni_changed(self): """Handle the use_afni checkbox change event.""" self.afni_choice.setDisabled(not self.use_afni_checkbox.isChecked()) self.afni_label.setDisabled(not self.use_afni_checkbox.isChecked())
[docs] def use_ants_changed(self): """Handle the use_ants checkbox change event.""" self.ants_choice.setDisabled(not self.use_ants_checkbox.isChecked()) self.ants_label.setDisabled(not self.use_ants_checkbox.isChecked())
[docs] def use_current_mainwindow_size(self): """Use the current main window size.""" self.mainwindow_size_x_spinbox.setValue(self.main_window.width()) self.mainwindow_size_y_spinbox.setValue(self.main_window.height())
[docs] def use_freesurfer_changed(self): """Handle the use_freesurfer checkbox change event.""" self.freesurfer_choice.setDisabled( not self.use_freesurfer_checkbox.isChecked() ) self.freesurfer_label.setDisabled( not self.use_freesurfer_checkbox.isChecked() )
[docs] def use_fsl_changed(self): """Handle the use_fsl checkbox change event.""" self.fsl_choice.setDisabled(not self.use_fsl_checkbox.isChecked()) self.fsl_label.setDisabled(not self.use_fsl_checkbox.isChecked())
[docs] def use_matlab_changed(self): """Handle the use_matlab checkbox change event.""" if not self.use_matlab_checkbox.isChecked(): self.matlab_choice.setDisabled(True) self.spm_choice.setDisabled(True) self.matlab_label.setDisabled(True) self.spm_label.setDisabled(True) self.spm_browse.setDisabled(True) self.matlab_browse.setDisabled(True) self.use_spm_checkbox.setChecked(False) else: self.matlab_choice.setDisabled(False) self.matlab_label.setDisabled(False) self.matlab_browse.setDisabled(False) self.use_matlab_standalone_checkbox.setChecked(False)
[docs] def use_matlab_standalone_changed(self): """Handle the use_matlab_standalone checkbox change event.""" if not self.use_matlab_standalone_checkbox.isChecked(): archi = platform.architecture() if "Windows" not in archi[1]: self.spm_standalone_choice.setDisabled(True) self.use_spm_standalone_checkbox.setChecked(False) self.spm_standalone_label.setDisabled(True) self.spm_standalone_browse.setDisabled(True) self.matlab_standalone_choice.setDisabled(True) self.matlab_standalone_label.setDisabled(True) self.matlab_standalone_browse.setDisabled(True) else: self.matlab_standalone_choice.setDisabled(False) self.matlab_standalone_label.setDisabled(False) self.matlab_standalone_browse.setDisabled(False) self.use_matlab_checkbox.setChecked(False)
[docs] def use_mrtrix_changed(self): """Handle the use_mrtrix checkbox change event.""" self.mrtrix_choice.setDisabled( not self.use_mrtrix_checkbox.isChecked() ) self.mrtrix_label.setDisabled(not self.use_mrtrix_checkbox.isChecked())
[docs] def use_spm_changed(self): """Handle the use_spm checkbox change event.""" if not self.use_spm_checkbox.isChecked(): self.spm_choice.setDisabled(True) self.spm_label.setDisabled(True) self.spm_browse.setDisabled(True) else: self.use_matlab_checkbox.setChecked(True) self.spm_choice.setDisabled(False) self.spm_label.setDisabled(False) self.spm_browse.setDisabled(False) self.spm_standalone_choice.setDisabled(True) self.spm_standalone_label.setDisabled(True) self.spm_standalone_browse.setDisabled(True) self.use_spm_standalone_checkbox.setChecked(False) self.use_matlab_standalone_checkbox.setChecked(False)
[docs] def use_spm_standalone_changed(self): """Handle the use_spm_standalone checkbox change event.""" if not self.use_spm_standalone_checkbox.isChecked(): self.spm_standalone_choice.setDisabled(True) self.spm_standalone_label.setDisabled(True) self.spm_standalone_browse.setDisabled(True) else: archi = platform.architecture() if "Windows" not in archi[1]: self.use_matlab_standalone_checkbox.setChecked(True) self.spm_standalone_choice.setDisabled(False) self.spm_standalone_label.setDisabled(False) self.spm_standalone_browse.setDisabled(False) self.spm_choice.setDisabled(True) self.spm_label.setDisabled(True) self.spm_browse.setDisabled(True) self.use_spm_checkbox.setChecked(False) self.use_matlab_checkbox.setChecked(False)
[docs] def validate_and_save(self, OK_clicked=False): """ Validate and save the preferences. :param ok_clicked (bool): Whether the OK button was clicked (True) when this method was launched. :Contains: - remove_capsul_config: Helper function to remove a module's configuration - clean_spm_config: Cleans the SPM configuration based on standalone mode - clean_matlab_config: Removes MATLAB-related configuration keys """ config = Config() if not OK_clicked: self.save_minimal_config(config) elif not self.save_full_config(config): return False c_c = config.config.setdefault("capsul_config", {}) c_e = config.get_capsul_engine() if not (c_c and c_e): config.get_capsul_config(sync_from_engine=False) config.saveConfig() return True def remove_capsul_config(module, key): """ Helper function to remove a module's configuration. :param module (str): The name of the module to remove. :param key (str): The specific configuration key to remove. """ # TODO: We only deal here with the global environment cif = c_e.settings.config_id_field with c_e.settings as settings: config_module = settings.config(module, "global") if config_module: settings.remove_config( module, "global", getattr(config_module, cif) ) if not module == "freesurfer": c_c.get("engine", {}).get("global", {}).get( f"capsul.engine.module.{module}", {} ).get(module, {}).pop(key, None) # Sync capsul config from mia config, if module is not used: for module in ["afni", "ants", "freesurfer", "fsl", "mrtrix"]: if not getattr(config, f"get_use_{module}")(): remove_capsul_config(module, "directory") if module == "fsl": remove_capsul_config(module, "config") def clean_spm_config(standalone_check): """ Cleans the SPM configuration based on standalone mode. :param standalone_check (bool): Indicates whether to clean based on standalone mode. """ try: keys = list( c_c["engine"]["global"]["capsul.engine.module.spm"].keys() ) except KeyError: return dict4clean = { k: bool( "standalone" in c_c["engine"]["global"]["capsul.engine.module.spm"][k] and c_c["engine"]["global"]["capsul.engine.module.spm"][k][ "standalone" ] == standalone_check ) for k in keys } for k, should_remove in dict4clean.items(): if should_remove: del c_c["engine"]["global"]["capsul.engine.module.spm"][k] if not config.get_use_spm_standalone(): clean_spm_config(True) if not config.get_use_spm(): clean_spm_config(False) if ( not c_c.get("engine", {}) .get("global", {}) .get("capsul.engine.module.spm", {}) ): c_c["engine"]["global"].pop("capsul.engine.module.spm", None) def clean_matlab_config(key): """ Removes MATLAB-related configuration keys. :param key (str): The configuration key to remove from the MATLAB module. """ try: keys = list( c_c["engine"]["global"][ "capsul.engine.module.matlab" ].keys() ) except KeyError: return for k in keys: c_c["engine"]["global"]["capsul.engine.module.matlab"][k].pop( key, None ) if not config.get_use_matlab(): clean_matlab_config("executable") if not config.get_use_matlab_standalone(): clean_matlab_config("mcr_directory") if ( not c_c.get("engine", {}) .get("global", {}) .get("capsul.engine.module.matlab", {}) ): c_c["engine"]["global"].pop("capsul.engine.module.matlab", None) config.get_capsul_config(sync_from_engine=False) config.saveConfig() return True
[docs] def validate_matlab_path(self, path, config): """ Validates the given Matlab executable path. :param path (str): The path to the Matlab executable. :param config (Config): The configuration object to update. :return (bool): True if the path is valid and updated in the configuration, False otherwise. """ if not os.path.isfile(path): self.wrong_path(path, "Matlab") return False if path == config.get_matlab_path(): config.set_use_matlab(True) config.set_use_matlab_standalone(False) if not self.use_spm_checkbox.isChecked(): config.set_use_spm(False) config.set_use_spm_standalone(False) # We don't test matlab because it has already been # tested before return True elif not self.use_spm_checkbox.isChecked(): try: matlab_cmd = "ver; exit" result = subprocess.run( [ path, "-nodisplay", "-nodesktop", "-nosplash", "-singleCompThread", "-r", matlab_cmd, ], capture_output=True, text=True, check=True, ) err = result.stderr except subprocess.CalledProcessError: self.wrong_path(path, "Matlab") return False except Exception as e: # Catch any other unexpected errors logger.warning(f"❌ Unexpected error: {e}") self.wrong_path(path, "Matlab") return False if err == "": config.set_matlab_path(path) config.set_use_matlab(True) config.set_use_matlab_standalone(False) config.set_use_spm(False) config.set_use_spm_standalone(False) return True else: self.wrong_path(path, "Matlab") return False else: # If self.use_spm_checkbox.isChecked() is True, the test will # take place when SPM is validated. pass
[docs] def validate_matlab_standalone_path(self, path, config): """ Validate the Matlab standalone path. This method does not thoroughly test the configuration for Matlab MCR alone (without SPM standalone) due to the lack of a concrete example. :param path (str): The path to the Matlab standalone directory. :param config (Config): The configuration object to update. :return (bool): True if the path is valid and updated in the configuration, False otherwise. """ if not os.path.isdir(path): self.wrong_path(path, "Matlab standalone") return False if not self.use_spm_standalone_checkbox.isChecked(): archi = platform.architecture() config.set_matlab_standalone_path(path) if "Windows" in archi[1]: logger.info( "Matlab Standalone Path enter, this is " "unnecessary to use SPM12 with Windows OS." ) config.set_use_matlab(True) config.set_use_matlab_standalone(False) config.set_use_spm_standalone(False) config.set_use_spm(False) return True else: config.set_use_matlab(False) config.set_use_matlab_standalone(True) config.set_use_spm_standalone(False) config.set_use_spm(False) return True else: # If self.use_spm_standalone_checkbox.isChecked() is True, # the test will take place when SPM Stadalone is validated. pass
[docs] def validate_paths(self, config): """ Validate the paths and settings. This method checks the validity of paths for various neuroimaging tools (AFNI, ANTS, FreeSurfer, FSL, MRtrix, Matlab, SPM, etc.) and updates the configuration accordingly. It also validates additional paths such as the projects folder, MRIFileManager.jar path, and resources folder. :param config (Config): The configuration object where validated paths and settings will be stored. :return (bool): True if all paths and settings are valid, False otherwise. """ QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.status_label.setText("Testing configuration ...") QtCore.QCoreApplication.processEvents() tools = [ ( "AFNI", self.afni_choice.text(), self.use_afni_checkbox.isChecked(), config.set_use_afni, config.set_afni_path, "afni", "afni", ), ( "ANTS", self.ants_choice.text(), self.use_ants_checkbox.isChecked(), config.set_use_ants, config.set_ants_path, "antsRegistration", "ANTS", ), ( "FreeSurfer", self.freesurfer_choice.text(), self.use_freesurfer_checkbox.isChecked(), config.set_use_freesurfer, config.set_freesurfer_setup, "recon-all", "freesurfer", ), ( "FSL", self.fsl_choice.text(), self.use_fsl_checkbox.isChecked(), config.set_use_fsl, config.set_fsl_config, "flirt", "FSL", ), ( "mrtrix", self.mrtrix_choice.text(), self.use_mrtrix_checkbox.isChecked(), config.set_use_mrtrix, config.set_mrtrix_path, "mrinfo", "mrtrix", ), ( "Matlab", self.matlab_choice.text(), self.use_matlab_checkbox.isChecked(), config.set_use_matlab, config.set_matlab_path, None, "Matlab", ), ( "SPM", self.spm_choice.text(), self.use_spm_checkbox.isChecked(), config.set_use_spm, config.set_spm_path, None, "SPM", ), ( "Matlab Standalone", self.matlab_standalone_choice.text(), self.use_matlab_standalone_checkbox.isChecked(), config.set_use_matlab_standalone, config.set_matlab_standalone_path, None, "Matlab standalone", ), ( "SPM Standalone", self.spm_standalone_choice.text(), self.use_spm_standalone_checkbox.isChecked(), config.set_use_spm_standalone, config.set_spm_standalone_path, None, "SPM standalone", ), ] for ( tool_name, path, is_checked, set_in_use, set_path, cmd, config_name, ) in tools: if not is_checked: set_in_use(is_checked) elif not self.validate_tool_path( tool_name, path, cmd, config, config_name, set_in_use, set_path ): QApplication.restoreOverrideCursor() return False # Projects folder projects_folder = self.projects_save_path_line_edit.text() if os.path.isdir(projects_folder): config.set_projects_save_path(projects_folder) else: self.show_error_message( "Invalid projects folder path", f"The projects folder path entered `{projects_folder}` is " f"invalid.", ) QApplication.restoreOverrideCursor() return False # MRIFileManager.jar path mri_conv_path = self.mri_conv_path_line_edit.text() if mri_conv_path == "": self.show_warning_message( "Empty MRIFileManager.jar path", "No path has been entered for MRIFileManager.jar.", ) config.set_mri_conv_path(mri_conv_path) elif os.path.isfile(mri_conv_path): config.set_mri_conv_path(mri_conv_path) else: self.show_error_message( "Invalid MRIFileManager.jar path", f"The MRIFileManager.jar path entered '{mri_conv_path}' is " f"invalid.", ) QApplication.restoreOverrideCursor() return False # Resources folder resources_folder = self.resources_path_line_edit.text() if os.path.isdir(resources_folder): config.set_resources_path(resources_folder) else: self.show_error_message( "Invalid resources folder path", f"The resources folder path entered '{resources_folder}' is " f"invalid.", ) QApplication.restoreOverrideCursor() return False QApplication.restoreOverrideCursor() return True
[docs] def validate_spm_path(self, path, config): """ Validates the SPM path and its compatibility with Matlab. This method checks whether the provided SPM and Matlab paths are valid. If they are already configured correctly, it enables SPM and Matlab usage without further checks. Otherwise, it attempts to run an SPM command via Matlab to confirm the setup. :param path (str): The file path to the SPM installation. :param config (Config): The configuration object where validated paths and settings will be stored. :return (bool): True if the SPM path and Matlab path are valid, False otherwise. """ matlab_path = self.matlab_choice.text() if not os.path.isdir(path): self.wrong_path(path, "SPM") return False if not os.path.isfile(matlab_path): self.wrong_path(matlab_path, "Matlab") return False if ( path == config.get_spm_path() and matlab_path == config.get_matlab_path() ): config.set_use_spm(True) config.set_use_matlab(True) config.set_use_matlab_standalone(False) config.set_use_spm_standalone(False) # We don't test Matlab and SPM because it has already been # tested before return True else: try: matlab_cmd = ( f"restoredefaultpath; " f"addpath('{path}'); " f"[name, ~]=spm('Ver'); " f"exit" ) result = subprocess.run( [ matlab_path, "-nodisplay", "-nodesktop", "-nosplash", "-singleCompThread", "-r", matlab_cmd, ], capture_output=True, text=True, check=True, ) err = result.stderr except subprocess.CalledProcessError: self.wrong_path(matlab_path, "Matlab") return False except Exception as e: # Catch any other unexpected errors logger.warning(f"❌ Unexpected error: {e}") self.wrong_path(matlab_path, "Matlab") return False if err == "": config.set_matlab_path(matlab_path) config.set_spm_path(path) config.set_use_matlab(True) config.set_use_spm(True) config.set_use_matlab_standalone(False) config.set_use_spm_standalone(False) return True elif "spm" in err: self.wrong_path(path, "SPM") return False else: self.wrong_path(matlab_path, "Matlab") return False
[docs] def validate_spm_standalone_path(self, path, config): """ Validates the SPM standalone path and its compatibility with Matlab standalone. This method checks whether the provided paths for SPM standalone and Matlab standalone (if applicable) are valid. It also verifies system architecture compatibility and attempts to execute SPM standalone to confirm its functionality. :param path (str): The file path to the SPM standalone installation. :param config (Config): The configuration object where validated paths and settings will be stored. :return (bool): True if the SPM standalone and Matlab standalone paths are valid, False otherwise. """ matlab_path = self.matlab_standalone_choice.text() archi = platform.architecture() if (not os.path.isdir(matlab_path)) and ("Windows" not in archi[1]): self.wrong_path(matlab_path, "Matlab standalone") return False if (matlab_path == config.get_matlab_standalone_path()) and ( path == config.get_spm_standalone_path() ): config.set_use_spm_standalone(True) if "Windows" in archi[1]: config.set_use_matlab(True) config.set_use_matlab_standalone(False) else: config.set_use_matlab_standalone(True) return True if os.path.isdir(path): if "Windows" in archi[1]: mcr = glob.glob(os.path.join(path, "spm*_win*.exe")) pos = False nb_bit_sys = archi[0] for i, mcr_path in enumerate(mcr): spm_path, spm_file_name = os.path.split(mcr_path) if nb_bit_sys[:2] in spm_file_name: pos = i if pos is False: self.wrong_path(path, "SPM standalone") return False elif os.path.isdir(matlab_path): mcr = glob.glob(os.path.join(path, "run_spm*.sh")) if mcr: try: if "Windows" in archi[1]: result = subprocess.run( [mcr[pos], "--version"], capture_output=True, text=True, check=True, ) else: result = subprocess.run( [mcr[0], matlab_path, "--version"], capture_output=True, text=True, check=True, ) err = result.stderr output = result.stdout except subprocess.CalledProcessError: self.wrong_path(path, "SPM standalone") return False except Exception as e: # Catch any other unexpected errors logger.warning(f"❌ Unexpected error: {e}") self.wrong_path(path, "SPM standalone") return False if (err == "" and output != "") or output.startswith("SPM8 "): # spm8 standalone doesn't accept --version but prints a # message that we can interpret as saying that SPM8 is # working anyway. config.set_use_spm_standalone(True) config.set_spm_standalone_path(path) config.set_matlab_standalone_path(matlab_path) if "Windows" not in archi[1]: config.set_use_matlab_standalone(True) return True elif ( (err != "") and ("version" in output.split()[2:]) and ("(standalone)" in output.split()[2:]) ): config.set_use_spm_standalone(True) config.set_spm_standalone_path(path) config.set_matlab_standalone_path(matlab_path) if "Windows" not in archi[1]: config.set_use_matlab_standalone(True) logger.warning( "The configuration for Matlab MCR and SPM " "standalone as defined in Mia's preferences " "seems to be valid, but the following issue has " "been detected:" ) logger.warning(f"{err}") logger.warning( "Please fix this issue to avoid a malfunction..." ) return True elif err != "": if "shared libraries" in err: self.wrong_path(matlab_path, "Matlab standalone") return False else: self.wrong_path(path, "SPM standalone") return False else: self.wrong_path(path, "SPM standalone") return False else: self.wrong_path(path, "SPM standalone") return False
[docs] def validate_tool_path( self, tool_name, path, cmd, config, config_name, set_in_use, set_path ): """ Validates the specified tool's path and checks its functionality. This method checks if the given tool path exists and verifies that the tool is functional by running a command to check its version. It also handles tool-specific setup for FreeSurfer and FSL by setting environment variables and adjusting paths as necessary. :param tool_name (str): The name of the tool to validate (e.g., "FreeSurfer", "FSL", "Matlab", etc.). :param path (str): The file path to the tool's installation directory. :param cmd (str): The command to execute within the tool's directory to check its version. :param config (Config): The configuration object where validated paths and settings will be stored. :param config_name (str): The name of the configuration setting for the tool. :param set_in_use (Callable): A function to set the tool's "in use" status in the configuration. :param set_path (Callable): A function to set the tool's path in the configuration. :return (bool): True if the tool's path is valid and functional, False otherwise. """ extra = "" option = "--version" if tool_name in ["FreeSurfer", "FSL"]: path_setup = path path = os.path.dirname(path) extra = "bin" if "FREESURFER_HOME" not in os.environ: os.environ["FREESURFER_HOME"] = path if tool_name == "FSL": option = "-version" if path.endswith(os.path.join("etc", "fslconf")): path = os.path.dirname(os.path.dirname(path)) elif path.endswith("etc"): path = os.path.dirname(path) if tool_name not in [ "Matlab", "Matlab Standalone", "SPM", "SPM Standalone", ] and not os.path.isdir(path): self.wrong_path(path, tool_name) return False cmd = os.path.join(path, extra, cmd) if cmd else None if cmd: try: result = subprocess.run( [cmd, option], capture_output=True, text=True, check=True, ) err = result.stderr except subprocess.CalledProcessError: self.wrong_path(path, tool_name, "config file") return False except Exception as e: # Catch any other unexpected errors logger.warning(f"❌ Unexpected error: {e}") self.wrong_path(path, tool_name) return False # Filter out lines containing 'warning' (case-insensitive) filtered_lines = [ line for line in err.split("\n") if "warning" not in line.lower() ] # If no lines left after filtering, set err to an empty string err = "\n".join(filtered_lines) if filtered_lines else "" if err == "": if tool_name in ["FreeSurfer", "FSL"]: set_path(path_setup) else: set_path(path) set_in_use(True) else: self.wrong_path(path, tool_name) return False if tool_name == "Matlab": if not self.validate_matlab_path(path, config): return False elif tool_name == "SPM": if not self.validate_spm_path(path, config): return False elif tool_name == "Matlab Standalone": if not self.validate_matlab_standalone_path(path, config): return False elif tool_name == "SPM Standalone": if not self.validate_spm_standalone_path(path, config): return False return True
[docs] def wrong_path(self, path, tool, extra_mess=""): """ Displays an error message for an invalid tool path. This method restores the cursor, clears the status label, and shows a QMessageBox with an error message indicating that the provided path for a specified tool is invalid. :param path (str): The invalid path entered by the user. :param tool (str): The name of the tool for which the path is being validated. :param extra_mess (str, optional): Additional context for the error message, such as specifying a configuration file. :return (None): This function does not return anything. """ QApplication.restoreOverrideCursor() self.status_label.setText("") self.msg = QMessageBox() self.msg.setIcon(QMessageBox.Critical) self.msg.setText(f"Invalid {tool} path") if extra_mess: extra_mess = f" {extra_mess}" self.msg.setInformativeText( f"The {tool}{extra_mess} path entered {path} is invalid." ) self.msg.setWindowTitle("Error") self.msg.setStandardButtons(QMessageBox.Ok) self.msg.buttonClicked.connect(self.msg.close) self.msg.show()
[docs] class PopUpProperties(QDialog): """ Dialog for modifying project properties. Allows users to change project settings, including visualized tags and information. Is called when the user wants to change the current project's properties (File > properties). .. Methods: - ok_clicked: saves the modifications and updates the data browser .. Signals: - signal_settings_change: Qt signal emitted when project settings are modified. """ # Signal that will be emitted at the end to tell that the project has # been created signal_settings_change = QtCore.pyqtSignal()
[docs] def __init__(self, project, databrowser, old_tags): """ Initialize the project properties dialog. :param project: current project in the software :param databrowser: data browser instance of the software :param old_tags: visualized tags before opening this dialog """ super().__init__() # Dialog setup self.setModal(True) self.project = project self.databrowser = databrowser self.old_tags = old_tags _translate = QtCore.QCoreApplication.translate self.setObjectName("Dialog") self.setWindowTitle("project properties") # Create tab widget self.tab_widget = QTabWidget(self) self.tab_widget.setEnabled(True) # The 'Visualized tags" tab with self.project.database.data() as database_data: self.tab_tags = PopUpVisualizedTags( self.project, database_data.get_shown_tags() ) self.tab_tags.setObjectName("tab_tags") self.tab_widget.addTab( self.tab_tags, _translate("Dialog", "Visualized tags") ) # The 'Informations" tab self.tab_infos = PopUpInformation(self.project) self.tab_infos.setObjectName("tab_infos") self.tab_widget.addTab( self.tab_infos, _translate("Dialog", "Information") ) # Create buttons # The 'OK' push button self.push_button_ok = QPushButton("OK") self.push_button_ok.setObjectName("pushButton_ok") self.push_button_ok.clicked.connect(self.ok_clicked) # The 'Cancel' push button self.push_button_cancel = QPushButton("Cancel") self.push_button_cancel.setObjectName("pushButton_cancel") self.push_button_cancel.clicked.connect(self.close) # Layout setup hbox_buttons = QHBoxLayout() hbox_buttons.addStretch(1) hbox_buttons.addWidget(self.push_button_ok) hbox_buttons.addWidget(self.push_button_cancel) vbox = QVBoxLayout() vbox.addWidget(self.tab_widget) vbox.addLayout(hbox_buttons) self.setLayout(vbox)
[docs] def ok_clicked(self): """Saves the modifications and updates the data browser.""" # Prepare history tracking history_maker = ["modified_visibilities", self.old_tags] # Collect newly selected tags new_visibilities = [ self.tab_tags.list_widget_selected_tags.item(x).text() for x in range(self.tab_tags.list_widget_selected_tags.count()) ] new_visibilities.append(TAG_FILENAME) with self.project.database.data(write=True) as database_data: database_data.set_shown_tags(new_visibilities) history_maker.append(new_visibilities) self.project.undos.append(history_maker) self.project.redos.clear() self.project.unsavedModifications = True # Update data browser columns with self.project.database.data() as database_data: self.databrowser.table_data.update_visualized_columns( self.old_tags, database_data.get_shown_tags() ) # Close dialog self.accept() self.close()
[docs] class PopUpQuit(QDialog): """ Dialog to handle unsaved project modifications when closing the software. Provides options to save, discard, or cancel the exit process. Is called when the user closes the software and the current project has been modified. .. Signals: - save_as_signal: Signal emitted when user chooses to save the project. - do_not_save_signal: Signal emitted when user chooses to exit without saving. - cancel_signal: Signal emitted when user cancels the exit process. .. Methods: - can_exit: returns the value of _bool_exit - cancel_clicked: makes the actions to cancel the action - do_not_save_clicked: makes the actions not to save the project - save_as_clicked: makes the actions to save the project """ save_as_signal = QtCore.pyqtSignal() do_not_save_signal = QtCore.pyqtSignal() cancel_signal = QtCore.pyqtSignal()
[docs] def __init__(self, project): """ Initialize the quit confirmation dialog. :param project: Current project with unsaved modifications. """ super().__init__() self.project = project self._bool_exit = False self.setWindowTitle("Confirm exit") # Create confirmation label label = QLabel( f"Do you want to exit without saving {self.project.getName()}?" ) # Create action buttons push_button_save_as = QPushButton("Save", self) push_button_do_not_save = QPushButton("Do not save", self) push_button_cancel = QPushButton("Cancel", self) # Button connections push_button_save_as.clicked.connect(self.save_as_clicked) push_button_do_not_save.clicked.connect(self.do_not_save_clicked) push_button_cancel.clicked.connect(self.cancel_clicked) # Create horizontal layout for buttons hbox = QHBoxLayout() hbox.addStretch(1) hbox.addWidget(push_button_save_as) hbox.addWidget(push_button_do_not_save) hbox.addWidget(push_button_cancel) hbox.addStretch(1) # Create vertical layout vbox = QVBoxLayout() vbox.addWidget(label) vbox.addLayout(hbox) self.setLayout(vbox)
[docs] def can_exit(self): """ Check if the application can exit. :return (bool): True if exit is allowed. """ return self._bool_exit
[docs] def cancel_clicked(self): """ Handle cancel action by preventing exit. """ self._bool_exit = False self.cancel_signal.emit() self.close()
[docs] def do_not_save_clicked(self): """ Handle 'do not save' action by allowing exit without saving. """ self._bool_exit = True self.do_not_save_signal.emit() self.close()
[docs] def save_as_clicked(self): """ Handle save action by emitting save signal and allowing exit. """ self.save_as_signal.emit() self._bool_exit = True self.close()
[docs] class PopUpRemoveScan(QDialog): """ Dialog to confirm removal of a scan previously sent to the pipeline manager. Provides options to remove or keep a scan, with additional 'apply to all' functionality when multiple scans are involved. Is called when the user wants to remove a scan that was previously sent to the pipeline manager. .. Methods: - cancel_clicked: - no_all_clicked: - yes_all_clicked: - yes_clicked: """
[docs] def __init__(self, scan, size): """ Initialize the remove scan confirmation dialog. :param scan: Identifier of the scan to be potentially removed. :param size: Total number of scans in the removal process. """ super().__init__() self.setWindowTitle("Document exists in Pipeline Manager") # Flags to control removal behavior self.stop = False self.repeat = False # Create confirmation message label = QLabel( f"The document {scan} \nwas previously sent to the pipeline " f"manager, do you really want to delete it?" ) # Create buttons push_button_yes = QPushButton("Ok", self) push_button_cancel = QPushButton("No", self) # Setup layout hbox = QHBoxLayout() hbox.addStretch(1) hbox.addWidget(push_button_yes) hbox.addWidget(push_button_cancel) # Add 'apply to all' buttons if multiple scans if size > 1: push_button_yes_all = QPushButton("Ok to all", self) push_button_no_all = QPushButton("No to all", self) hbox.addWidget(push_button_yes_all) hbox.addWidget(push_button_no_all) push_button_yes_all.clicked.connect(self.yes_all_clicked) push_button_no_all.clicked.connect(self.no_all_clicked) hbox.addStretch(1) # Create main layout vbox = QVBoxLayout() vbox.addWidget(label) vbox.addLayout(hbox) self.setLayout(vbox) # Connect standard buttons push_button_yes.clicked.connect(self.yes_clicked) push_button_cancel.clicked.connect(self.cancel_clicked)
[docs] def cancel_clicked(self): """ Handle 'Cancel' action. Sets stop flag to True and indicates no global action. """ self.stop = True self.repeat = False self.close()
[docs] def no_all_clicked(self): """ Handle 'No to All' action. Sets stop flag to True and indicates a global 'No' action. """ self.stop = True self.repeat = True self.close()
[docs] def yes_all_clicked(self): """ Handle 'Yes to All' action. Clears stop flag and indicates a global 'Yes' action. """ self.stop = False self.repeat = True self.close()
[docs] def yes_clicked(self): """ Handle 'Yes' action. Clears both stop and repeat flags for a single item removal. """ self.stop = False self.repeat = False self.close()
[docs] class PopUpRemoveTag(QDialog): """ Dialog for removing user-defined tags from a Populse MIA project. Allows users to select and remove custom tags from the project's database. .. Methods: - ok_action: Verifies the selected tags and send the information to the data browser. - search_str: Matches the searched pattern with the tags of the project. .. Signals: - signal_remove_tag: Qt signal emitted when tags are removed. """ # Signal that will be emitted at the end to tell that # the project has been created signal_remove_tag = QtCore.pyqtSignal()
[docs] def __init__(self, databrowser, project): """ Initialize the remove tag dialog. :param databrowser: Data browser instance managing project data. :param project: Current project containing tags to be removed. """ super().__init__() self.databrowser = databrowser self.project = project self.setWindowTitle("Remove a tag") self.setModal(True) self._translate = QtCore.QCoreApplication.translate self.setObjectName("Remove a tag") # The 'OK' push button self.push_button_ok = QPushButton(self) self.push_button_ok.setObjectName("push_button_ok") self.push_button_ok.setText(self._translate("Remove a tag", "OK")) # The "Tag list" label self.label_tag_list = QLabel(self) self.label_tag_list.setTextFormat(QtCore.Qt.AutoText) self.label_tag_list.setObjectName("label_tag_list") self.label_tag_list.setText( self._translate("Remove a tag", "Available tags:") ) # Search bar self.search_bar = QLineEdit(self) self.search_bar.setObjectName("lineEdit_search_bar") self.search_bar.setPlaceholderText("Search") # Tag list widget self.list_widget_tags = QListWidget(self) self.list_widget_tags.setObjectName("listWidget_tags") self.list_widget_tags.setSelectionMode( QAbstractItemView.MultiSelection ) # Layout setup hbox_buttons = QHBoxLayout() hbox_buttons.addStretch(1) hbox_buttons.addWidget(self.push_button_ok) hbox_top = QHBoxLayout() hbox_top.addWidget(self.label_tag_list) hbox_top.addStretch(1) hbox_top.addWidget(self.search_bar) vbox = QVBoxLayout() vbox.addLayout(hbox_top) vbox.addWidget(self.list_widget_tags) vbox.addLayout(hbox_buttons) self.setLayout(vbox) with self.project.database.data() as database_data: user_tags = [ tag["index"].split("|")[1] for tag in database_data.get_field_attributes( COLLECTION_CURRENT ) if tag["origin"] == TAG_ORIGIN_USER ] for tag in user_tags: QListWidgetItem( self._translate("Dialog", tag), self.list_widget_tags ) # Connect signals self.push_button_ok.clicked.connect(self.ok_action) self.search_bar.textChanged.connect(self.search_str)
[docs] def ok_action(self): """ Process selected tags for removal and update the data browser. Retrieves selected tags and passes them to the data browser for removal. Closes the dialog after processing. """ self.accept() # Collect selected tag names self.tag_names_to_remove = [ item.text() for item in self.list_widget_tags.selectedItems() ] # Remove selected tags self.databrowser.remove_tag_infos(self.tag_names_to_remove) self.close()
[docs] def search_str(self, search_pattern): """ Filter tags based on search pattern. :param search_pattern: String to match against tag names. """ with self.project.database.data() as database_data: field_attributes = database_data.get_field_attributes( COLLECTION_CURRENT ) # Filter tags based on search pattern matching_tags = [ tag["index"].split("|")[1] for tag in field_attributes if ( tag["origin"] == TAG_ORIGIN_USER and ( not search_pattern or search_pattern.upper() in tag["index"].split("|")[1].upper() ) ) ] # Update list widget self.list_widget_tags.clear() for tag in matching_tags: QListWidgetItem( self._translate("Dialog", tag), self.list_widget_tags )
[docs] class PopUpSaveProjectAs(QDialog): """ Dialog for saving a project under a new name. Provides a user interface to select and save a project with a new name, with options to browse existing projects and validate the new project name. .. Method: - fill_input: Fills the input field when a project is clicked on - return_value: Sets the widget's attributes depending on the selected file name .. Signals: - signal_saved_project: Qt signal emitted when a new project name is selected. """ # Signal that will be emitted at the end to tell # that the new file name has been chosen signal_saved_project = QtCore.pyqtSignal()
[docs] def __init__(self): """ Initialize the save project as dialog. Sets up the user interface with a scrollable list of existing projects, input field for new project name, and save/cancel buttons. """ super().__init__() self.setWindowTitle("Save project as") self.validate = False # Configuration and path setup self.config = Config() self.project_path = self.config.get_projects_save_path() # Project name input self.new_project = QLineEdit() self.new_project_label = QLabel("New project name") # Projects list setup self.v_box = QVBoxLayout() project_list = sorted( [ proj for proj in os.listdir(self.project_path) if os.path.isdir(os.path.join(self.project_path, proj)) ] ) for project_name in project_list: label = QLabel_clickable(project_name) label.clicked.connect(partial(self.fill_input, project_name)) self.v_box.addWidget(label) # Scroll area for projects list self.scroll = QScrollArea() self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) self.scroll.setMaximumHeight(250) self.final = QVBoxLayout() self.final.addWidget(self.scroll) # Wrap projects list in a widget self.widget = QWidget() self.widget.setLayout(self.v_box) self.scroll.setWidget(self.widget) # Create input layout self.h_box_text = QHBoxLayout() self.h_box_text.addWidget(self.new_project_label) self.h_box_text.addWidget(self.new_project) self.h_box_bottom = QHBoxLayout() self.h_box_bottom.addStretch(1) # Save button self.push_button_ok = QPushButton("Save as") self.push_button_ok.setObjectName("pushButton_ok") self.push_button_ok.clicked.connect(self.return_value) self.h_box_bottom.addWidget(self.push_button_ok) # Cancel button self.push_button_cancel = QPushButton("Cancel") self.push_button_cancel.setObjectName("pushButton_cancel") self.push_button_cancel.clicked.connect(self.close) self.h_box_bottom.addWidget(self.push_button_cancel) # Final layout self.final_layout = QVBoxLayout() self.final_layout.addWidget(QLabel("Projects list:")) self.final_layout.addLayout(self.final) self.final_layout.addLayout(self.h_box_text) self.final_layout.addLayout(self.h_box_bottom) self.setLayout(self.final_layout)
[docs] def fill_input(self, name): """ Fill the project name input field with the selected project name. :param name: Name of the project to fill in the input field. """ self.new_project.setText(name)
[docs] def return_value(self): """ Validate and process the selected project name. Checks project name validity, handles potential naming conflicts, and emits a signal when a valid project name is selected. :return (str): Full path of the new project if successful, None otherwise. """ # import message_already_exists only here to prevent circular # import issue from populse_mia.utils import message_already_exists file_name = self.new_project.text().strip() if not file_name: return None projects_folder = self.project_path entire_path = os.path.abspath(os.path.join(projects_folder, file_name)) # Set path attributes self.path, self.name = os.path.split(entire_path) self.total_path = entire_path self.relative_path = os.path.relpath(entire_path) self.relative_subpath = os.path.relpath(self.path) # Handle project name conflicts if not os.path.exists(self.relative_path): self.signal_saved_project.emit() self.validate = True self.close() return entire_path # Project already exists - handle based on user mode if self.config.get_user_mode(): message_already_exists() return None # Confirm project overwrite msgtext = ( f"Do you really want to overwrite the {file_name} " f"project?\nThis action will delete all contents " f"inside this folder!" ) msg = QMessageBox() msg.setIcon(QMessageBox.Warning) reply = msg.question( self, "populse_mia - Warning: Overwriting project", msgtext, QMessageBox.Yes | QMessageBox.No, ) if reply == QMessageBox.Yes: self.validate = True self.close() return entire_path return None
[docs] class PopUpSeeAllProjects(QDialog): """ A dialog window for displaying and managing saved projects. This dialog allows users to view a list of saved projects, check their existence, and open a selected project. .. Methods: - checkState: Checks if the project still exists and returns the corresponding icon - item_to_path: Returns the path of the first selected item - open_project: Switches to the selected project """
[docs] def __init__(self, saved_projects, main_window): """ Initialize the PopUpSeeAllProjects dialog. :param saved_projects: Container with a list of project paths. :param main_window: Main window. """ super().__init__() self.mainWindow = main_window self.setWindowTitle("See all saved projects") self.setMinimumWidth(500) # Create and configure widgets self.label = QLabel("List of Saved Projects") self.treeWidget = QTreeWidget() self.treeWidget.setColumnCount(3) self.treeWidget.setHeaderLabels(["Name", "Path", "State"]) for path in saved_projects.pathsList: item = QTreeWidgetItem() item.setText(0, os.path.basename(path)) item.setText(1, os.path.abspath(path)) item.setIcon(2, self.checkState(path)) self.treeWidget.addTopLevelItem(item) header = self.treeWidget.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) # Create buttons self.pushButtonOpenProject = QPushButton("Open project") self.pushButtonOpenProject.setObjectName("pushButton_ok") self.pushButtonOpenProject.clicked.connect(self.open_project) self.pushButtonCancel = QPushButton("Cancel") self.pushButtonCancel.setObjectName("pushButton_cancel") self.pushButtonCancel.clicked.connect(self.close) # Button layout self.hBoxButtons = QHBoxLayout() self.hBoxButtons.addStretch(1) self.hBoxButtons.addWidget(self.pushButtonOpenProject) self.hBoxButtons.addWidget(self.pushButtonCancel) # Main vertical layout self.vBox = QVBoxLayout() self.vBox.addWidget(self.label) self.vBox.addWidget(self.treeWidget) self.vBox.addLayout(self.hBoxButtons) self.setLayout(self.vBox)
[docs] def checkState(self, path): """ Determine the icon based on project existence. :param path (str): Path to the project directory. :return: QIcon: Green checkmark if project exists, red cross if not. """ sources_images_dir = Config().getSourceImageDir() icon_name = "green_v.png" if os.path.exists(path) else "red_cross.png" return QtGui.QIcon(os.path.join(sources_images_dir, icon_name))
[docs] def item_to_path(self): """ Returns the path of the first selected item. :return (str): Absolute path of the selected project, or empty string if no selection. """ selected_items = self.treeWidget.selectedItems() if not selected_items: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("Please select a project to open") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return "" return selected_items[0].text(1)
[docs] def open_project(self): """ Attempt to switch to the selected project. Opens the selected project in the main window if a valid project is chosen. Closes the dialog upon successful project switch. """ file_name = self.item_to_path() if file_name: self.path, self.name = os.path.split(os.path.abspath(file_name)) self.relative_path = os.path.relpath(file_name) if self.mainWindow.switch_project(self.relative_path, self.name): self.accept() self.close()
[docs] class PopUpSelectFilter(PopUpFilterSelection): """ Popup window for selecting and opening a previously saved filter. .. Methods: - ok_clicked: Saves the modifications and updates the data browser. """
[docs] def __init__(self, project, databrowser): """ Initializes the PopUpSelectFilter dialog :param project (Project): The current project instance. :param databrowser (DataBrowser): The data browser instance. """ super().__init__(project) self.project = project self.databrowser = databrowser self.config = Config() self.setWindowTitle("Open a filter") # Populate the filter list for filter_obj in self.project.filters: item = QListWidgetItem(filter_obj.name) self.list_widget_filters.addItem(item)
[docs] def ok_clicked(self): """ Saves the selected filter as the current filter and updates the data browser. """ selected_items = self.list_widget_filters.selectedItems() if selected_items: filter_name = selected_items[0].text() self.project.setCurrentFilter(self.project.getFilter(filter_name)) self.databrowser.open_filter_infos() self.accept() self.close()
[docs] class PopUpSelectIteration(QDialog): """ Dialog for selecting values to iterate over when running an iterated pipeline. .. Methods: - ok_clicked: Stores selected values and closes the dialog. """
[docs] def __init__(self, iterated_tag, tag_values): """ Initializes the selection popup. :param iterated_tag (str): The name of the tag being iterated. :param tag_values (list[str]): The possible values for the iterated tag. """ super().__init__() self.iterated_tag = iterated_tag self.tag_values = tag_values self.final_values = [] self.setWindowTitle( f"Iterate pipeline run over tag {self.iterated_tag}" ) self.v_box = QVBoxLayout() self.v_box.addWidget(QLabel("Select values to iterate over:")) self.check_boxes = [] for tag_value in self.tag_values: check_box = QCheckBox(tag_value) check_box.setCheckState(QtCore.Qt.Checked) self.check_boxes.append(check_box) self.v_box.addWidget(check_box) self.h_box_bottom = QHBoxLayout() self.h_box_bottom.addStretch(1) # The 'OK' push button self.push_button_ok = QPushButton("OK") self.push_button_ok.setObjectName("pushButton_ok") self.push_button_ok.clicked.connect(self.ok_clicked) # The 'Cancel' push button self.push_button_cancel = QPushButton("Cancel") self.push_button_cancel.setObjectName("pushButton_cancel") self.push_button_cancel.clicked.connect(self.close) self.h_box_bottom.addWidget(self.push_button_ok) self.h_box_bottom.addWidget(self.push_button_cancel) self.scroll = QScrollArea() self.widget = QWidget() self.widget.setLayout(self.v_box) self.scroll.setWidget(self.widget) self.final = QVBoxLayout() self.final.addWidget(self.scroll) self.final_layout = QVBoxLayout() self.final_layout.addLayout(self.final) self.final_layout.addLayout(self.h_box_bottom) self.setLayout(self.final_layout)
[docs] def ok_clicked(self): """Stores selected values and closes the dialog.""" self.final_values = [ cb.text() for cb in self.check_boxes if cb.isChecked() ] self.accept() self.close()
[docs] class PopUpTagSelection(QDialog): """ A dialog for selecting and filtering tags in a data browser. This class provides a user interface for: - Displaying available tags - Searching through tags - Selecting a single tag Is called when the user wants to update the tags that are visualized in the data browser. .. Methods: - _create_button: Create a standard button with text and click handler - _setup_ui: Set up the user interface components - cancel_clicked: closes the pop-up - item_clicked: checks the checkbox of an item when the latter is clicked - ok_clicked: actions when the "OK" button is clicked - search_str: matches the searched pattern with the tags of the project """
[docs] def __init__(self, project): """ Initialize the tag selection dialog. :param project: The current project containing the database with available tags """ super().__init__() self.project = project _translate = QtCore.QCoreApplication.translate # Create UI components self._setup_ui(_translate)
def _setup_ui(self, _translate): """ Set up the user interface components. :param_translate (callable): Localization translation function """ # The "Tag list" label self.label_tag_list = QLabel(self) self.label_tag_list.setTextFormat(QtCore.Qt.AutoText) self.label_tag_list.setObjectName("label_tag_list") self.label_tag_list.setText( _translate("main_window", "Available tags:") ) # The search bar to search in the list of tags self.search_bar = QLineEdit(self) self.search_bar.setObjectName("lineEdit_search_bar") self.search_bar.setPlaceholderText("Search") self.search_bar.textChanged.connect(self.search_str) # The list of tags self.list_widget_tags = QListWidget(self) self.list_widget_tags.setObjectName("listWidget_tags") self.list_widget_tags.setSelectionMode( QAbstractItemView.SingleSelection ) self.list_widget_tags.itemClicked.connect(self.item_clicked) # OK and Cancel buttons self.push_button_ok = self._create_button("OK", self.ok_clicked) self.push_button_cancel = self._create_button( "Cancel", self.cancel_clicked ) # Top layout with label and search bar hbox_top_left = QHBoxLayout() hbox_top_left.addWidget(self.label_tag_list) hbox_top_left.addWidget(self.search_bar) # Vertical layout for top section vbox_top_left = QVBoxLayout() vbox_top_left.addLayout(hbox_top_left) vbox_top_left.addWidget(self.list_widget_tags) # Buttons layout hbox_buttons = QHBoxLayout() hbox_buttons.addStretch(1) hbox_buttons.addWidget(self.push_button_ok) hbox_buttons.addWidget(self.push_button_cancel) # Final vertical layout vbox_final = QVBoxLayout() vbox_final.addLayout(vbox_top_left) vbox_final.addLayout(hbox_buttons) self.setLayout(vbox_final) def _create_button(self, text, clicked_handler): """ Create a standard button with text and click handler. :param text (str): Button text :param clicked_handler (callable): Function to call when button is clicked :return (QPushButton): Configured button """ button = QPushButton(self) button.setObjectName(f"pushButton_{text}") button.setText(text) button.clicked.connect(clicked_handler) return button
[docs] def cancel_clicked(self): """Close the dialog without selecting a tag.""" self.close()
[docs] def item_clicked(self, item): """ Handle item selection by checking/unchecking tags. :param (QListWidgetItem): The clicked list item """ for idx in range(self.list_widget_tags.count()): itm = self.list_widget_tags.item(idx) itm.setCheckState( QtCore.Qt.Checked if itm == item else QtCore.Qt.Unchecked )
[docs] def ok_clicked(self): """ Placeholder method to be overridden by subclasses. Defines actions to take when the OK button is clicked. """ # Has to be override in the PopUpSelectTag* classes pass
[docs] def search_str(self, str_search): """ Filter tags based on search term. :param str_search (str): Text to search for in tag names """ with self.project.database.data() as database_data: field_names = database_data.get_field_names(COLLECTION_CURRENT) # Filter tags based on search term filtered_tags = [ tag for tag in field_names if tag not in [TAG_CHECKSUM, COLLECTION_HISTORY] and (not str_search or str_search.upper() in tag.upper()) ] # Update visibility of list items for idx in range(self.list_widget_tags.count()): item = self.list_widget_tags.item(idx) item.setHidden(item.text() not in filtered_tags)
[docs] class PopUpSelectTag(PopUpTagSelection): """ A dialog for selecting and updating the thumbnail tag in the mini viewer. This class allows users to choose which tag will be displayed as the thumbnail in the application's mini viewer. It presents a list of available tags and allows selecting a single tag to be used as the thumbnail. .. Methods: - _populate_tag_list: Populate the list widget with tags from the database. - ok_clicked: saves the modifications and updates the mini viewer """
[docs] def __init__(self, project): """ Initialize the tag selection dialog. :param project: The current project in the software context. """ super().__init__(project) self.project = project self.config = Config() with self.project.database.data() as database_data: self._populate_tag_list( database_data.get_field_names(COLLECTION_CURRENT) )
def _populate_tag_list(self, field_names): """ Populate the list widget with tags from the database. :param field_names (list): List of available field names/tags. """ # Filter out special tags and create checkable list items filtered_tags = [ tag for tag in field_names if tag not in {TAG_CHECKSUM, TAG_HISTORY} ] current_thumbnail_tag = self.config.getThumbnailTag() for tag in sorted(filtered_tags): item = QListWidgetItem(tag) item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) # Set initial check state based on current thumbnail tag check_state = ( QtCore.Qt.Checked if tag == current_thumbnail_tag else QtCore.Qt.Unchecked ) item.setCheckState(check_state) self.list_widget_tags.addItem(item) self.list_widget_tags.sortItems()
[docs] def ok_clicked(self): """ Save the selected tag and update the mini viewer. Finds the checked tag and sets it as the new thumbnail tag. Closes the dialog after selection. """ for idx in range(self.list_widget_tags.count()): item = self.list_widget_tags.item(idx) if item.checkState() == QtCore.Qt.Checked: self.config.setThumbnailTag(item.text()) break # Close the dialog self.accept() self.close()
[docs] class PopUpSelectTagCountTable(PopUpTagSelection): """ A pop-up dialog for selecting a tag from a count table. Allows users to choose a single tag from a list of available tags, with an option to pre-select a specific tag. .. Methods: - ok_clicked: updates the selected tag and closes the pop-up """
[docs] def __init__(self, project, tags_to_display, tag_name_checked=None): """ Initialize the tag selection pop-up. :param project: The current project context. :param tags_to_display (list): List of tags to be displayed for selection. :param tag_name_checked (str): Optional tag to be pre-checked on initialization. """ super().__init__(project) self.selected_tag = None # Filtered tags, excluding specific system tags filterable_tags = [ tag for tag in tags_to_display if tag not in {TAG_CHECKSUM, TAG_HISTORY} ] # Populate list widget with checkable items for tag in sorted(filterable_tags): item = QListWidgetItem(tag) item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) # Set initial check state check_state = ( QtCore.Qt.Checked if tag == tag_name_checked else QtCore.Qt.Unchecked ) item.setCheckState(check_state) self.list_widget_tags.addItem(item) self.list_widget_tags.sortItems()
[docs] def ok_clicked(self): """ Determine the selected tag and close the dialog. Finds the first checked item and sets it as the selected tag, then closes the dialog. """ for idx in range(self.list_widget_tags.count()): item = self.list_widget_tags.item(idx) if item.checkState() == QtCore.Qt.Checked: self.selected_tag = item.text() break # Close the dialog self.accept() self.close()
[docs] class PopUpShowHistory(QDialog): """ A dialog for displaying the history of a document in a software pipeline. This class creates a popup window that provides comprehensive information about a specific brick (processing node) in a pipeline, including: - Pipeline visualization - Input and output parameters - Initialization and execution details The dialog allows users to: - View the pipeline structure - Inspect node details - Navigate between associated bricks - Select and highlight specific files .. Methods: - _updateio_table: fill in the input and output sections of the table - adjust_size: Adjust the size of the dialog based on screen resolution - file_clicked: close the history window and select the file in the data browser - find_associated_bricks: Find bricks associated with a given node name. - find_process_from_plug: Find the process and plug name from a given plug. - handle_pipeline_nodes: Handle pipeline nodes and initialize the pipeline visualization - highlight_selected_node: Highlight the selected node in the pipeline view - initialize_pipeline: Initialize the pipeline view using the given pipeline XML. - io_value_is_scan: Checks if the I/O value is a scan - load_data: Load data from the project database - load_pipeline_data: Load pipeline data from the database - node_selected: Handle node selection and update the table. - select_node: Select the node in the pipeline view based on the provided brick UUID - setup_ui: Set up the user interface components - update_table: Update the brick row at the bottom of the table. - update_table_for_single_brick: Update the table for a single brick - update_table_for_subpipeline: Update the table for a subpipeline - update_table_with_brick_data: Update the table with the brick's input and output data after processing """
[docs] def __init__(self, project, brick_uuid, scan, databrowser, main_window): """ Initialize the document history popup. :param project: Current project in the software :param brick_uuid (str): Unique identifier of the brick :param scan (str): Filename of the scan :param databrowser: Data browser instance :param main_window: Main window of the software """ super().__init__() # We do not want few parameters in the outputs parameters display self.banished_param = ["notInDb", "dict4runtime"] self.setModal(False) self.setWindowFlags( self.windowFlags() & QtCore.Qt.WindowStaysOnBottomHint ) self.databrowser = databrowser self.main_window = main_window self.project = project self.setWindowTitle(f"History of {scan}") self.setup_ui() self.load_data(brick_uuid, scan)
def _updateio_table(self, io_dict, item_idx): """ Populate the table's input and output sections with given dictionary data. This method dynamically creates table headers and cell widgets based on the input dictionary, handling nested lists and detecting scanned file paths. :param io_dict (dict): Dictionary containing input or output data to be displayed. Keys represent column headers, and values can be strings, lists, or nested lists. :param item_idx (int): The starting column index for populating the table :return (int): The updated column index after processing the dictionary. """ for key, value in sorted(io_dict.items()): item = QTableWidgetItem(key) self.table.setHorizontalHeaderItem(item_idx, item) if isinstance(value, list) and value: widget = QWidget() v_layout = QVBoxLayout() v_layout.setAlignment(QtCore.Qt.AlignTop) label = QLabel("[") v_layout.addWidget(label) for sub_value in value: if isinstance(sub_value, list) and sub_value: label = QLabel("[") v_layout.addWidget(label) for sub_sub_value in sub_value: sub_sub_value = str(sub_sub_value) value_scan = self.io_value_is_scan(sub_sub_value) if value_scan is None: del v_layout del label v_layout = QVBoxLayout() v_layout.setAlignment( QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop ) label = QLabel(str(value)) v_layout.addWidget(label) break else: h_layout = QHBoxLayout() h_layout.setAlignment(QtCore.Qt.AlignLeft) button = QPushButton(value_scan) button.clicked.connect(self.file_clicked) h_layout.addWidget(button) label = QLabel(",") h_layout.addWidget(label) v_layout.addLayout(h_layout) else: label = QLabel("],") v_layout.addWidget(label) continue break else: sub_value = str(sub_value) value_scan = self.io_value_is_scan(sub_value) if value_scan is None: del v_layout del label v_layout = QVBoxLayout() v_layout.setAlignment( QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop ) label = QLabel(str(value)) v_layout.addWidget(label) break else: h_layout = QHBoxLayout() button = QPushButton(value_scan) button.clicked.connect(self.file_clicked) h_layout.addWidget(button) label = QLabel(",") h_layout.addWidget(label) v_layout.addLayout(h_layout) else: label = QLabel("]") v_layout.addWidget(label) widget.setLayout(v_layout) self.table.setCellWidget(0, item_idx, widget) else: value_scan = self.io_value_is_scan(str(value)) if value_scan is not None: widget = QWidget() v_layout = QVBoxLayout() button = QPushButton(value_scan) button.clicked.connect(self.file_clicked) v_layout.addWidget(button) v_layout.setAlignment( QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop ) widget.setLayout(v_layout) self.table.setCellWidget(0, item_idx, widget) else: widget = QWidget() v_layout = QVBoxLayout() v_layout.setAlignment( QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop ) label = QLabel(str(value)) v_layout.addWidget(label) widget.setLayout(v_layout) self.table.setCellWidget(0, item_idx, widget) item_idx += 1 return item_idx
[docs] def adjust_size(self): """Adjust the size of the dialog based on screen resolution.""" screen_resolution = QApplication.instance().desktop().screenGeometry() width, height = screen_resolution.width(), screen_resolution.height() self.setGeometry(300, 200, round(0.6 * width), round(0.4 * height))
[docs] def file_clicked(self): """ Close the history window and select the file in the data browser. """ file = self.sender().text() self.databrowser.table_data.clearSelection() row_to_select = self.databrowser.table_data.get_scan_row(file) self.databrowser.table_data.selectRow(row_to_select) item_to_scroll_to = self.databrowser.table_data.item(row_to_select, 0) self.databrowser.table_data.scrollToItem(item_to_scroll_to) self.close()
[docs] def find_associated_bricks(self, node_name): """ Find bricks associated with a given node name. :param node_name (str): The name of the node to find associated bricks for. :return (dict): A dictionary where the keys are the full brick names and the values are lists of associated UUIDs. """ bricks = {} for uuid in self.brick_list: with self.project.database.data() as database_data: full_brick_name = database_data.get_value( collection_name=COLLECTION_BRICK, primary_key=uuid, field=BRICK_NAME, ) list_full_brick_name = full_brick_name.split(".") if self.unitary_pipeline: list_full_brick_name.pop(0) if list_full_brick_name[0] == node_name: bricks.setdefault(full_brick_name, []).append(uuid) return bricks
[docs] def find_process_from_plug(self, plug): """ Find the process and plug name from a given plug. :param plug (Plug): The plug object to find the process and plug name from. :return (tuple): A tuple containing the process name (str) and plug name (str). """ process_name = "" plug_name = "" for link in plug.links_from if plug.output else plug.links_to: process_name = f"{process_name}.{link[2].name}" plug_name = link[1] if isinstance(link[2], PipelineNode): sub_process_name, plug_name = self.find_process_from_plug( link[2].plugs[link[1]] ) process_name += sub_process_name break return process_name, plug_name
[docs] def handle_pipeline_nodes(self, pipeline, full_brick_name): """ Handle pipeline nodes, set up the view, and initialize the pipeline visualization. :param pipeline (Pipeline): The pipeline object containing the nodes to handle. :param full_brick_name (list): The full name of the brick, split into parts. """ # handle case of pipeline node alone --> exploded view # (e.g. a pipeline alone and plug exported) if len(pipeline.nodes) == 2: for key, node in pipeline.nodes.items(): if key and isinstance(node, PipelineNode): pipeline = node.process full_brick_name.pop(0) self.unitary_pipeline = True break if ( not self.unitary_pipeline and pipeline.name != "CustomPipeline" ) or (len(full_brick_name) == 2 and full_brick_name[1] == "main"): full_brick_name.pop(0) self.unitary_pipeline = True self.pipeline_view = PipelineDeveloperView( pipeline, allow_open_controller=False ) self.pipeline_view.auto_dot_node_positions() self.splitter.addWidget(self.pipeline_view) self.pipeline_view.node_clicked.connect(self.node_selected) self.pipeline_view.process_clicked.connect(self.node_selected) bricks = self.find_associated_bricks(full_brick_name[0]) self.select_node(pipeline, bricks, full_brick_name)
[docs] def highlight_selected_node(self, node_name): """ Highlight the selected node in the pipeline view. :param node_name (str): The name of the node to highlight. """ for name, gnode in self.pipeline_view.scene.gnodes.items(): gnode.fonced_viewer(name != node_name)
[docs] def initialize_pipeline(self, full_brick_name): """ Initialize the pipeline view using the given pipeline XML. :param full_brick_name (list): The full name of the brick, split into parts. """ engine = Config.get_capsul_engine() try: pipeline = engine.get_process_instance(self.pipeline_xml) except Exception: pipeline = None if pipeline: self.handle_pipeline_nodes(pipeline, full_brick_name)
[docs] def io_value_is_scan(self, value): """ Check if the I/O value is a scan. :param value: I/O value :return: The scan corresponding to the value if it exists, None otherwise """ with self.project.database.data() as database_data: for scan in database_data.get_document_names(COLLECTION_CURRENT): if scan in str(value): return scan return None
[docs] def load_data(self, brick_uuid, scan): """ Load data from the project database and update the table with brick data. :param brick_uuid (str): The UUID of the brick to load. :param scan (str): The identifier of the scan associated with the brick. """ with self.project.database.data() as database_data: brick_row = database_data.get_document( collection_name=COLLECTION_BRICK, primary_keys=brick_uuid ) full_brick_name = database_data.get_value( collection_name=COLLECTION_BRICK, primary_key=brick_uuid, field=BRICK_NAME, ).split(".") history_uuid = database_data.get_value( collection_name=COLLECTION_CURRENT, primary_key=scan, field=TAG_HISTORY, ) self.unitary_pipeline = False self.uuid_idx = 0 if history_uuid: self.load_pipeline_data(history_uuid, full_brick_name) self.update_table_with_brick_data(brick_row, full_brick_name)
[docs] def load_pipeline_data(self, history_uuid, full_brick_name): """ Load pipeline data from the database based on the provided history UUID. :param history_uuid (str): The UUID of the history record to load pipeline data from. :param full_brick_name (list): The full name of the brick, split into parts. """ with self.project.database.data() as database_data: self.pipeline_xml = database_data.get_value( collection_name=COLLECTION_HISTORY, primary_key=history_uuid, field=HISTORY_PIPELINE, ) if self.pipeline_xml: self.brick_list = database_data.get_value( collection_name=COLLECTION_HISTORY, primary_key=history_uuid, field=HISTORY_BRICKS, ) self.initialize_pipeline(full_brick_name)
[docs] def node_selected(self, node_name, process): """ Handle node selection and update the table. :param node_name: node name :param process: process of the corresponding node """ if hasattr(process, "pipeline_node"): process = process.pipeline_node bricks = self.find_associated_bricks(node_name) full_node_name = getattr(process, "full_name", "") if node_name not in full_node_name: full_node_name = "" if bricks: if len(bricks) == 1: self.update_table_for_single_brick( bricks, node_name, full_node_name ) else: self.update_table_for_subpipeline( bricks, process, full_node_name ) self.highlight_selected_node(node_name)
[docs] def select_node(self, pipeline, bricks, full_brick_name): """ Select the node in the pipeline view based on the provided brick UUID. :param pipeline (Pipeline): The pipeline object containing the nodes to handle. :param bricks (dict): A dictionary of bricks with UUIDs as values. :param full_brick_name (list): The full name of the brick, split into parts. """ for bricks_uuids in bricks.values(): if bricks_uuids[self.uuid_idx] == full_brick_name[0]: break selected_name = full_brick_name[0] try: self.node_selected(selected_name, pipeline.nodes[selected_name]) except Exception as e: logger.warning( f"Error in naming association brick/pipeline, " f"cannot select node: {e}" )
[docs] def setup_ui(self): """Set up the user interface components.""" self.layout = QVBoxLayout() self.splitter = QSplitter(QtCore.Qt.Vertical) self.table = QTableWidget() self.table.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.table.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.layout.addWidget(self.splitter) self.setLayout(self.layout) self.adjust_size()
[docs] def update_table( self, inputs, outputs, brick_name, init="", init_time=None, exec="", exec_time=None, ): """ Updates the table with information about a brick's execution state. :param inputs (dict): Dictionary containing input data. :param outputs (dict): Dictionary containing output data. :param brick_name (str): Name of the brick. :param init (str, optional): Initialization status. :param init_time (Any, optional): Initialization timestamp. :param exec (str, optional): Execution status. :param exec_time (Any, optional): Execution timestamp. :Contains: - create_cell_widget: Creates a QWidget containing a vertically aligned QLabel """ self.table.clear() self.table.setRowCount(1) column_count = ( 1 + (2 if init else 0) + (2 if exec else 0) + len(inputs) + len(outputs) ) self.table.setColumnCount(column_count) def create_cell_widget(text): """ Creates a QWidget containing a vertically aligned QLabel with the given text. :param text (str): The text to display in the QLabel. :return (QWidget): A QWidget containing a QLabel with the specified text, aligned to the top within a vertical layout. """ widget = QWidget() layout = QVBoxLayout() layout.setAlignment(QtCore.Qt.AlignTop) layout.addWidget(QLabel(text)) widget.setLayout(layout) return widget # Brick name column item_idx = 0 self.table.setHorizontalHeaderItem( item_idx, QTableWidgetItem(BRICK_NAME) ) self.table.setCellWidget(0, item_idx, create_cell_widget(brick_name)) item_idx += 1 # Brick initialization columns if init: self.table.setHorizontalHeaderItem( item_idx, QTableWidgetItem(BRICK_INIT) ) self.table.setCellWidget(0, item_idx, create_cell_widget(init)) item_idx += 1 self.table.setHorizontalHeaderItem( item_idx, QTableWidgetItem(BRICK_INIT_TIME) ) self.table.setCellWidget( 0, item_idx, create_cell_widget(str(init_time) if init_time else ""), ) item_idx += 1 # Brick execution columns if exec: self.table.setHorizontalHeaderItem( item_idx, QTableWidgetItem(BRICK_EXEC) ) self.table.setCellWidget(0, item_idx, create_cell_widget(exec)) item_idx += 1 self.table.setHorizontalHeaderItem( item_idx, QTableWidgetItem(BRICK_EXEC_TIME) ) self.table.setCellWidget( 0, item_idx, create_cell_widget(str(exec_time) if exec_time else ""), ) item_idx += 1 # Update inputs and outputs item_idx = self._updateio_table(inputs, item_idx) _ = self._updateio_table(outputs, item_idx) # Final table adjustments self.table.verticalHeader().setMinimumSectionSize(30) self.table.resizeColumnsToContents() self.table.resizeRowsToContents()
[docs] def update_table_for_single_brick(self, bricks, node_name, full_node_name): """ Update the table for a single brick, using the provided brick data. :param bricks (dict): A dictionary of bricks with UUIDs as values. :param node_name (str): The name of the node associated with the brick. :param full_node_name (list): The full name of the node, split into parts. """ with self.project.database.data() as database_data: brick_row = database_data.get_document( collection_name=COLLECTION_BRICK, primary_keys=next(iter(bricks.values()))[self.uuid_idx], ) inputs = brick_row[0][BRICK_INPUTS] outputs = brick_row[0][BRICK_OUTPUTS] for param in self.banished_param: outputs.pop(param, None) inputs.pop(param, None) brick_name = brick_row[0][BRICK_NAME] init = brick_row[0][BRICK_INIT] init_time = brick_row[0][BRICK_INIT_TIME] exec = brick_row[0][BRICK_INIT] exec_time = brick_row[0][BRICK_INIT_TIME] self.update_table( inputs, outputs, brick_name, init, init_time, exec, exec_time )
[docs] def update_table_for_subpipeline(self, bricks, process, full_node_name): """ Update the table for a subpipeline based on the given process and brick data. :param bricks (dict): A dictionary of bricks with UUIDs as values. :param process (PipelineNode): The process node associated with the subpipeline. :param full_node_name (list): The full name of the node, split into parts. """ inputs_dict = {} outputs_dict = {} if isinstance(process, PipelineNode): with self.project.database.data() as database_data: for plug_name, plug in process.plugs.items(): if plug.activated: process_result = self.find_process_from_plug(plug) process_name = process_result[0] inner_plug_name = process_result[1] for uuid in bricks.values(): full_brick_name = database_data.get_value( collection_name=COLLECTION_BRICK, primary_key=uuid[0], field=BRICK_NAME, ) if full_brick_name == ( f"{full_node_name}{process_name}" ): plugs = database_data.get_value( collection_name=COLLECTION_BRICK, primary_key=uuid[self.uuid_idx], field=( BRICK_OUTPUTS if plug.output else BRICK_INPUTS ), ) (outputs_dict if plug.output else inputs_dict)[ plug_name ] = plugs[inner_plug_name] for param in self.banished_param: outputs_dict.pop(param, None) inputs_dict.pop(param, None) self.update_table(inputs_dict, outputs_dict, full_node_name)
[docs] def update_table_with_brick_data(self, brick_row, full_brick_name): """ Update the table with the brick's input and output data after processing. :param brick_row (list): A list containing the brick data to update the table with. :param full_brick_name (list): The full name of the brick, split into parts. """ inputs = brick_row[0][BRICK_INPUTS] outputs = brick_row[0][BRICK_OUTPUTS] for param in self.banished_param: outputs.pop(param, None) inputs.pop(param, None) brick_name = brick_row[0][BRICK_NAME] init = brick_row[0][BRICK_INIT] init_time = brick_row[0][BRICK_INIT_TIME] exec = brick_row[0][BRICK_INIT] exec_time = brick_row[0][BRICK_INIT_TIME] self.update_table( inputs, outputs, brick_name, init, init_time, exec, exec_time ) self.splitter.addWidget(self.table)
[docs] class PopUpVisualizedTags(QWidget): """ A widget for managing tag visualization preferences in a project. This class provides an interface for users to select and unselect tags to be displayed in the project. It allows searching through available tags and moving them between available and visualized lists. .. Methods: - _create_button: Create a customized QPushButton with specified properties - _create_button_layout: Create the layout for selection buttons - _create_label: Create a customized QLabel with specified properties - _create_left_layout: Create the layout for available tags - _create_right_layout: Create the layout for visualized tags - _create_search_bar: Create the search bar with placeholder and connection - _create_tag_list: Create a QListWidget configured for multi-selection of tags - _populate_tags: Populate the tags list from the project database - _setup_ui: Create and layout the user interface components - search_str: Matches the searched pattern with the tags of the project - click_select_tag: Puts the selected tags in the "selected tag" table - click_unselect_tag: removes the unselected tags from populse_mia "selected tag" table .. Signals: - signal_preferences_change (QtCore.pyqtSignal): Emitted when tag visualization preferences are modified. """ # Signal to indicate preference changes signal_preferences_change = QtCore.pyqtSignal()
[docs] def __init__(self, project, visualized_tags): """ Initialize the tag visualization management widget. :param project: The current project in the software. :param visualized_tags: Tags currently being visualized before opening this widget. """ super().__init__() self.project = project self.visualized_tags = visualized_tags self._translate = QtCore.QCoreApplication.translate # Track available (non-visualized) tags self.left_tags = [] # Setup the user interface self._setup_ui() self._populate_tags()
def _create_button(self, object_name, text, click_handler): """ Create a customized QPushButton with specified properties. This method instantiates a QPushButton, sets its object name, translates and sets its text, and connects a click event handler. :param object_name (str): The unique identifier name for the button. :param text (str): The text to be displayed on the button, will be translated. :param click_handler (callable): The function to be called when the button is clicked. :return (QPushButton): A configured button with the specified properties. """ button = QPushButton(self) button.setObjectName(object_name) button.setText(self._translate("main_window", text)) button.clicked.connect(click_handler) return button def _create_button_layout(self): """Create the layout for selection buttons.""" layout = QVBoxLayout() layout.addWidget(self.push_button_select_tag) layout.addWidget(self.push_button_unselect_tag) return layout def _create_label(self, object_name, text): """ Create a customized QLabel with specified properties. This method instantiates a QLabel, sets its text format, object name, and translates its text. :param object_name (str): The unique identifier name for the label. :param text (str): The text to be displayed on the label, will be translated. :return (QLabel): A configured label with the specified properties. """ label = QLabel(self) label.setTextFormat(QtCore.Qt.AutoText) label.setObjectName(object_name) label.setText(self._translate("main_window", text)) return label def _create_left_layout(self): """Create the layout for available tags.""" layout = QVBoxLayout() top_layout = QHBoxLayout() top_layout.addWidget(self.label_tag_list) top_layout.addWidget(self.search_bar) layout.addLayout(top_layout) layout.addWidget(self.list_widget_tags) return layout def _create_right_layout(self): """Create the layout for visualized tags.""" layout = QVBoxLayout() layout.addWidget(self.label_visualized_tags) layout.addWidget(self.list_widget_selected_tags) return layout def _create_search_bar(self): """Create the search bar with placeholder and connection.""" search_bar = QLineEdit(self) search_bar.setObjectName("lineEdit_search_bar") search_bar.setPlaceholderText("Search") search_bar.textChanged.connect(self.search_str) return search_bar def _create_tag_list(self, text=""): """ Create a QListWidget configured for multi-selection of tags. This method initializes a QListWidget with multi-selection mode nabled, allowing users to select multiple items simultaneously. The list widget is assigned a unique object name based on the optional text parameter. :param text (str): A prefix used to create a unique object name for the QListWidget. :return (QListWidget): A configured QListWidget with multi-selection mode enabled. """ tag_list = QListWidget(self) tag_list.setObjectName(f"listWidget_{text}tags") tag_list.setSelectionMode(QAbstractItemView.MultiSelection) return tag_list def _populate_tags(self): """Populate the tags list from the project database.""" excluded_tags = {TAG_CHECKSUM, TAG_FILENAME, TAG_HISTORY} with self.project.database.data() as database_data: for tag in database_data.get_field_names(COLLECTION_CURRENT): if tag not in excluded_tags: item = QListWidgetItem(tag) if tag not in self.visualized_tags: # Tag not visible: left side self.list_widget_tags.addItem(item) self.left_tags.append(tag) else: # Tag visible: right side self.list_widget_selected_tags.addItem(item) self.list_widget_tags.sortItems() def _setup_ui(self): """Create and layout the user interface components.""" # Selection buttons self.push_button_select_tag = self._create_button( "pushButton_select_tag", "-->", self.click_select_tag ) self.push_button_unselect_tag = self._create_button( "pushButton_unselect_tag", "<--", self.click_unselect_tag ) # Available tags section self.label_tag_list = self._create_label( "label_tag_list", "Available tags:" ) self.search_bar = self._create_search_bar() self.list_widget_tags = self._create_tag_list() # Visualized tags section self.label_visualized_tags = self._create_label( "label_visualized_tags", "Visualized tags:" ) self.list_widget_selected_tags = self._create_tag_list("visualized_") main_layout = QHBoxLayout() main_layout.addLayout(self._create_left_layout()) main_layout.addLayout(self._create_button_layout()) main_layout.addLayout(self._create_right_layout()) self.setLayout(main_layout)
[docs] def search_str(self, str_search): """ Filter tags based on search string. :param str_search (str): Search pattern to match against tags """ # Find matching tags, case-insensitive return_list = [ tag for tag in self.left_tags if not str_search or str_search.upper() in tag.upper() ] # Selection updated self.list_widget_tags.clear() for tag_name in return_list: item = QListWidgetItem(tag_name) self.list_widget_tags.addItem(item) self.list_widget_tags.sortItems()
[docs] def click_select_tag(self): """ Move selected tags from available to visualized list. Removes selected tags from the left (available) list and adds them to the right (visualized) list. """ rows = sorted( [index.row() for index in self.list_widget_tags.selectedIndexes()], reverse=True, ) for row in rows: tag_item = self.list_widget_tags.takeItem(row) tag_text = tag_item.text() # assuming the other listWidget is called listWidget_2 self.left_tags.remove(tag_text) self.list_widget_selected_tags.addItem(tag_text) # Emit signal to indicate preferences have changed self.signal_preferences_change.emit()
[docs] def click_unselect_tag(self): """ Remove selected tags from the visualized list and return them to the available tags list. Moves selected tags from the right (visualized) list to the left (available) list, maintaining sorted order. """ rows = sorted( [ index.row() for index in self.list_widget_selected_tags.selectedIndexes() ], reverse=True, ) for row in rows: # Add tag back to left tags and available list ( self.left_tags.append( self.list_widget_selected_tags.item(row).text() ) ) ( self.list_widget_tags.addItem( self.list_widget_selected_tags.takeItem(row) ) ) self.list_widget_tags.sortItems()
[docs] class QLabel_clickable(QLabel): """ A custom QLabel that emits a clicked signal when mouse pressed. This class extends the standard QLabel to provide a signal that can be connected to other methods when the label is clicked, enabling more interactive label behaviors. .. Signals: - clicked (pyqtSignal): Signal emitted when the label is clicked. """ clicked = QtCore.pyqtSignal()
[docs] def __init__(self, parent=None): """ Initialize the clickable label. :param parent (QWidget): Parent widget. """ super().__init__(parent)
[docs] def mousePressEvent(self, event): """ Override the default mouse press event to emit the clicked signal. :parm event (QMouseEvent): Mouse press event details. """ self.clicked.emit() super().mousePressEvent(event)