Source code for mantidimaging.gui.utility.qt_helpers
# Copyright (C) 2022 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
"""
Module containing helper functions relating to PyQt.
"""
import os
from enum import IntEnum, auto
from logging import getLogger
from typing import Any, Tuple, Union, List, Callable
from PyQt5 import uic # type: ignore
from PyQt5.QtCore import QObject
from PyQt5.QtWidgets import (QLabel, QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QWidget, QSizePolicy, QAction,
QMenu, QPushButton, QLayout, QFileDialog, QComboBox)
from mantidimaging.core.utility import finder
MAX_SPIN_BOX = 2147483647
[docs]
class BlockQtSignals(object):
"""
Used to block Qt signals from a selection of QWidgets within a context.
"""
def __init__(self, q_objects: Union[QObject, List[QObject]]):
if not isinstance(q_objects, list):
q_objects = [q_objects]
for obj in q_objects:
assert isinstance(obj, QObject), \
"This class must be used with QObjects"
self.q_objects = q_objects
self.previous_values = None
def __enter__(self):
self.previous_values = \
[obj.blockSignals(True) for obj in self.q_objects]
def __exit__(self, *args):
for obj, prev in zip(self.q_objects, self.previous_values):
obj.blockSignals(prev)
[docs]
def compile_ui(ui_file, qt_obj=None):
base_path = finder.ROOT_PATH
return uic.loadUi(os.path.join(base_path, ui_file), qt_obj)
[docs]
def select_directory(field, caption):
assert isinstance(field, QLineEdit), ("The passed object is of type {0}. This function only works with "
"QLineEdit".format(type(field)))
# open file dialogue and set the text if file is selected
field.setText(QFileDialog.getExistingDirectory(caption=caption))
[docs]
def get_value_from_qwidget(widget: QWidget):
if isinstance(widget, QLineEdit):
return widget.text()
elif isinstance(widget, QSpinBox) or isinstance(widget, QDoubleSpinBox):
return widget.value()
elif isinstance(widget, QCheckBox):
return widget.isChecked()
[docs]
class Type(IntEnum):
STR = auto()
TUPLE = auto()
NONETYPE = auto()
LIST = auto()
FLOAT = auto()
INT = auto()
CHOICE = auto()
BOOL = auto()
LABEL = auto()
STACK = auto()
BUTTON = auto()
[docs]
def on_change_and_disable(widget: QWidget, on_change: Callable):
"""
Makes sure the widget is disabled while running the on_update method. This is required for spin boxes that
continue increasing when generating a preview image is computationally intensive.
:param widget: The widget to disable.
:param on_change: The method to call when the widget has been changed.
"""
widget.setEnabled(False)
on_change()
widget.setEnabled(True)
[docs]
def add_property_to_form(label: str,
dtype: Union[Type, str],
default_value=None,
valid_values=None,
tooltip=None,
on_change=None,
form=None,
filters_view=None,
run_on_press=None,
single_step_size=None) -> Tuple[Union[QLabel, QLineEdit], Any]:
"""
Adds a property to the algorithm dialog.
Handles adding basic data options to the UI.
:param label: Label that describes the option
:param dtype: Option data type (any of: file, int, float, bool, list)
:param default_value: Optionally select the default value
:param valid_values: Optionally provide the range or selection of valid
values
:param tooltip: Optional tooltip text to show on property
:param on_change: Function to be called when the property changes
:param form: Form layout to optionally add the new widgets to
:param filters_view: The Filter window view - passed to connect Type.STACK to the stack change events
:param run_on_press: Run this function on press, specialisation for button.
:param single_step_size: Optionally provide a step size for a SpinBox widget.
"""
# By default assume the left hand side widget will be a label
left_widget = QLabel(label)
right_widget = None
# sanitize default value
if isinstance(default_value, str) and default_value.lower() == "none":
default_value = None
def set_spin_box(box, cast_func):
"""
Helper function to set default options on a spin box.
"""
if valid_values:
box.setMinimum(valid_values[0])
box.setMaximum(valid_values[1])
else:
box.setMinimum(0)
box.setMaximum(10000)
if default_value:
box.setValue(cast_func(default_value))
if single_step_size is not None:
box.setSingleStep(single_step_size)
elif cast_func == float:
# Override the default step size for QDoubleSpinBox
box.setSingleStep(0.1)
if dtype in ['str', Type.STR, 'tuple', Type.TUPLE, 'NoneType', Type.NONETYPE, 'list', Type.LIST]:
# TODO for tuple with numbers add N combo boxes, N = number of tuple members
right_widget = QLineEdit()
right_widget.setToolTip(tooltip)
right_widget.setText(default_value)
if on_change is not None:
right_widget.editingFinished.connect(lambda: on_change())
elif dtype == 'int' or dtype == Type.INT:
right_widget = QSpinBox()
right_widget.setKeyboardTracking(False)
set_spin_box(right_widget, int)
if on_change is not None:
right_widget.valueChanged.connect(lambda: on_change_and_disable(right_widget, on_change))
elif dtype == 'float' or dtype == Type.FLOAT:
right_widget = QDoubleSpinBox()
set_spin_box(right_widget, float)
right_widget.setKeyboardTracking(False)
if on_change is not None:
right_widget.valueChanged.connect(lambda: on_change_and_disable(right_widget, on_change))
elif dtype == 'bool' or dtype == Type.BOOL:
right_widget = QCheckBox()
if isinstance(default_value, bool):
right_widget.setChecked(default_value)
elif isinstance(default_value, str):
right_widget.setChecked("True" == default_value)
elif default_value is None:
# have to pick something
right_widget.setChecked(False)
else:
raise ValueError(f"Cannot convert value {default_value} to a Boolean.")
if on_change is not None:
right_widget.stateChanged[int].connect(lambda: on_change())
elif dtype == "choice" or dtype == Type.CHOICE:
right_widget = QComboBox()
right_widget.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
if valid_values:
right_widget.addItems(valid_values)
if on_change is not None:
right_widget.currentIndexChanged[int].connect(lambda: on_change())
elif dtype == 'stack' or dtype == Type.STACK:
from mantidimaging.gui.widgets.dataset_selector import DatasetSelectorWidgetView
right_widget = DatasetSelectorWidgetView(filters_view, show_stacks=True)
if on_change is not None:
right_widget.currentIndexChanged[int].connect(lambda: on_change())
elif dtype == 'button' or dtype == Type.BUTTON:
left_widget = QPushButton(label)
if run_on_press is not None:
left_widget.clicked.connect(lambda: run_on_press())
elif dtype == 'label' or dtype == Type.LABEL:
# do nothing for label, just use the left widget
pass
else:
raise ValueError("Unknown data type")
if tooltip:
if left_widget:
left_widget.setToolTip(tooltip)
if right_widget:
right_widget.setToolTip(tooltip)
# right widget check avoids printing debug msg for labels only
if tooltip is None and right_widget is not None:
log = getLogger(__name__)
log.debug("Missing tooltip for %s", label)
if left_widget:
left_widget.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
if right_widget:
right_widget.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
# Add to form layout
if form is not None:
form.addRow(left_widget, right_widget)
return left_widget, right_widget
[docs]
def delete_all_widgets_from_layout(lo):
"""
Removes and deletes all child widgets form a layout.
:param lo: Layout to clean
"""
# For each item in the layout (removed as iterated)
while lo.count() > 0:
item = lo.takeAt(0)
# Recurse for child layouts
if isinstance(item, QLayout):
delete_all_widgets_from_layout(item)
# Orphan child widgets (seting a None parent removes them from the
# layout and marks them for deletion)
elif item.widget() is not None:
item.widget().setParent(None)