Source code for populse_mia.user_interface.pipeline_manager.type_editors
"""
Define the Mia logger.
The soma control classes are overloaded for the needs of Mia.
:Contains:
:Class:
- PopulseFileControlWidget
- PopulseDirectoryControlWidget
- PopulseOffscreenListFileControlWidget
- PopulseUndefinedControlWidget
"""
###############################################################################
# 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 logging
import os
from functools import partial
import traits.api as traits
from soma.qt_gui.controls.Directory import DirectoryControlWidget
from soma.qt_gui.controls.File import FileControlWidget
from soma.qt_gui.controls.List_File_offscreen import (
OffscreenListFileControlWidget,
)
from soma.qt_gui.qt_backend import Qt, QtGui, QtWidgets
from soma.utils.weak_proxy import weak_proxy
logger = logging.getLogger(__name__)
[docs]
class PopulseFileControlWidget(FileControlWidget):
"""
Widget control for selecting a file.
Provides methods to create a file selection widget, display a filter
dialog, and update plug values based on filter results.
:Contains:
:Method:
- create_widget: Method to create the file widget.
- filter_clicked: Display a filter widget.
- update_plug_value_from_filter: Update the plug value from
a filter result.
"""
[docs]
@staticmethod
def create_widget(
parent,
control_name,
control_value,
trait,
label_class=None,
user_data=None,
):
"""
Creates a file selection widget.
:param parent (QWidget): The parent widget.
:param control_name (str): The name of the control to create.
:param control_value (str): The default control value.
:param trait (Trait): The trait associated with the control.
:param label_class (Optional[Type[QWidget]]): Custom label widget
class.
:param user_data (Optional[dict]): Additional user data.
:return (Tuple[QWidget, QLabel]):
A tuple containing the created widget and its associated label.
The widget includes a QLineEdit ('path') and a browse
button ('browse').
"""
# Create the widget that will be used to select a file
widget, label = FileControlWidget.create_widget(
parent,
control_name,
control_value,
trait,
label_class=label_class,
user_data=user_data,
)
user_data = user_data or {}
# regular File does not store data
widget.user_data = user_data
layout = widget.layout()
project = user_data.get("project")
scan_list = user_data.get("scan_list")
connected_inputs = user_data.get("connected_inputs", set())
def _is_number(value):
"""
Checks if a value is a number.
:param value (str): The value to check.
:return (bool): True if the value is a number, False otherwise.
"""
try:
int(value)
return True
except ValueError:
return False
# files in a list don't get a Filter button.
if (
project
and scan_list
and not trait.output
and control_name not in connected_inputs
and not _is_number(control_name)
):
# Create a browse button
button = Qt.QPushButton("Filter", widget)
button.setObjectName("filter_button")
button.setStyleSheet(
"QPushButton#filter_button "
"{padding: 2px 10px 2px 10px; margin: 0px;}"
)
layout.addWidget(button)
widget.filter_b = button
# Set a callback on the browse button
control_class = parent.get_control_class(trait)
node_name = getattr(
parent.controller, "name", parent.controller.__class__.__name__
)
browse_hook = partial(
control_class.filter_clicked,
weak_proxy(widget),
node_name,
control_name,
)
widget.filter_b.clicked.connect(browse_hook)
return (widget, label)
[docs]
@staticmethod
def filter_clicked(widget, node_name, plug_name):
"""
Display a filter widget.
:param widget (QWidget): The parent widget.
:param node_name (str): The name of the node.
:param plug_name (str): The name of the plug.
"""
# This import is not at the beginning of the file to avoid a cyclic
# import issue.
from .node_controller import PlugFilter
project = widget.user_data.get("project")
scan_list = widget.user_data.get("scan_list")
main_window = widget.user_data.get("main_window")
node_controller = widget.user_data.get("node_controller")
widget.pop_up = PlugFilter(
project,
scan_list,
None,
node_name,
plug_name,
node_controller,
main_window,
)
widget.pop_up.setWindowModality(Qt.Qt.WindowModal)
widget.pop_up.show()
widget.pop_up.plug_value_changed.connect(
partial(
PopulseFileControlWidget.update_plug_value_from_filter,
widget,
plug_name,
)
)
[docs]
@staticmethod
def update_plug_value_from_filter(widget, plug_name, filter_res_list):
"""
Updates the plug value based on a filter result.
:param widget (QWidget): The parent widget.
:param plug_name (str): The name of the plug.
:param filter_res_list (List[str]): List of filtered file paths.
"""
# If the list contains only one element, setting
# this element as the plug value
len_list = len(filter_res_list)
if len_list == 1:
res = filter_res_list[0]
else:
res = traits.Undefined
if len_list > 1:
msg = QtWidgets.QMessageBox()
msg.setText(
f"The '{plug_name}' parameter must by a filename, "
f"but received {filter_res_list}."
)
msg.setIcon(QtWidgets.QMessageBox.Warning)
msg.setWindowTitle("TraitError")
msg.exec_()
# Set the selected file path to the path sub control
widget.path.set_value(str(res))
[docs]
class PopulseDirectoryControlWidget(DirectoryControlWidget):
"""
Widget for selecting a directory.
:Contains:
:Method:
- create_widget: Creates the directory selection widget.
- filter_clicked: Displays a filtering widget.
- update_plug_value_from_filter: Updates the plug value based on
the filter result.
"""
[docs]
@staticmethod
def create_widget(
parent,
control_name,
control_value,
trait,
label_class=None,
user_data=None,
):
"""
Creates and returns a directory selection widget.
:param parent (QWidget): The parent widget.
:param control_name (str): The name of the control.
:param control_value (Any): The initial value of the control.
:param trait (Any): The trait associated with the control.
:param label_class (Optional[Any]): The label class (optional).
:param user_data (Optional[dict]): User data associated with
the widget.
:return (QWidget): The directory selection widget.
"""
return PopulseFileControlWidget.create_widget(
parent,
control_name,
control_value,
trait,
label_class=label_class,
user_data=user_data,
)
[docs]
@staticmethod
def filter_clicked(widget, node_name, plug_name):
"""
Displays a filter widget for selecting a directory.
:param widget (QWidget): The calling widget.
:param node_name (str): The name of the node.
:param plug_name (str): The name of the associated plug.
"""
# this import is not at the beginning of the file to avoid a cyclic
# import issue.
from .node_controller import PlugFilter
project = widget.user_data.get("project")
scan_list = widget.user_data.get("scan_list")
main_window = widget.user_data.get("main_window")
node_controller = widget.user_data.get("node_controller")
widget.pop_up = PlugFilter(
project,
scan_list,
None,
node_name,
plug_name,
node_controller,
main_window,
)
widget.pop_up.show()
widget.pop_up.plug_value_changed.connect(
partial(
PopulseDirectoryControlWidget.update_plug_value_from_filter,
widget,
plug_name,
)
)
[docs]
@staticmethod
def update_plug_value_from_filter(widget, plug_name, filter_res_list):
"""
Updates the plug value based on the filter result.
If multiple elements are returned, the first one is selected.
If the selected element is not a directory, its parent directory
is used.
:param widget (QWidget): The widget being updated.
:param plug_name (str): The name of the associated plug.
:param filter_res_list (list[str]): The list of filtered files.
"""
if filter_res_list:
res = str(filter_res_list[0])
res = res if os.path.isdir(res) else os.path.dirname(res)
else:
res = traits.Undefined
# Set the selected file path to the path sub control
widget.path.setText(str(res))
[docs]
class PopulseOffscreenListFileControlWidget(OffscreenListFileControlWidget):
"""
A control widget for entering a list of files.
:Contains:
:Method:
- create_widget: Creates the list of files widget.
- filter_clicked: Displays a filter widget.
- update_plug_value_from_filter: Updates the plug value based on
the filter result.
"""
[docs]
@staticmethod
def create_widget(
parent,
control_name,
control_value,
trait,
label_class=None,
user_data=None,
):
"""
Creates and returns a file list control widget with an optional
filter button.
:param parent (QWidget): The parent widget.
:param control_name (str): The name of the control.
:param control_value (list): The default control value.
:param trait (Trait): The trait associated with the control.
:param label_class (type): A Qt widget class for the label.
:param user_data (dict): Additional data, including project,
scan list, and connected inputs.
:return (tuple): A tuple (control widget, (QLabel, QWidget)).
"""
widget, label = OffscreenListFileControlWidget.create_widget(
parent,
control_name,
control_value,
trait,
label_class=label_class,
user_data=user_data,
)
layout = widget.layout()
project = user_data.get("project")
scan_list = user_data.get("scan_list")
connected_inputs = user_data.get("connected_inputs", set())
if (
project
and scan_list
and not trait.output
and control_name not in connected_inputs
):
# Create a browse button
button = Qt.QPushButton("Filter", widget)
button.setObjectName("filter_button")
button.setStyleSheet(
"QPushButton#filter_button "
"{padding: 2px 10px 2px 10px; margin: 0px;}"
)
layout.addWidget(button)
widget.filter_b = button
# Set a callback on the browse button
control_class = parent.get_control_class(trait)
node_name = getattr(
parent.controller, "name", parent.controller.__class__.__name__
)
browse_hook = partial(
control_class.filter_clicked,
weak_proxy(widget),
node_name,
control_name,
)
# parameters, process)
widget.filter_b.clicked.connect(browse_hook)
return (widget, label)
[docs]
@staticmethod
def filter_clicked(widget, node_name, plug_name):
"""
Displays a filter widget for selecting files.
:param widget (QWidget): The file control widget.
:param node_name (str): The name of the node.
:param plug_name (str): The name of the plug.
"""
# this import is not at the beginning of the file to avoid a cyclic
# import issue.
from .node_controller import PlugFilter
project = widget.user_data.get("project")
scan_list = widget.user_data.get("scan_list")
main_window = widget.user_data.get("main_window")
node_controller = widget.user_data.get("node_controller")
widget.pop_up = PlugFilter(
project,
scan_list,
None,
node_name,
plug_name,
node_controller,
main_window,
)
widget.pop_up.show()
# fmt: off
widget.pop_up.plug_value_changed.connect(
partial(
(
PopulseOffscreenListFileControlWidget.
update_plug_value_from_filter
),
widget,
plug_name,
)
)
# fmt: on
[docs]
@staticmethod
def update_plug_value_from_filter(widget, plug_name, filter_res_list):
"""
Updates the plug value based on the filter results.
:param widget (QWidget): The file control widget.
:param plug_name (str): The name of the plug.
:param filter_res_list (list): The filtered file list.
"""
controller = widget.parent().controller
try:
setattr(controller, plug_name, filter_res_list)
except Exception as e:
logger.warning(e)
[docs]
class PopulseUndefinedControlWidget:
"""
Widget control for handling Undefined trait values in a Qt interface.
This class provides methods to create, validate, and update Qt widgets
that represent undefined values in a controller-based UI framework.
:Contains:
:Method:
- check: Check if a controller widget control is filled correctly.
- connect: Connect a 'Str' or 'String' controller trait and a
'StrControlWidget' controller widget control.
- create_widget: Method to create the Undefined widget.
- disconnect: Disconnect a 'Str' or 'String' controller trait and
a 'StrControlWidget' controller widget control.
- is_valid: Method to check if the new control value is correct.
- update_controller: Update one element of the controller.
- update_controller_widget: Update one element of the controller
widget.
"""
# Class constants
UNDEFINED_TEXT = "<undefined>"
STYLED_UNDEFINED_TEXT = (
"<style>background-color: gray; text-color: red;</style>" "<undefined>"
)
VALID_REPRESENTATIONS = [UNDEFINED_TEXT, STYLED_UNDEFINED_TEXT]
[docs]
@classmethod
def check(cls, control_instance):
"""
Check if a controller widget control is filled correctly.
This method is a placeholder in this implementation.
:param cls (StrControlWidget): A StrControlWidget control.
:param control_instance (QLineEdit): The control widget to check.
"""
# Implementation can be added here if needed
pass
[docs]
@classmethod
def connect(cls, controller_widget, control_name, control_instance):
"""
Connect a 'Str' or 'String' controller trait and a 'StrControlWidget'
controller widget control.
This method is a placeholder in this implementation.
:param cls (StrControlWidget): A StrControlWidget control.
:param controller_widget (ControllerWidget): The controller widget
containing the controller.
:param control_name (str): The name of the control to connect.
:param control_instance (QWidget): The widget instance to connect
"""
# Signal connections can be added here if needed
pass
[docs]
@staticmethod
def create_widget(
parent,
control_name,
label_class=None,
):
"""
Create a widget for displaying Undefined values.
:param parent (QWidget): The parent widget.
:param control_name (str): The name of the control to create.
:param label_class: The class to use for the label widget. Default is
QLabel if None.
:return (tuple): (control_widget, label_widget) where control_widget
is a QLabel displaying the Undefined value and
label_widget is the associated label.
"""
# Create widget with styled representation of Undefined
widget = Qt.QLabel(
PopulseUndefinedControlWidget.STYLED_UNDEFINED_TEXT, parent
)
# Create and return the label
if label_class is None:
label_class = QtGui.QLabel
if control_name is not None:
label = label_class(control_name, parent)
else:
label = None
return (widget, label)
[docs]
@staticmethod
def disconnect(controller_widget, control_name, control_instance):
"""
Disconnect a 'Str' or 'String' controller trait and a
'StrControlWidget' controller widget control.
This method is a placeholder in this implementation.
:param controller_widget (ControllerWidget): The controller widget
containing the controller.
:param control_name (str): The name of the control to disconnect
:param control_instance (QWidget): The widget instance to disconnect
"""
# Signal disconnections can be added here if needed
pass
[docs]
@staticmethod
def is_valid(control_instance, *args, **kwargs):
"""
Validate if the control contains an Undefined value representation.
*args, **kwargs : Additional arguments. Not used, kept for
interface compatibility.
:param control_instance (QWidget): The control widget to validate.
:return (bool): True if the control value is Undefined, False
otherwise
"""
# Get the control current value
control_text = control_instance.text()
return control_text in (
PopulseUndefinedControlWidget.VALID_REPRESENTATIONS
)
[docs]
@staticmethod
def update_controller(
controller_widget,
control_name,
control_instance,
reset_invalid_value,
*args,
**kwargs,
):
"""
Update the controller with the widget's value if valid.
At the end the controller trait value with the name 'control_name'
will match the controller widget user parameters defined in
'control_instance'.
*args, **kwargs : Additional arguments. Not used, kept for
interface compatibility.
:param controller_widget (ControllerWidget): The controller widget
containing the controller
to update
:param control_name (str): The name of the control to synchronize
with the controller
:param control_instance (QWidget): The widget instance to synchronize
with the controller
:param reset_invalid_value (bool): If True and the value is invalid,
reset the widget to the
controller's value
"""
# Update the controller only if the control is valid
if PopulseUndefinedControlWidget.is_valid(control_instance):
# Set controller's trait to Undefined
new_trait_value = traits.Undefined
setattr(
controller_widget.controller, control_name, new_trait_value
)
logger.info(
f"'PopulseUndefinedControlWidget' associated controller "
f"trait '{control_name}' has been updated with "
f"value '{new_trait_value}'."
)
elif reset_invalid_value:
# Invalid value, reset GUI to the previous value
old_trait_value = getattr(
controller_widget.controller, control_name
)
control_instance.setText(old_trait_value)
[docs]
@staticmethod
def update_controller_widget(
controller_widget, control_name, control_instance
):
"""
Update the widget to reflect the controller's value.
At the end the controller widget user editable parameter with the
name 'control_name' will match the controller trait value with the
same name.
:param controller_widget (ControllerWidget): The controller widget
containing the controller.
:param control_name (str): The name of the control to synchronize.
:param control_instance (QWidget): The widget instance to update.
"""
# Set the widget text to represent Undefined
new_controller_value = str(traits.Undefined)
control_instance.setText(new_controller_value)
logger.info(
f"'PopulseUndefinedControlWidget' has been updated "
f"with value '{new_controller_value}'."
)
# Update the controller to ensure consistency
PopulseUndefinedControlWidget.update_controller(
controller_widget, control_name, control_instance, True
)