diff --git a/pyproject.toml b/pyproject.toml index 9e4eaf79..657eb4e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ extend-ignore-names = ['allKeys', 'isClean', 'mergeWith', 'mouseDoubleClickEvent', + 'paintEvent', 'resizeEvent', 'rowCount', 'setClean', diff --git a/rascal2/core/writer.py b/rascal2/core/writer.py index b3f71194..f7581906 100644 --- a/rascal2/core/writer.py +++ b/rascal2/core/writer.py @@ -49,9 +49,7 @@ def write_result_to_zipped_csvs(filename, results): if not isinstance(results, BayesResults): return - procedure_field = ( - "nestedSamplerOutput" if results.nestedSamplerOutput.nestSamples.shape != (1, 2) else "dreamOutput" - ) + procedure_field = "nestedSamplerOutput" if results.from_procedure() == "ns" else "dreamOutput" for inner_class in ["predictionIntervals", "confidenceIntervals", procedure_field]: subclass = getattr(results, inner_class) diff --git a/rascal2/static/style.css b/rascal2/static/style.css index acef0705..e2d145cd 100644 --- a/rascal2/static/style.css +++ b/rascal2/static/style.css @@ -455,3 +455,11 @@ MultiSelectList QToolButton { padding: 0; margin: 0; } + +/***************************** + LabeledSlider Styles +*****************************/ + +LabeledSlider{ + border: 1px solid #999; +} diff --git a/rascal2/ui/presenter.py b/rascal2/ui/presenter.py index b7c13210..66ffb5ed 100644 --- a/rascal2/ui/presenter.py +++ b/rascal2/ui/presenter.py @@ -170,14 +170,22 @@ def interrupt_terminal(self): """Sends an interrupt signal to the RAT runner.""" self.runner.interrupt() - def quick_run(self): - """Run rat calculation with calculate procedure. + def quick_run(self, project=None): + """Run rat calculation with calculate procedure on the given project. + The project in the MainWindowModel is used if no project is provided. + + Parameters + ---------- + project : Optional[ratapi.Project] + The project to use for run Returns ------- results : Union[ratapi.outputs.Results, ratapi.outputs.BayesResults] The calculation results. """ + if project is None: + project = self.model.project if ratapi.wrappers.MatlabWrapper.loader is None and any( [file.language == "matlab" for file in self.model.project.custom_files] ): @@ -185,7 +193,7 @@ def quick_run(self): result = get_matlab_engine(matlab_helper.ready_event, matlab_helper.engine_output) if isinstance(result, Exception): raise result - return rat.run(self.model.project, rat.Controls(display="off"))[1] + return rat.run(project, rat.Controls(display="off"))[1] def run(self): """Run rat using multiprocessing.""" @@ -242,7 +250,7 @@ def handle_event(self): case LogData(): self.view.logging.log(event.level, event.msg) - def edit_project(self, updated_project: dict, preview: bool = False) -> None: + def edit_project(self, updated_project: dict, preview: bool = True) -> None: """Edit the Project with a dictionary of attributes. Parameters diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 8076322a..a129c84d 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -8,7 +8,7 @@ from rascal2.dialogs.settings_dialog import SettingsDialog from rascal2.dialogs.startup_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog, StartupDialog from rascal2.settings import MDIGeometries, Settings, get_global_settings -from rascal2.widgets import ControlsWidget, PlotWidget, SlidersViewWidget, TerminalWidget +from rascal2.widgets import ControlsWidget, PlotWidget, TerminalWidget from rascal2.widgets.project import ProjectWidget from rascal2.widgets.startup import StartUpWidget @@ -22,13 +22,8 @@ class MainWindowView(QtWidgets.QMainWindow): def __init__(self): super().__init__() - # Public interface - self.disabled_elements = [] - self.show_sliders = False # no one displays sliders initially except got from configuration - # (not implemented yet) self.setWindowTitle(MAIN_WINDOW_TITLE) - window_icon = QtGui.QIcon(path_for("logo.png")) self.undo_stack = QtGui.QUndoStack(self) @@ -43,22 +38,12 @@ def __init__(self): self.plot_widget = PlotWidget(self) self.terminal_widget = TerminalWidget() self.controls_widget = ControlsWidget(self) - self.sliders_view_widget = SlidersViewWidget(self) self.project_widget = ProjectWidget(self) - ## protected interface and public properties construction - - # define menu controlling switch between table and slider views - self._sliders_menu_control_text = { - "ShowSliders": "&Show Sliders", # if state is show sliders, click will show them - "HideSliders": "&Hide Sliders", - } # if state is show table, click will show sliders + self.disabled_elements = [] self.create_actions() - - main_menu = self.menuBar() - self.add_submenus(main_menu) - + self.create_menus() self.create_toolbar() self.create_status_bar() @@ -172,30 +157,22 @@ def create_actions(self): self.settings_action.setEnabled(False) self.disabled_elements.append(self.settings_action) - open_help_action = QtGui.QAction("&Help", self) - open_help_action.setStatusTip("Open Documentation") - open_help_action.setIcon(QtGui.QIcon(path_for("help.png"))) - open_help_action.triggered.connect(self.open_docs) - self.open_help_action = open_help_action - - # done this way expecting the value "show_sliders" being stored - # in configuration in a future + "show_sliders" is public for this reason - if self.show_sliders: - # if show_sliders state is True, action will be hide - show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control_text["HideSliders"], self) - else: - # if display_sliders state is False, action will be show - show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control_text["ShowSliders"], self) - show_or_hide_slider_action.setStatusTip("Show or Hide Sliders") - show_or_hide_slider_action.triggered.connect(lambda: self.show_or_hide_sliders(None)) - self._show_or_hide_slider_action = show_or_hide_slider_action - self._show_or_hide_slider_action.setEnabled(False) - self.disabled_elements.append(self._show_or_hide_slider_action) - - open_about_action = QtGui.QAction("&About", self) - open_about_action.setStatusTip("Report RAT version&info") - open_about_action.triggered.connect(self.open_about_info) - self.open_about_action = open_about_action + self.open_help_action = QtGui.QAction("&Help", self) + self.open_help_action.setStatusTip("Open Documentation") + self.open_help_action.setIcon(QtGui.QIcon(path_for("help.png"))) + self.open_help_action.triggered.connect(self.open_docs) + + self.toggle_slider_action = QtGui.QAction("Show &Sliders", self) + self.toggle_slider_action.setProperty("show_text", "Show &Sliders") + self.toggle_slider_action.setProperty("hide_text", "Hide &Sliders") + self.toggle_slider_action.setStatusTip("Show or Hide Sliders") + self.toggle_slider_action.triggered.connect(self.toggle_sliders) + self.toggle_slider_action.setEnabled(False) + self.disabled_elements.append(self.toggle_slider_action) + + self.open_about_action = QtGui.QAction("&About", self) + self.open_about_action.setStatusTip("Report RAT version&info") + self.open_about_action.triggered.connect(self.open_about_info) self.exit_action = QtGui.QAction("E&xit", self) self.exit_action.setStatusTip(f"Quit {MAIN_WINDOW_TITLE}") @@ -231,13 +208,12 @@ def create_actions(self): self.setup_matlab_action.setStatusTip("Set the path of the MATLAB executable") self.setup_matlab_action.triggered.connect(lambda: self.show_settings_dialog(tab_name="Matlab")) - def add_submenus(self, main_menu: QtWidgets.QMenuBar): + def create_menus(self): """Add sub menus to the main menu bar""" - + main_menu = self.menuBar() main_menu.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.PreventContextMenu) file_menu = main_menu.addMenu("&File") - file_menu.setObjectName("&File") file_menu.addAction(self.new_project_action) file_menu.addSeparator() file_menu.addAction(self.open_project_action) @@ -253,13 +229,11 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): file_menu.addAction(self.exit_action) edit_menu = main_menu.addMenu("&Edit") - edit_menu.setObjectName("&Edit") edit_menu.addAction(self.undo_action) edit_menu.addAction(self.redo_action) edit_menu.addAction(self.undo_view_action) windows_menu = main_menu.addMenu("&Windows") - windows_menu.setObjectName("&Windows") windows_menu.addAction(self.tile_windows_action) windows_menu.addAction(self.reset_windows_action) windows_menu.addAction(self.save_default_windows_action) @@ -267,42 +241,25 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): self.disabled_elements.append(windows_menu) tools_menu = main_menu.addMenu("&Tools") - tools_menu.setObjectName("&Tools") - tools_menu.addAction(self._show_or_hide_slider_action) + tools_menu.addAction(self.toggle_slider_action) tools_menu.addSeparator() tools_menu.addAction(self.clear_terminal_action) tools_menu.addSeparator() tools_menu.addAction(self.setup_matlab_action) help_menu = main_menu.addMenu("&Help") - help_menu.setObjectName("&Help") help_menu.addAction(self.open_about_action) help_menu.addAction(self.open_help_action) - def show_or_hide_sliders(self, do_show_sliders=None): - """Depending on current state, show or hide sliders for - table properties within Project class view. - - Parameters: - ----------- - - do_show_sliders: bool,default None - if provided, sets self.show_sliders logical variable into the requested state - (True/False), forcing sliders widget to appear/disappear. if None, applies not to current state. - """ - if do_show_sliders is None: - self.show_sliders = not self.show_sliders - else: - self.show_sliders = do_show_sliders - - if self.show_sliders: - self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["HideSliders"]) - self.sliders_view_widget.show() - self.project_widget.setWindowTitle("Sliders View") - self.project_widget.stacked_widget.setCurrentIndex(2) + def toggle_sliders(self): + """Toggles sliders for the fitted parameters in project class view.""" + show_text = self.toggle_slider_action.property("show_text") + if self.toggle_slider_action.text() == show_text: + hide_text = self.toggle_slider_action.property("hide_text") + self.toggle_slider_action.setText(hide_text) + self.project_widget.show_slider_view() else: - self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) - self.sliders_view_widget.hide() + self.toggle_slider_action.setText(show_text) self.project_widget.show_project_view() def open_about_info(self): diff --git a/rascal2/widgets/__init__.py b/rascal2/widgets/__init__.py index bd884688..92ec968e 100644 --- a/rascal2/widgets/__init__.py +++ b/rascal2/widgets/__init__.py @@ -1,7 +1,7 @@ from rascal2.widgets.controls import ControlsWidget from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, MultiSelectComboBox, MultiSelectList, get_validated_input from rascal2.widgets.plot import PlotWidget -from rascal2.widgets.sliders_view import SlidersViewWidget +from rascal2.widgets.project.slider_view import SliderViewWidget from rascal2.widgets.terminal import TerminalWidget __all__ = [ @@ -12,5 +12,5 @@ "MultiSelectList", "PlotWidget", "TerminalWidget", - "SlidersViewWidget", + "SliderViewWidget", ] diff --git a/rascal2/widgets/plot.py b/rascal2/widgets/plot.py index 25f7acf1..807ec736 100644 --- a/rascal2/widgets/plot.py +++ b/rascal2/widgets/plot.py @@ -244,14 +244,12 @@ def update_figure_size(self): def show_result_summary(self, results): """Show log z and log z error in summary label""" - if isinstance(results, ratapi.outputs.BayesResults): - samples = results.nestedSamplerOutput.nestSamples - if samples.shape != (1, 2): - self.result_summary.setText( - f"log (Z) = {results.nestedSamplerOutput.logZ:.5f}\n" - f"log (Z) error = {results.nestedSamplerOutput.logZErr:.5f}" - ) - self.result_summary.setVisible(True) + if isinstance(results, ratapi.outputs.BayesResults) and results.from_procedure() == "ns": + self.result_summary.setText( + f"log (Z) = {results.nestedSamplerOutput.logZ:.5f}\n" + f"log (Z) error = {results.nestedSamplerOutput.logZErr:.5f}" + ) + self.result_summary.setVisible(True) def make_interaction_layout(self): """Make layout with pan, zoom, and reset button. diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index a9048cbd..449bd405 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -11,6 +11,7 @@ from rascal2.config import path_for from rascal2.widgets.project.lists import ContrastWidget, DataWidget +from rascal2.widgets.project.slider_view import SliderViewWidget from rascal2.widgets.project.tables import ( BackgroundsFieldWidget, CustomFileWidget, @@ -41,6 +42,7 @@ def __init__(self, parent): self.parent_model = self.parent.presenter.model self.parent_model.project_updated.connect(self.update_project_view) + self.parent_model.project_updated.connect(self.update_slider_view) self.parent_model.controls_updated.connect(self.handle_controls_update) self.tabs = { @@ -72,7 +74,6 @@ def __init__(self, parent): self.stacked_widget = QtWidgets.QStackedWidget() self.stacked_widget.addWidget(project_view) self.stacked_widget.addWidget(project_edit) - self.stacked_widget.addWidget(self.parent.sliders_view_widget) layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) @@ -85,8 +86,8 @@ def create_project_view(self) -> QtWidgets.QWidget: main_layout = QtWidgets.QVBoxLayout() main_layout.setSpacing(20) - show_sliders_button = QtWidgets.QPushButton("Show sliders", self, objectName="ShowSliders") - show_sliders_button.clicked.connect(lambda: self.parent.show_or_hide_sliders(True)) + show_sliders_button = QtWidgets.QPushButton("Show sliders", self) + show_sliders_button.clicked.connect(self.parent.toggle_sliders) self.edit_project_button = QtWidgets.QPushButton("Edit Project", self, icon=QtGui.QIcon(path_for("edit.png"))) self.edit_project_button.clicked.connect(self.show_edit_view) @@ -246,6 +247,26 @@ def create_edit_view(self) -> QtWidgets.QWidget: return edit_project_widget + def show_slider_view(self): + """Create slider view and make it visible.""" + if self.stacked_widget.count() == 3: + # 3 widgets means slider view already exist + # (with project view and edit view) so delete before replacing with new one + old_slider_widget = self.stacked_widget.widget(2) + self.stacked_widget.removeWidget(old_slider_widget) + old_slider_widget.deleteLater() + slider_view = SliderViewWidget(create_draft_project(self.parent_model.project), self.parent) + self.stacked_widget.addWidget(slider_view) + self.stacked_widget.setCurrentIndex(2) + + def update_slider_view(self): + """Update the slider view if the project changes when it is opened.""" + if self.stacked_widget.currentIndex() == 2: + # slider view is the 3rd widget in the layout + widget = self.stacked_widget.widget(2) + widget.draft_project = create_draft_project(self.parent_model.project) + widget.initialize() + def update_project_view(self, update_tab_index=None) -> None: """Updates the project view.""" diff --git a/rascal2/widgets/project/slider_view.py b/rascal2/widgets/project/slider_view.py new file mode 100644 index 00000000..80fc0cbe --- /dev/null +++ b/rascal2/widgets/project/slider_view.py @@ -0,0 +1,264 @@ +"""Widget for the Sliders View window.""" + +import ratapi +from PyQt6 import QtCore, QtGui, QtWidgets + + +class SliderViewWidget(QtWidgets.QWidget): + """The slider view widget which allows user change fitted parameters with sliders.""" + + def __init__(self, draft_project, parent): + """Initialize widget. + + Parameters + ---------- + draft_project: ratapi.Project + A copy of the project that will be modified by slider + parent: MainWindowView + An instance of the MainWindowView + """ + super().__init__() + self._parent = parent + self.draft_project = draft_project + + self._sliders = {} + self.parameters = {} + + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + self.accept_button = QtWidgets.QPushButton("Accept", self) + self.accept_button.clicked.connect(self._apply_changes_from_sliders) + + cancel_button = QtWidgets.QPushButton("Cancel", self) + cancel_button.clicked.connect(self._cancel_changes_from_sliders) + + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch(1) + button_layout.addWidget(self.accept_button) + button_layout.addWidget(cancel_button) + main_layout.addLayout(button_layout) + + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + main_layout.addWidget(scroll) + content = QtWidgets.QWidget() + scroll.setWidget(content) + self.slider_content_layout = QtWidgets.QVBoxLayout() + content.setLayout(self.slider_content_layout) + + self.initialize() + + def initialize(self): + """Populate parameters and slider from draft project.""" + self._init_parameters_for_sliders() + self._add_sliders_widgets() + + def _init_parameters_for_sliders(self): + """Extract fitted parameters from the draft project.""" + self.parameters.clear() + + for class_list in self.draft_project.values(): + if hasattr(class_list, "_class_handle") and class_list._class_handle is ratapi.models.Parameter: + for parameter in class_list: + if parameter.fit: + self.parameters[parameter.name] = parameter + + def _add_sliders_widgets(self): + """Add sliders to the layout.""" + # We are adding new sliders, so delete all previous ones. + for slider in self._sliders.values(): + self.slider_content_layout.removeWidget(slider) + slider.deleteLater() + for _ in range(self.slider_content_layout.count()): + w = self.slider_content_layout.takeAt(0).widget() + if w is not None: + w.deleteLater() + self._sliders.clear() + self.accept_button.setDisabled(not self.parameters) + + if not self.parameters: + no_label = QtWidgets.QLabel( + "There are no fitted parameters.\n " + "Select parameters to fit in the project view to populate the slider view.", + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + ) + self.slider_content_layout.addWidget(no_label) + else: + self.slider_content_layout.setSpacing(0) + for name, params in self.parameters.items(): + slider = LabeledSlider(params, self) + + self._sliders[name] = slider + self.slider_content_layout.addWidget(slider) + self.slider_content_layout.addStretch(1) + + def update_result_and_plots(self): + project = ratapi.Project() + vars(project).update(self.draft_project) + results = self._parent.presenter.quick_run(project) + self._parent.plot_widget.reflectivity_plot.plot(project, results) + + def _cancel_changes_from_sliders(self): + """Revert changes to parameter values and close slider view.""" + self._parent.plot_widget.update_plots() + self._parent.toggle_sliders() + + def _apply_changes_from_sliders(self): + """ + Apply changes obtained from sliders to the project and close slider view. + """ + self._parent.presenter.edit_project(self.draft_project) + self._parent.toggle_sliders() + + +class LabeledSlider(QtWidgets.QFrame): + def __init__(self, param, parent): + """Create a LabeledSlider for a given RAT parameter + + Parameters + ---------- + param : ratapi.models.Parameter + The parameter which the slider updates. + parent : SliderViewWidget + The container for the slider widget. + """ + + super().__init__() + self.parent = parent + self._value_label_format: str = "{:.3g}" + + self.param = param + + self._slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + self._slider.setMinimum(0) + self._slider.setMaximum(100) + self._slider.setTickInterval(10) + self._slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBothSides) + self._slider.setValue(self._param_value_to_slider_value(self.param.value)) + + # name of given slider can not change. It will be different slider with different name + name_label = QtWidgets.QLabel(param.name, alignment=QtCore.Qt.AlignmentFlag.AlignLeft) + self._value_label = QtWidgets.QLabel( + self._value_label_format.format(self.param.value), alignment=QtCore.Qt.AlignmentFlag.AlignRight + ) + lab_layout = QtWidgets.QHBoxLayout() + lab_layout.addWidget(name_label) + lab_layout.addWidget(self._value_label) + + scale_layout = QtWidgets.QHBoxLayout() + num_of_ticks = self._slider.maximum() // self._slider.tickInterval() + tick_step = (self.param.max - self.param.min) / num_of_ticks + self.labels = [self.param.min + i * tick_step for i in range(num_of_ticks + 1)] + + self.margins = [10, 10, 10, 15] # left, top, right, bottom + layout = QtWidgets.QVBoxLayout(self) + layout.addLayout(lab_layout) + layout.addWidget(self._slider) + layout.addLayout(scale_layout) + layout.setContentsMargins(*self.margins) + + self._slider.valueChanged.connect(self._update_value) + self.setFrameShape(QtWidgets.QFrame.Shape.Box) + self.setSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + + def paintEvent(self, event): + # Draws tick labels + # Adapted from https://gist.github.com/wiccy46/b7d8a1d57626a4ea40b19c5dbc5029ff""" + super().paintEvent(event) + style = self._slider.style() + painter = QtGui.QPainter(self) + st_slider = QtWidgets.QStyleOptionSlider() + st_slider.initFrom(self._slider) + st_slider.orientation = self._slider.orientation() + + length = style.pixelMetric(QtWidgets.QStyle.PixelMetric.PM_SliderLength, st_slider, self._slider) + available = style.pixelMetric(QtWidgets.QStyle.PixelMetric.PM_SliderSpaceAvailable, st_slider, self._slider) + for i, label_value in enumerate(self.labels): + value = i * (len(self.labels) - 1) + value_label = self._value_label_format.format(label_value) + + # get the size of the label + rect = painter.drawText(QtCore.QRect(), QtCore.Qt.TextFlag.TextDontPrint, value_label) + + if self._slider.orientation() == QtCore.Qt.Orientation.Horizontal: + # I assume the offset is half the length of slider, therefore + # + length//2 + x_loc = ( + QtWidgets.QStyle.sliderPositionFromValue( + self._slider.minimum(), self._slider.maximum(), value, available + ) + + length // 2 + ) + + # left bound of the text = center - half of text width + L_margin + left = x_loc - rect.width() // 2 + self.margins[0] + bottom = self.rect().bottom() - 5 + + # enlarge margins if clipping + if value == self._slider.minimum(): + if left <= 0: + self.margins[0] = rect.width() // 2 - x_loc + if self.margins[3] <= rect.height(): + self.margins[3] = rect.height() + + self.layout().setContentsMargins(*self.margins) + + if value == self._slider.maximum() and rect.width() // 2 >= self.margins[2]: + self.margins[2] = rect.width() // 2 + self.layout().setContentsMargins(*self.margins) + + pos = QtCore.QPoint(left, bottom) + painter.drawText(pos, value_label) + + def _param_value_to_slider_value(self, param_value: float) -> int: + """Convert parameter value into slider value. + + Parameters: + ----------- + param_value : float + parameter value + + Returns: + -------- + value : int + slider value that corresponds to the parameter value + """ + param_value_range = self.param.max - self.param.min + if abs(param_value_range) < 10e-7: + return self._slider.maximum() + return int(round(self._slider.maximum() * (param_value - self.param.min) / param_value_range, 0)) + + def _slider_value_to_param_value(self, value: int) -> float: + """Convert slider value into parameter value. + + Parameters + ---------- + value : int + slider value + + Returns + ------- + param_value : float + parameter value that corresponds to slider value + """ + + value_step = (self.param.max - self.param.min) / self._slider.maximum() + param_value = self.param.min + value * value_step + if param_value > self.param.max: # This should not happen but do occur due to round-off errors + param_value = self.param.max + return param_value + + def _update_value(self, value: int): + """Update parameter value and plot when slider value is changed. + + Parameters + ---------- + value : int + slider value + + """ + param_value = self._slider_value_to_param_value(value) + self._value_label.setText(self._value_label_format.format(param_value)) + self.param.value = param_value + self.parent.update_result_and_plots() diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 2017d624..e2d72834 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -75,26 +75,17 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): elif role == QtCore.Qt.ItemDataRole.CheckStateRole and self.index_header(index) == "fit": return QtCore.Qt.CheckState.Checked if data else QtCore.Qt.CheckState.Unchecked - def setData( - self, index: QtCore.QModelIndex, value, role=QtCore.Qt.ItemDataRole.EditRole, recalculate_proj=True - ) -> bool: - """Implement abstract setData method of QAbstractTableModel. + def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool: + """Set the data of a given index in the table model. Parameters ---------- index: QtCore.QModelIndex - QModelIndex representing the row and column indices of edited cell wrt. the edited table - value: - new value of appropriate cell of the table. + The model index indicates which cells to change + value: Any + The new data value role: QtCore.Qt.ItemDataRole - not sure what it is but apparently controls table behaviour amd needs to be Edit. - it nof Edit, method does nothing. - recalculate_proj: bool,default True - Additional control for RAT project recalculation. Set it to False when modifying - a bunch of properties in a loop changing it to True for the last value to recalculate - project and update all table's dependent widgets. - IMPORTANT: ensure last value differs from the existing one for this property as project - will be not recalculated otherwise. + Indicates the role of the Data. """ if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole: row = index.row() @@ -113,7 +104,7 @@ def setData( return False if not self.edit_mode: # recalculate plots if value was changed - recalculate = self.index_header(index) == "value" and recalculate_proj + recalculate = self.index_header(index) == "value" self.parent.update_project(recalculate) self.dataChanged.emit(index, index) return True @@ -269,18 +260,10 @@ def update_model(self, classlist: ratapi.classlist.ClassList): def set_item_delegates(self): """Set item delegates and open persistent editors for the table.""" for i, header in enumerate(self.model.headers): - delegate = delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table) - self.table.setItemDelegateForColumn(i + self.model.col_offset, delegate) - - def get_item_delegates(self, fields_list: list): - """Return list of delegates attached to the fields - with the names provided as input - """ - dlgts = [] - for i, header in enumerate(self.model.headers): - if header in fields_list: - dlgts.append(self.table.itemDelegateForColumn(i + self.model.col_offset)) - return dlgts + self.table.setItemDelegateForColumn( + i + self.model.col_offset, + delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table), + ) def append_item(self): """Append an item to the model if the model exists.""" diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py deleted file mode 100644 index edf0ed09..00000000 --- a/rascal2/widgets/sliders_view.py +++ /dev/null @@ -1,600 +0,0 @@ -"""Widget for the Sliders View window.""" - -import ratapi.models -from PyQt6 import QtCore, QtWidgets - -from rascal2.widgets.project.tables import ParametersModel - - -class SlidersViewWidget(QtWidgets.QWidget): - """ - The sliders view Widget represents properties user intends to fit. - The sliders allow user to change the properties and immediately see how the change affects contrast. - """ - - def __init__(self, parent): - """ - Initialize widget. - - Parameters - ---------- - parent: MainWindowView - An instance of the MainWindowView - """ - super().__init__() - # within the main window for subsequent calls to show sliders. Not yet restored from hdd properly - # inherits project geometry on the first view. - self._parent = parent # reference to main view widget which holds sliders view - - self._values_to_revert = {} # dictionary of values of original properties with fit parameter "true" - # to be restored back into original project if cancel button is pressed. - self._prop_to_change = {} # dictionary of references to SliderChangeHolder classes containing properties - # with fit parameter "true" to build sliders for and allow changes when slider is moved. - # Their values are reflected in project and affect plots. - - self._sliders = {} # dictionary of the sliders used to display fittable values. - - self.__accept_button = None # Placeholder for accept button indicating particular. Presence indicates - # initial stage of widget construction was completed - self.__sliders_widgets_layout = None # Placeholder for the area, containing sliders widgets. - # presence indicates advanced stage of slider widget construction was completed and sliders widgets - # cam be propagated. - - # create initial slider view layout and everything else which depends on it - self.init() - - def show(self): - """Overload parent show method sets up or updates sliders - list depending on previous state of the widget. - """ - - # avoid running init view more than once if sliders are visible. - if self.isVisible(): - return - - self.init() - super().show() - - def init(self) -> None: - """Initializes general contents (buttons) of the sliders widget if they have not been initialized. - - If project is defined extracts properties, used to build sliders and generate list of sliders - widgets to control the properties. - """ - if self.__accept_button is None: - self._create_slider_view_layout() - - if self._parent.presenter.model.project is None: - return # Project may be not initialized at all so project gui is not initialized - - update_sliders = self._init_properties_for_sliders() - if update_sliders: - self._update_sliders_widgets() - else: - self._add_sliders_widgets() - - def _init_properties_for_sliders(self) -> bool: - """Loop through project's widget view tabs and models associated with them and extract - properties used by sliders widgets. - - Select all ParametersModel-s and copy all their properties which have attribute - "Fit" == True into dictionary used to build sliders for them. Also set back-up - dictionary to reset properties values back to their initial values if "Cancel" - button is pressed. - - Requests: SlidersViewWidget with initialized Project. - - Returns - -------- - bool - true if all properties in the project have already had sliders, generated for them - earlier so we may update existing widgets instead of generating new ones. - - Sets up dictionary of slider parameters used to define sliders and sets up connections - necessary to interact with table view, namely: - - 1) slider to table and update graphics -> in the dictionary of slider parameters - 2) change from Table view delegates -> routine which modifies sliders view. - """ - - proj = self._parent.project_widget - if proj is None: - return False - - n_updated_properties = 0 - trial_properties = {} - - for widget in proj.view_tabs.values(): - for table_view in widget.tables.values(): - if not hasattr(table_view, "model"): - continue # usually in tests when table view model is not properly established for all tabs - data_model = table_view.model - if not isinstance(data_model, ParametersModel): - continue # data may be empty - - for row, model_param in enumerate(data_model.classlist): - if model_param.fit: - # Store information about necessary property and the model, which contains the property. - # The model is the source of methods which modify dependent table and force project - # recalculation. - trial_properties[model_param.name] = SliderChangeHolder( - row_number=row, model=data_model, param=model_param - ) - - if model_param.name in self._prop_to_change: - n_updated_properties += 1 - - # if all properties of trial dictionary are in existing dictionary and the number of properties are the same - # no new/deleted sliders have appeared. - # We will update widgets parameters instead of deleting old and creating the new one. - update_properties = ( - n_updated_properties == len(trial_properties) - and len(self._prop_to_change) == n_updated_properties - and n_updated_properties != 0 - ) - - # store information about sliders properties - self._prop_to_change = trial_properties - # remember current values of properties controlled by sliders in case you want to revert them back later - self._values_to_revert = {name: prop.value for name, prop in trial_properties.items()} - - return update_properties - - def _create_slider_view_layout(self) -> None: - """Create sliders layout with all necessary controls and connections - but without sliders themselves. - """ - - main_layout = QtWidgets.QVBoxLayout() - - accept_button = QtWidgets.QPushButton("Accept", self, objectName="AcceptButton") - accept_button.clicked.connect(self._apply_changes_from_sliders) - self.__accept_button = accept_button - - cancel_button = QtWidgets.QPushButton("Cancel", self, objectName="CancelButton") - cancel_button.clicked.connect(self._cancel_changes_from_sliders) - - button_layout = QtWidgets.QHBoxLayout() - button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) - button_layout.addWidget(accept_button) - button_layout.addWidget(cancel_button) - - main_layout.addLayout(button_layout) - - self.setLayout(main_layout) - - def _add_sliders_widgets(self) -> None: - """Given sliders view layout and list of properties which can be controlled by sliders - add appropriate sliders to sliders view Widget - """ - - if self.__sliders_widgets_layout is None: - main_layout = self.layout() - scroll = QtWidgets.QScrollArea() - scroll.setWidgetResizable(True) # important: resize content to fit area - main_layout.addWidget(scroll) - content = QtWidgets.QWidget() - scroll.setWidget(content) - # --- Add content layout - content_layout = QtWidgets.QVBoxLayout(content) - self.__sliders_widgets_layout = content_layout - else: - content_layout = self.__sliders_widgets_layout - - # We are adding new sliders, so delete all previous ones. Update is done in another routine. - for slider in self._sliders.values(): - slider.deleteLater() - self._sliders = {} - - if len(self._prop_to_change) == 0: - no_label = EmptySlider() - content_layout.addWidget(no_label) - self._sliders[no_label.slider_name] = no_label - else: - content_layout.setSpacing(0) - for name, prop in self._prop_to_change.items(): - slider = LabeledSlider(prop) - slider.setMaximumHeight(100) - - self._sliders[name] = slider - content_layout.addWidget(slider, alignment=QtCore.Qt.AlignmentFlag.AlignTop) - - def _update_sliders_widgets(self) -> None: - """ - Updates the sliders given the project properties to fit are the same but their values may be modified - """ - for name, prop in self._prop_to_change.items(): - self._sliders[name].update_slider_parameters(prop) - - def _cancel_changes_from_sliders(self): - """Revert changes to values of properties, controlled and modified by sliders - to their initial values and hide sliders view. - """ - - changed_properties = self._identify_changed_properties() - if len(changed_properties) > 0: - last_changed_prop_num = len(changed_properties) - 1 - for prop_num, (name, val) in enumerate(self._values_to_revert.items()): - self._prop_to_change[name].update_value_representation( - val, - recalculate_project=(prop_num == last_changed_prop_num), # it is important to update project for - # last changed property only not to recalculate project multiple times. - ) - # else: all properties value remain the same so no point in reverting to them - self._parent.show_or_hide_sliders(do_show_sliders=False) - - def _identify_changed_properties(self) -> dict: - """Identify properties changed by sliders from initial sliders state. - - Returns - ------- - :dict - dictionary of the original values for properties changed by sliders. - """ - - changed_properties = {} - for prop_name, value in self._values_to_revert.items(): - if value != self._prop_to_change[prop_name].value: - changed_properties[prop_name] = value - return changed_properties - - def _apply_changes_from_sliders(self) -> None: - """ - Apply changes obtained from sliders to the project and make them permanent - """ - # Changes have already been applied so just hide sliders widget - self._parent.show_or_hide_sliders(do_show_sliders=False) - return - - -class SliderChangeHolder: - """Helper class containing information necessary for update ratapi parameter and its representation - in project table view when slider position is changed. - """ - - def __init__(self, row_number: int, model: ParametersModel, param: ratapi.models.Parameter) -> None: - """Class Initialization function: - - Parameters - ---------- - row_number: int - the number of the row in the project table, which should be changed - model: rascal2.widgets.project.tables.ParametersModel - parameters model (in QT sense) participating in ParametersTableView - and containing the parameter (below) to modify here. - param: ratapi.models.Parameter - the parameter which value field may be changed by slider widget - """ - self.param = param - self._vis_model = model - self._row_number = row_number - - @property - def name(self): - return self.param.name - - @property - def value(self) -> float: - return self.param.value - - @value.setter - def value(self, value: float) -> None: - self.param.value = value - - def update_value_representation(self, val: float, recalculate_project=True) -> None: - """given new value, updates project table and property representations in the tables - - No checks are necessary as value comes from slider or undo cache - - Parameters - ---------- - val: float - new value to set up slider position according to the slider's numerical scale - (recalculated into actual integer position) - recalculate_project: bool - if True, run ratapi calculations and update representation of results in all dependent widgets. - if False, just update tables and properties - """ - # value for ratapi parameter is defined in column 4 and this number is hardwired here - # should be a better way of doing this. - index = self._vis_model.index(self._row_number, 4) - self._vis_model.setData(index, val, QtCore.Qt.ItemDataRole.EditRole, recalculate_project) - - -class LabeledSlider(QtWidgets.QFrame): - """Class describes slider widget which allows modifying rascal property value and its representation - in project table view. - - It also connects with table view and accepts changes in min/max/value - obtained from property. - """ - - # Class attributes of slider widget which usually remain the same for all classes. - # Affect all sliders behaviour so are global. - _num_slider_ticks: int = 10 - _slider_max_idx: int = 100 # defines accuracy of slider motion - _ticks_step: int = 10 # Number of sliders ticks - _value_label_format: str = ( - "{:.4g}" # format to display slider value. Should be not too accurate as slider accuracy is 1/100 - ) - _tick_label_format: str = "{:.2g}" # format to display numbers under the sliders ticks - - def __init__(self, param: SliderChangeHolder): - """Construct LabeledSlider for a particular property - - Parameters - ---------- - param: SliceChangeHolder - instance of the SliderChangeHolder class, containing reference to the property to be modified by - slider and the reference to visual model, which controls the position and the place of this - property in the correspondent project table. - """ - - super().__init__() - # Defaults for property min/max. Will be overwritten from actual input property - self._value_min = 0 # minimal value property may have - self._value_max = 100 # maximal value property may have - self._value = 50 # cache for property value - self._value_range = 100 # difference between maximal and minimal values of the property - self._value_step = 1 # the change in property value per single step slider move - - self._prop = param # hold the property controlled by slider - if param is None: - return - - self._labels = [] # list of slider labels describing sliders axis - self.__block_slider_value_changed_signal = False - - self.slider_name = param.name # name the slider as the property it refers to. Sets up once here. - self.update_slider_parameters(param, in_constructor=True) # Retrieve slider's parameters from input property - - # Build all sliders widget and arrange them as expected - self._slider = self._build_slider(param.value) - - # name of given slider can not change. It will be different slider with different name - name_label = QtWidgets.QLabel(self.slider_name, alignment=QtCore.Qt.AlignmentFlag.AlignLeft) - self._value_label = QtWidgets.QLabel( - self._value_label_format.format(self._value), alignment=QtCore.Qt.AlignmentFlag.AlignRight - ) - lab_layout = QtWidgets.QHBoxLayout() - lab_layout.addWidget(name_label) - lab_layout.addWidget(self._value_label) - - # layout for numeric scale below - scale_layout = QtWidgets.QHBoxLayout() - - tick_step = self._value_range / self._num_slider_ticks - middle_val = self._value_min + 0.5 * self._value_range - middle_min = middle_val - 0.5 * tick_step - middle_max = middle_val + 0.5 * tick_step - for idx in range(0, self._num_slider_ticks + 1): - tick_value = ( - self._value_min + idx * tick_step - ) # it is not _slider_idx_to_value as tick step there is different - label = QtWidgets.QLabel(self._tick_label_format.format(tick_value)) - if tick_value < middle_min: - label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) - elif tick_value > middle_max: - label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) - else: - label.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) - - scale_layout.addWidget(label) - self._labels.append(label) - - layout = QtWidgets.QVBoxLayout(self) - layout.addLayout(lab_layout) - layout.addWidget(self._slider) - layout.addLayout(scale_layout) - - # signal to update label dynamically and change all dependent properties - self._slider.valueChanged.connect(self._update_value) - - self.setObjectName(self.slider_name) - self.setFrameShape(QtWidgets.QFrame.Shape.Box) - self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.setMaximumHeight(self._slider.height()) - - def set_slider_gui_position(self, value: float) -> None: - """Set specified slider GUI position programmatically. - - As value assumed to be already correct, block signal - for change, associated with slider position change in GUI - - Parameters - ---------- - value: float - new float value of the slider - """ - self._value = value - self._value_label.setText(self._value_label_format.format(value)) - - idx = self._value_to_slider_pos(value) - self.__block_slider_value_changed_signal = True - self._slider.setValue(idx) - self.__block_slider_value_changed_signal = False - - def update_slider_parameters(self, param: SliderChangeHolder, in_constructor=False): - """Modifies slider values which may change for this slider from his parent property - - Parameters - ---------- - param: SliderChangeHolder - instance of the SliderChangeHolder class, containing updated values for the slider - in_constructor: bool,default False - logical value, indicating that the method is invoked in constructor. If true, - some additional initialization will be performed. - """ - self._prop = param - # Changing RASCAL property this slider modifies is currently prohibited, - # as property connected through table model and project parameters: - if self._prop.name != self.slider_name: - # This should not happen but if it is, ensure failure. Something wrong with logic. - raise RuntimeError("Existing slider may be responsible for only one property") - self.update_slider_display_from_property(in_constructor) - - def update_slider_display_from_property(self, in_constructor: bool) -> None: - """Change internal sliders parameters and their representation in GUI - if property, underlying sliders parameters have changed. - - Bound to event received from delegate when table values are changed. - - Parameters - ---------- - in_constructor: bool,default False - logical value, indicating that the method is invoked in constructor. If True, - avoid change in graphics as these changes + graphics initialization - will be performed separately. - """ - # note the order of methods in comparison. Should be as here, as may break - # property updates in constructor otherwise. - if not (self._updated_from_rascal_property() or in_constructor): - return - - self._value_range = self._value_max - self._value_min - # the change in property value per single step slider move - self._value_step = self._value_range / self._slider_max_idx - - if in_constructor: - return - # otherwise, update slider's labels - self.set_slider_gui_position(self._value) - tick_step = self._value_range / self._num_slider_ticks - for idx in range(0, self._num_slider_ticks + 1): - tick_value = self._value_min + idx * tick_step - self._labels[idx].setText(self._tick_label_format.format(tick_value)) - - def _updated_from_rascal_property(self) -> bool: - """Check if rascal property values related to slider widget have changed - and update them accordingly - - Returns: - ------- - True if change detected and False otherwise - """ - updated = False - if self._value_min != self._prop.param.min: - self._value_min = self._prop.param.min - updated = True - if self._value_max != self._prop.param.max: - self._value_max = self._prop.param.max - updated = True - if self._value != self._prop.param.value: - self._value = self._prop.param.value - updated = True - return updated - - def _value_to_slider_pos(self, value: float) -> int: - """Convert double (property) value into slider position - - Parameters: - ----------- - value : float - double value within slider's min-max range to identify integer - position corresponding to this value - - Returns: - -------- - index : int - integer position within 0-self._slider_max_idx range corresponding to input value - """ - return int(round(self._slider_max_idx * (value - self._value_min) / self._value_range, 0)) - - def _slider_pos_to_value(self, index: int) -> float: - """Convert slider GUI position (index) into double property value - - Parameters - ---------- - index : int - integer position within 0-self._slider_max_idx range to process - - Returns - ------- - value : float - double value within slider's min-max range corresponding to input index - """ - - value = self._value_min + index * self._value_step - if value > self._value_max: # This should not happen but do occur due to round-off errors - value = self._value_max - return value - - def _build_slider(self, initial_value: float) -> QtWidgets.QSlider: - """Construct slider widget with integer scales and ticks in integer positions - - Part of slider constructor - - Parameters - ---------- - value : float - double value within slider's min-max range to identify integer - position corresponding to this value. - - Returns - ------- - QtWidgets.QSlider instance - with settings, corresponding to input parameters. - """ - - slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) - slider.setMinimum(0) - slider.setMaximum(self._slider_max_idx) - slider.setTickInterval(self._ticks_step) - slider.setSingleStep(self._slider_max_idx) - slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBothSides) - slider.setValue(self._value_to_slider_pos(initial_value)) - - return slider - - def _update_value(self, idx: int) -> None: - """Method which converts slider position into double property value - and informs all dependent clients about this. - - Bound in constructor to GUI slider position changed event - - Parameters - ---------- - idx : int - integer position of slider deal in GUI - - """ - if self.__block_slider_value_changed_signal: - return - val = self._slider_pos_to_value(idx) - self._value = val - self._value_label.setText(self._value_label_format.format(val)) - - self._prop.update_value_representation(val) - # This should not be necessary as already done through setter above - self._prop.param.value = val # but fast and nice for tests - - -class EmptySlider(LabeledSlider): - def __init__(self): - """Construct empty slider which have interface of LabeledSlider but no properties - associated with it - - Parameters - ---------- - All input parameters are ignored - """ - super().__init__(None) - - name_label = QtWidgets.QLabel( - "There are no fitted parameters.\n" - " Select parameters to fit in the project view to populate the sliders view.", - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, - ) - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(name_label) - self.slider_name = "Empty Slider" - self.setObjectName(self.slider_name) - - def set_slider_gui_position(self, value: float) -> None: - return - - def update_slider_parameters(self, param: SliderChangeHolder, in_constructor=False): - return - - def update_slider_display_from_property(self, in_constructor: bool) -> None: - return diff --git a/requirements.txt b/requirements.txt index 2ec8523b..99744d01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyInstaller==6.9.0 PyQt6==6.7.1 PyQt6-Qt6==6.7.3 -ratapi==0.0.0.dev9 +ratapi==0.0.0.dev10 pydantic==2.8.2 PyQt6-QScintilla==2.14.1 nexusformat==1.0.7 diff --git a/tests/ui/test_presenter.py b/tests/ui/test_presenter.py index 9af92fcc..80aa9e37 100644 --- a/tests/ui/test_presenter.py +++ b/tests/ui/test_presenter.py @@ -44,7 +44,6 @@ def __init__(self): self.logging = MagicMock() self.settings = MagicMock() self.get_project_folder = lambda: "new path/" - self.sliders_view_widget = MagicMock() @pytest.fixture diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 5a8e4d3d..70a5d128 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -179,7 +179,7 @@ def test_menu_element_present(test_view, submenu_name): ), ("&Edit", ["&Undo", "&Redo", "Undo &History"]), ("&Windows", ["Tile Windows", "Reset to Default", "Save Current Window Positions"]), - ("&Tools", ["&Show Sliders", "", "Clear Terminal", "", "Setup MATLAB"]), + ("&Tools", ["Show &Sliders", "", "Clear Terminal", "", "Setup MATLAB"]), ("&Help", ["&About", "&Help"]), ], ) @@ -187,66 +187,31 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay """Test if menu actions are available and their layouts are as specified in parameterize""" main_menu = test_view.menuBar() - submenu = main_menu.findChild(QtWidgets.QMenu, submenu_name) + submenus = main_menu.findChildren(QtWidgets.QMenu) + for menu in submenus: + if menu.title() == submenu_name: + submenu = menu + break actions = submenu.actions() assert len(actions) == len(action_names_and_layout) for action, name in zip(actions, action_names_and_layout, strict=True): assert action.text() == name -@pytest.fixture -def test_view_with_mdi(): - """An instance of MainWindowView with mdi property defined to some rubbish - for mimicking operations performed in MainWindowView.reset_mdi_layout - """ - +def test_toggle_slider(): mw = MainWindowView() - mw.mdi.addSubWindow(mw.sliders_view_widget) - mdi_windows = mw.mdi.subWindowList() - mw.sliders_view_widget.mdi_holder = mdi_windows[0] - mw.enable_elements() - return mw - - -@patch("rascal2.ui.view.SlidersViewWidget.show") -@patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_click_on_select_sliders_works_as_expected(mock_hide, mock_show, test_view_with_mdi): - """Test if click on menu in the state "Show Slider" changes text appropriately - and initiates correct callback - """ - - main_menu = test_view_with_mdi.menuBar() - submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") - all_actions = submenu.actions() - - # Trigger the action - all_actions[0].trigger() - assert all_actions[0].text() == "&Hide Sliders" - assert test_view_with_mdi.show_sliders - assert mock_show.call_count == 1 - - -@patch("rascal2.ui.view.SlidersViewWidget.show") -@patch("rascal2.ui.view.SlidersViewWidget.hide") -@patch("rascal2.ui.view.ProjectWidget.update_project_view") -def test_click_on_select_tabs_works_as_expected(mock_update_proj, mock_hide, mock_show, test_view_with_mdi): - """Test if click on menu in the state "Show Sliders" changes text appropriately - and initiates correct callback - """ - - main_menu = test_view_with_mdi.menuBar() - submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") - all_actions = submenu.actions() - - # Trigger the action - all_actions[0].trigger() - assert test_view_with_mdi.show_sliders - assert mock_show.call_count == 1 # this would show sliders widget - # check if next click returns to initial state - assert mock_update_proj.call_count == 0 - all_actions[0].trigger() - - assert all_actions[0].text() == "&Show Sliders" - assert not test_view_with_mdi.show_sliders - assert mock_hide.call_count == 1 # this would hide sliders widget - assert mock_update_proj.call_count == 1 + with patch.object(mw, "project_widget") as project_mock: + show_text = mw.toggle_slider_action.property("show_text") + hide_text = mw.toggle_slider_action.property("hide_text") + assert mw.toggle_slider_action.text() == show_text + project_mock.show_slider_view.assert_not_called() + project_mock.show_project_view.assert_not_called() + + mw.toggle_sliders() + + assert mw.toggle_slider_action.text() == hide_text + project_mock.show_slider_view.assert_called_once() + + mw.toggle_sliders() + assert mw.toggle_slider_action.text() == show_text + project_mock.show_project_view.assert_called_once() diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 48a3d31a..22ddc14d 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -254,58 +254,39 @@ def test_parameter_flags(param_model, prior_type, protected): assert item_flags & QtCore.Qt.ItemFlag.ItemIsEditable -@pytest.fixture -def widget_with_delegates(): +def test_param_item_delegates(param_classlist): + """Test that parameter models have the expected item delegates.""" widget = ParameterFieldWidget("Test", parent) widget.parent = MagicMock() + widget.update_model(param_classlist([])) - param = [ratapi.models.Parameter() for i in [0, 1, 2]] - class_list = ratapi.ClassList(param) - widget.update_model(class_list) - - return widget - - -def test_param_item_delegates(widget_with_delegates): - """Test that parameter models have the expected item delegates.""" - - for column, header in enumerate(widget_with_delegates.model.headers, start=1): + for column, header in enumerate(widget.model.headers, start=1): if header in ["min", "value", "max"]: - assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), delegates.ValueSpinBoxDelegate) + assert isinstance(widget.table.itemDelegateForColumn(column), delegates.ValueSpinBoxDelegate) else: - assert isinstance( - widget_with_delegates.table.itemDelegateForColumn(column), delegates.ValidatedInputDelegate - ) + assert isinstance(widget.table.itemDelegateForColumn(column), delegates.ValidatedInputDelegate) -def test_param_item_delegates_exposed_to_sliders(widget_with_delegates): - """Test that parameter models provides the item delegates related to slides""" - - delegates_list = widget_with_delegates.get_item_delegates(["min", "max", "value"]) - assert len(delegates_list) == 3 - - for delegate in delegates_list: - assert isinstance(delegate, delegates.ValueSpinBoxDelegate) - - -def test_hidden_bayesian_columns(widget_with_delegates): +def test_hidden_bayesian_columns(param_classlist): """Test that Bayes columns are hidden when procedure is not Bayesian.""" - - mock_controls = widget_with_delegates.parent.parent.parent_model.controls = MagicMock() + widget = ParameterFieldWidget("Test", parent) + widget.parent = MagicMock() + widget.update_model(param_classlist([])) + mock_controls = widget.parent.parent.parent_model.controls = MagicMock() mock_controls.procedure = "calculate" bayesian_columns = ["prior_type", "mu", "sigma"] - widget_with_delegates.handle_bayesian_columns("calculate") + widget.handle_bayesian_columns("calculate") for item in bayesian_columns: - index = widget_with_delegates.model.headers.index(item) - assert widget_with_delegates.table.isColumnHidden(index + 1) + index = widget.model.headers.index(item) + assert widget.table.isColumnHidden(index + 1) - widget_with_delegates.handle_bayesian_columns("dream") + widget.handle_bayesian_columns("dream") for item in bayesian_columns: - index = widget_with_delegates.model.headers.index(item) - assert not widget_with_delegates.table.isColumnHidden(index + 1) + index = widget.model.headers.index(item) + assert not widget.table.isColumnHidden(index + 1) def test_layer_model_init(): @@ -402,7 +383,7 @@ def test_layer_widget_delegates(init_list): "hydrate_with": delegates.ValidatedInputDelegate, } - widget = LayerFieldWidget("Test", parent) + widget = LayerFieldWidget("test", parent) widget.update_model(init_list) for i, header in enumerate(widget.model.headers): diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index afc5f24f..d8cd7053 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -6,7 +6,6 @@ from PyQt6 import QtCore, QtWidgets from ratapi.utils.enums import Calculations, Geometries, LayerModels -from rascal2.widgets import SlidersViewWidget from rascal2.widgets.project.project import ProjectTabWidget, ProjectWidget, create_draft_project from rascal2.widgets.project.tables import ( ClassListTableModel, @@ -38,21 +37,7 @@ def __init__(self): self.presenter = MockPresenter() self.controls_widget = MagicMock() self.project_widget = None - self.sliders_view_widget = SlidersViewWidget(self) - - def show_or_hide_sliders(self, do_show_sliders=True): - if do_show_sliders: - self.sliders_view_widget.show() - else: - self.sliders_view_widget.hide() - - def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bool = False): - self.sliders_view_widget.setEnabled(is_enabled) - # hide sliders when disabled or else - if is_enabled: - self.show_or_hide_sliders(do_show_sliders=prev_call_vis_sliders_state) - else: - self.show_or_hide_sliders(do_show_sliders=False) + self.toggle_sliders = MagicMock() class DataModel(pydantic.BaseModel, validate_assignment=True): diff --git a/tests/widgets/project/test_slider_view.py b/tests/widgets/project/test_slider_view.py new file mode 100644 index 00000000..c57aa0d5 --- /dev/null +++ b/tests/widgets/project/test_slider_view.py @@ -0,0 +1,125 @@ +from unittest.mock import MagicMock, patch + +import pytest +import ratapi +from PyQt6 import QtWidgets + +from rascal2.ui.view import MainWindowView +from rascal2.widgets.project.project import create_draft_project +from rascal2.widgets.project.slider_view import LabeledSlider, SliderViewWidget + + +@pytest.fixture +def draft_project(): + draft = create_draft_project(ratapi.Project()) + draft["parameters"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="Param 1", min=1, max=10, value=2.1, fit=True), + ratapi.models.Parameter(name="Param 2", min=10, max=100, value=20, fit=True), + ] + ) + draft["bulk_in"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="H2O", min=0, max=1, value=0.2, fit=True), + ] + ) + draft["bulk_out"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="Silicon", min=0, max=1, value=0.2, fit=True), + ] + ) + draft["scalefactors"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="Scale Factor 1", min=0, max=1, value=0.2, fit=True), + ] + ) + draft["background_parameters"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="Background Param 1", min=0, max=1, value=0.2, fit=True), + ] + ) + draft["resolution_parameters"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="Resolution Param 1", min=0, max=1, value=0.2, fit=True), + ] + ) + draft["domain_ratios"] = ratapi.ClassList( + [ + ratapi.models.Parameter(name="Domain ratio 1", min=0, max=1, value=0.2, fit=True), + ] + ) + + return draft + + +def test_no_sliders_creation(): + """Sliders should be created for fitted parameter only""" + mw = MainWindowView() + draft = create_draft_project(ratapi.Project()) + draft["parameters"][0].fit = False + slider_view = SliderViewWidget(draft, mw) + assert len(slider_view.parameters) == 0 + assert len(slider_view._sliders) == 0 + label = slider_view.slider_content_layout.takeAt(0).widget() + assert label.text().startswith("There are no fitted parameters") + + +def test_sliders_creation(draft_project): + """Sliders should be created for fitted parameter only""" + mw = MainWindowView() + slider_view = SliderViewWidget(draft_project, mw) + + assert len(slider_view.parameters) == 8 + assert len(slider_view._sliders) == 8 + + for param_name, slider_name in zip(slider_view.parameters, slider_view._sliders, strict=True): + assert param_name == slider_name + + draft_project["parameters"][0].fit = False + slider_view = SliderViewWidget(draft_project, mw) + assert len(slider_view.parameters) == 7 + assert draft_project["parameters"][0].name not in slider_view._sliders + + +def test_slider_buttons(): + mw = MainWindowView() + draft = create_draft_project(ratapi.Project()) + mw.toggle_sliders = MagicMock() + mw.plot_widget.update_plots = MagicMock() + mw.presenter.edit_project = MagicMock() + + slider_view = SliderViewWidget(draft, mw) + buttons = slider_view.findChildren(QtWidgets.QPushButton) + accept_button = buttons[0] + accept_button.click() + mw.toggle_sliders.assert_called_once() + mw.presenter.edit_project.assert_called_once_with(draft) + + mw.toggle_sliders.reset_mock() + reject_button = buttons[1] + reject_button.click() + mw.toggle_sliders.assert_called_once() + mw.plot_widget.update_plots.assert_called_once() + + +@pytest.mark.parametrize( + "param", + [ + ratapi.models.Parameter(name="Param 1", min=1, max=75, value=21, fit=True), + ratapi.models.Parameter(name="Param 2", min=-0.1, max=0.5, value=0.3, fit=True), + ratapi.models.Parameter(name="Param 3", min=3, max=3, value=3, fit=True), + ], +) +@patch("rascal2.widgets.project.slider_view.SliderViewWidget", autospec=True) +def test_labelled_slider_value(slider_view, param): + slider_view.update_result_and_plots = MagicMock() + slider = LabeledSlider(param, slider_view) + # actual range of the slider should never change but + # value would be scaled to parameter range. + assert slider._slider.maximum() == 100 + assert slider._slider.minimum() == 0 + assert slider._slider.value() == slider._param_value_to_slider_value(param.value) + + slider._slider.setValue(79) + assert param.value == slider._slider_value_to_param_value(slider._slider.value()) + slider_view.update_result_and_plots.assert_called_once() diff --git a/tests/widgets/test_labeled_slider_class.py b/tests/widgets/test_labeled_slider_class.py deleted file mode 100644 index 11fd1135..00000000 --- a/tests/widgets/test_labeled_slider_class.py +++ /dev/null @@ -1,133 +0,0 @@ -import pydantic -import pytest -import ratapi -from PyQt6 import QtCore, QtWidgets - -from rascal2.widgets.project.tables import ParametersModel -from rascal2.widgets.sliders_view import LabeledSlider, SliderChangeHolder - - -class ParametersModelMock(ParametersModel): - _value: float - _index: QtCore.QModelIndex - _role: QtCore.Qt.ItemDataRole - _recalculate_proj: bool - call_count: int - - def __init__(self, class_list: ratapi.ClassList, parent: QtWidgets.QWidget): - super().__init__(class_list, parent) - self.call_count = 0 - - def setData( - self, index: QtCore.QModelIndex, val: float, qt_role=QtCore.Qt.ItemDataRole.EditRole, recalculate_project=True - ) -> bool: - self._index = index - self._value = val - self._role = qt_role - self._recalculate_proj = recalculate_project - self.call_count += 1 - return True - - -class DataModel(pydantic.BaseModel, validate_assignment=True): - """A test Pydantic model.""" - - name: str - min: float - max: float - value: float - fit: bool - show_priors: bool - - -@pytest.fixture -def slider(): - param = ratapi.models.Parameter(name="Test Slider", min=1, max=10, value=2.1, fit=True) - parent = QtWidgets.QWidget() - class_view = ratapi.ClassList( - [ - DataModel(name="Slider_A", min=0, value=1, max=100, fit=True, show_priors=False), - DataModel(name="Slider_B", min=0, value=1, max=100, fit=True, show_priors=False), - DataModel(name="Slider_C", min=0, value=1, max=100, fit=True, show_priors=False), - ] - ) - model = ParametersModelMock(class_view, parent) - # note 3 elements in ratapi.ClassList needed for row_number == 2 to work - inputs = SliderChangeHolder(row_number=2, model=model, param=param) - return LabeledSlider(inputs) - - -def test_a_slider_construction(slider): - """constructing a slider widget works and have all necessary properties""" - assert slider.slider_name == "Test Slider" - assert slider._value_min == 1 - assert slider._value_range == 10 - 1 - assert slider._value == 2.1 - assert slider._value_step == 9 / 100 - assert len(slider._labels) == 11 - - -def test_a_slider_label_range(slider): - """check if labels cover whole property range""" - assert len(slider._labels) == 11 - assert slider._labels[0].text() == slider._tick_label_format.format(1) - assert slider._labels[-1].text() == slider._tick_label_format.format(10) - - -def test_a_slider_value_text(slider): - """check if slider have correct value label""" - assert slider._value_label.text() == slider._value_label_format.format(2.1) - - -def test_set_slider_value_changes_label(slider): - """check if slider accepts correct value and uses correct index""" - slider.set_slider_gui_position(4) - assert slider._value_label.text() == slider._value_label_format.format(4) - idx = slider._value_to_slider_pos(4) - assert slider._slider.value() == idx - - -def test_set_slider_max_value_in_range(slider): - """round-off error keep sliders within the ranges""" - slider.set_slider_gui_position(slider._value_max) - assert slider._value_label.text() == slider._value_label_format.format(slider._value_max) - assert slider._slider.value() == slider._slider_max_idx - - -def test_set_slider_min_value_in_range(slider): - """round-off error keep sliders within the ranges""" - slider.set_slider_gui_position(slider._value_min) - assert slider._value_label.text() == slider._value_label_format.format(slider._value_min) - assert slider._slider.value() == 0 - - -def test_set_value_do_correct_calls(slider): - """update value bound correctly and does correct calls""" - - assert slider._prop._vis_model.call_count == 0 - slider._slider.setValue(50) - float_val = slider._slider_pos_to_value(50) - assert float_val == slider._value - assert slider._slider.value() == 50 - assert slider._prop._vis_model.call_count == 1 - assert slider._prop._vis_model._value == float_val - assert slider._prop._vis_model._index.row() == 2 # row number in slider fixture - assert slider._prop._vis_model._role == QtCore.Qt.ItemDataRole.EditRole # row number in slider fixture - - -@pytest.mark.parametrize( - "minmax_slider_idx, min_max_prop_value", - [ - (0, 1), # min_max indices are the indices hardwired in class and - (100, 10), # min_max values are the values supplied for property in the slider fixture - ], -) -def test_set_values_in_limits_work(slider, minmax_slider_idx, min_max_prop_value): - """update_value bound correctly and does correct calls at limiting values""" - - slider._slider.setValue(minmax_slider_idx) - assert min_max_prop_value == slider._value - assert slider._slider.value() == minmax_slider_idx - assert slider._value == min_max_prop_value - assert slider._prop._vis_model._value == min_max_prop_value - assert slider._prop.param.value == min_max_prop_value diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py deleted file mode 100644 index b5474da7..00000000 --- a/tests/widgets/test_sliders_widget.py +++ /dev/null @@ -1,234 +0,0 @@ -from unittest.mock import patch - -import pytest -import ratapi -from PyQt6 import QtWidgets - -from rascal2.ui.view import MainWindowView -from rascal2.widgets.project.project import create_draft_project -from rascal2.widgets.project.tables import ParameterFieldWidget -from rascal2.widgets.sliders_view import EmptySlider, LabeledSlider - - -class MockFigureCanvas(QtWidgets.QWidget): - """A mock figure canvas.""" - - def draw(*args, **kwargs): - pass - - -@pytest.fixture -def view_with_proj(): - """An instance of MainWindowView with project partially defined - for mimicking sliders generation from project tabs - """ - mw = MainWindowView() - - draft = create_draft_project(ratapi.Project()) - draft["parameters"] = ratapi.ClassList( - [ - ratapi.models.Parameter(name="Param 1", min=1, max=10, value=2.1, fit=True), - ratapi.models.Parameter(name="Param 2", min=10, max=100, value=20, fit=False), - ratapi.models.Parameter(name="Param 3", min=100, max=1000, value=209, fit=True), - ratapi.models.Parameter(name="Param 4", min=200, max=2000, value=409, fit=True), - ] - ) - draft["background_parameters"] = ratapi.ClassList( - [ - ratapi.models.Parameter(name="Background Param 1", min=0, max=1, value=0.2, fit=False), - ] - ) - project = ratapi.Project(name="Sliders Test Project") - for param in draft["parameters"]: - project.parameters.append(param) - for param in draft["background_parameters"]: - project.parameters.append(param) - - mw.project_widget.view_tabs["Parameters"].update_model(draft) - mw.presenter.model.project = project - - yield mw - - -def test_extract_properties_for_sliders(view_with_proj): - update_sliders = view_with_proj.sliders_view_widget._init_properties_for_sliders() - assert not update_sliders # its false as at first call sliders should be regenerated - assert len(view_with_proj.sliders_view_widget._prop_to_change) == 3 - assert list(view_with_proj.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3", "Param 4"] - assert list(view_with_proj.sliders_view_widget._values_to_revert.values()) == [2.1, 209.0, 409] - assert view_with_proj.sliders_view_widget._init_properties_for_sliders() # now its true as sliders should be - # available for update on second call - - -@patch("rascal2.ui.view.SlidersViewWidget._update_sliders_widgets") -@patch("rascal2.ui.view.SlidersViewWidget._add_sliders_widgets") -def test_create_update_called(add_sliders, update_sliders, view_with_proj): - view_with_proj.sliders_view_widget.init() - assert add_sliders.called == 1 - assert update_sliders.called == 0 - view_with_proj.sliders_view_widget.init() - assert add_sliders.called == 1 - assert update_sliders.called == 1 - - -def test_init_slider_widget_builds_sliders(view_with_proj): - view_with_proj.sliders_view_widget.init() - assert len(view_with_proj.sliders_view_widget._sliders) == 3 - assert "Param 1" in view_with_proj.sliders_view_widget._sliders - assert "Param 3" in view_with_proj.sliders_view_widget._sliders - assert "Param 4" in view_with_proj.sliders_view_widget._sliders - slider1 = view_with_proj.sliders_view_widget._sliders["Param 1"] - slider2 = view_with_proj.sliders_view_widget._sliders["Param 3"] - slider3 = view_with_proj.sliders_view_widget._sliders["Param 4"] - assert slider1._prop._vis_model == view_with_proj.project_widget.view_tabs["Parameters"].tables["parameters"].model - assert slider2._prop._vis_model == view_with_proj.project_widget.view_tabs["Parameters"].tables["parameters"].model - assert slider3._prop._vis_model == view_with_proj.project_widget.view_tabs["Parameters"].tables["parameters"].model - - -def fake_update(self, recalculate_project): - fake_update.num_calls += 1 - fake_update.project_updated.append(recalculate_project) - - -fake_update.num_calls = 0 -fake_update.project_updated = [] - - -def test_identify_changed_properties_empty_for_unchanged(view_with_proj): - view_with_proj.sliders_view_widget.init() - - assert len(view_with_proj.sliders_view_widget._identify_changed_properties()) == 0 - - -def test_identify_changed_properties_picks_up_changed(view_with_proj): - view_with_proj.sliders_view_widget.init() - view_with_proj.sliders_view_widget._values_to_revert["Param 1"] = 4 - view_with_proj.sliders_view_widget._values_to_revert["Param 3"] = 400 - - assert len(view_with_proj.sliders_view_widget._identify_changed_properties()) == 2 - assert list(view_with_proj.sliders_view_widget._identify_changed_properties().keys()) == ["Param 1", "Param 3"] - - -@patch.object(ParameterFieldWidget, "update_project", fake_update) -@patch("rascal2.ui.view.ProjectWidget.show_project_view") -def test_cancel_button_called(mock_show_project, view_with_proj): - """Cancel button sets value of controlled properties to value, stored in - _value_to_revert dictionary - """ - - view_with_proj.sliders_view_widget.init() - - view_with_proj.sliders_view_widget._values_to_revert["Param 1"] = 4 - view_with_proj.sliders_view_widget._values_to_revert["Param 3"] = 400 - cancel_button = view_with_proj.sliders_view_widget.findChild(QtWidgets.QPushButton, "CancelButton") - - cancel_button.click() - - assert fake_update.num_calls == 2 - # project update should be true for last property change - assert fake_update.project_updated == [False, True] - assert not view_with_proj.show_sliders - assert view_with_proj.presenter.model.project.parameters["Param 1"].value == 4 - assert view_with_proj.presenter.model.project.parameters["Param 2"].value == 20 - assert view_with_proj.presenter.model.project.parameters["Param 3"].value == 400 - assert view_with_proj.presenter.model.project.parameters["Param 4"].value == 409 - - assert mock_show_project.call_count == 1 - - -@patch("rascal2.ui.view.SlidersViewWidget._apply_changes_from_sliders") -def test_cancel_accept_button_connections(mock_accept, view_with_proj): - view_with_proj.sliders_view_widget.init() - - accept_button = view_with_proj.sliders_view_widget.findChild(QtWidgets.QPushButton, "AcceptButton") - accept_button.clicked.disconnect() # previous actual function was connected regardless - accept_button.clicked.connect(view_with_proj.sliders_view_widget._apply_changes_from_sliders) - accept_button.click() - assert mock_accept.called == 1 - - -@patch("rascal2.ui.view.SlidersViewWidget._cancel_changes_from_sliders") -def test_cancel_cancel_button_connections(mock_cancel, view_with_proj): - view_with_proj.sliders_view_widget.init() - cancel_button = view_with_proj.sliders_view_widget.findChild(QtWidgets.QPushButton, "CancelButton") - cancel_button.clicked.disconnect() # previous actual function was connected regardless - cancel_button.clicked.connect(view_with_proj.sliders_view_widget._cancel_changes_from_sliders) - - cancel_button.click() - assert mock_cancel.called == 1 - - -def fake_show_or_hide_sliders(self, do_show_sliders): - fake_show_or_hide_sliders.num_calls = +1 - fake_show_or_hide_sliders.call_param = do_show_sliders - - -fake_show_or_hide_sliders.num_calls = 0 -fake_show_or_hide_sliders.call_param = [] - - -@patch.object(MainWindowView, "show_or_hide_sliders", fake_show_or_hide_sliders) -def test_apply_cancel_changes_called_hide_sliders(view_with_proj): - view_with_proj.sliders_view_widget._cancel_changes_from_sliders() - assert fake_show_or_hide_sliders.num_calls == 1 - assert not fake_show_or_hide_sliders.call_param - - fake_show_or_hide_sliders.num_calls = 0 - fake_show_or_hide_sliders.call_param = [] - - view_with_proj.sliders_view_widget._apply_changes_from_sliders() - assert fake_show_or_hide_sliders.num_calls == 1 - assert not fake_show_or_hide_sliders.call_param - - -# ====================================================================================================================== -def set_proj_properties_fit_to_requested(proj, true_list: list): - """set up all projects properties "fit" parameter to False except provided - within the true_list, which to be set to True""" - - project = proj.presenter.model.project - for field in ratapi.Project.model_fields: - attr = getattr(project, field) - if isinstance(attr, ratapi.ClassList): - for item in attr: - if hasattr(item, "fit"): - if item.name in true_list: - item.fit = True - else: - item.fit = False - - -def test_empty_slider_generated(view_with_proj): - set_proj_properties_fit_to_requested(view_with_proj, []) - - view_with_proj.sliders_view_widget.init() - assert len(view_with_proj.sliders_view_widget._sliders) == 1 - slider1 = view_with_proj.sliders_view_widget._sliders["Empty Slider"] - assert isinstance(slider1, EmptySlider) - - -def test_empty_slider_updated(view_with_proj): - set_proj_properties_fit_to_requested(view_with_proj, []) - - view_with_proj.sliders_view_widget.init() - assert len(view_with_proj.sliders_view_widget._sliders) == 1 - slider1 = view_with_proj.sliders_view_widget._sliders["Empty Slider"] - assert isinstance(slider1, EmptySlider) - view_with_proj.sliders_view_widget.init() - assert isinstance(slider1, EmptySlider) - - -def test_empty_slider_removed(view_with_proj): - set_proj_properties_fit_to_requested(view_with_proj, []) - - view_with_proj.sliders_view_widget.init() - assert len(view_with_proj.sliders_view_widget._sliders) == 1 - slider1 = view_with_proj.sliders_view_widget._sliders["Empty Slider"] - assert isinstance(slider1, EmptySlider) - - set_proj_properties_fit_to_requested(view_with_proj, ["Param 2"]) - - view_with_proj.sliders_view_widget.init() - assert len(view_with_proj.sliders_view_widget._sliders) == 1 - slider1 = view_with_proj.sliders_view_widget._sliders["Param 2"] - assert isinstance(slider1, LabeledSlider)