From 90a4420643659a55e560d3bb125e2a78337a9993 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 21 Oct 2025 17:58:27 +0100 Subject: [PATCH 01/69] Re #132 Menu and stub for sliders widget --- rascal2/ui/view.py | 15 +++++++++++ rascal2/widgets/project/tables.py | 44 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 6ac85324..cf862a8b 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -162,6 +162,11 @@ def create_actions(self): open_help_action.triggered.connect(self.open_docs) self.open_help_action = open_help_action + show_or_hide_slider_action = QtGui.QAction("&Show Sliders", self) + show_or_hide_slider_action.setStatusTip("Show or Hide Sliders") + show_or_hide_slider_action.triggered.connect(self.show_or_hide_sliders) + self._show_or_hide_slider_action = 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) @@ -238,6 +243,8 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): tools_menu = main_menu.addMenu("&Tools") tools_menu.setObjectName("&Tools") + tools_menu.addAction(self._show_or_hide_slider_action) + tools_menu.addSeparator() tools_menu.addAction(self.clear_terminal_action) tools_menu.addSeparator() tools_menu.addAction(self.setup_matlab_action) @@ -247,6 +254,14 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): help_menu.addAction(self.open_about_action) help_menu.addAction(self.open_help_action) + def show_or_hide_sliders(self): + """Depending on current state, show or hide sliders submenu""" + if "Sliders" in self._show_or_hide_slider_action.text(): + self._show_or_hide_slider_action.setText("&Show Parameters") + else: + self._show_or_hide_slider_action.setText("&Show Sliders") + + def open_about_info(self): """Opens about menu containing information about RASCAL gui""" self.about_dialog.update_rascal_info(self) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 16d7b1dd..f9db8832 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -381,6 +381,50 @@ def edit(self): if i in self.model.protected_indices: self.table.setIndexWidget(self.model.index(i, 0), None) +class ParameterSliderWidget(ProjectFieldWidget): + """Subclass of field widgets for slider view""" + + classlist_model = ParametersModel + + def set_item_delegates(self): + for i, header in enumerate(self.model.headers): + if header in ["min", "value", "max"]: + self.table.setItemDelegateForColumn(i + 1, delegates.ValueSpinBoxDelegate(header, self.table)) + else: + self.table.setItemDelegateForColumn( + i + 1, delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table) + ) + + def update_model(self, classlist): + super().update_model(classlist) + header = self.table.horizontalHeader() + header.setSectionResizeMode( + self.model.headers.index("fit") + 1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents + ) + + def handle_bayesian_columns(self, procedure: Procedures): + """Hide or show Bayes-related columns based on procedure. + + Parameters + ---------- + procedure : Procedure + The procedure in Controls. + """ + is_bayesian = procedure in ["ns", "dream"] + bayesian_columns = ["prior_type", "mu", "sigma"] + for item in bayesian_columns: + index = self.model.headers.index(item) + if is_bayesian: + self.table.showColumn(index + 1) + else: + self.table.hideColumn(index + 1) + + def edit(self): + super().edit() + for i in range(0, self.model.rowCount()): + if i in self.model.protected_indices: + self.table.setIndexWidget(self.model.index(i, 0), None) + class LayersModel(ClassListTableModel): """Classlist model for Layers.""" From 20228fcbacf266276e468e2469eb80613a72282f Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 22 Oct 2025 10:39:23 +0100 Subject: [PATCH 02/69] Re #132 Dependency necessary for testing slider widget --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 674ed904..e69135e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ pytest pytest-cov +pytest-qt ruff Sphinx From f9f3add24f591c862e92bb4813f4c442fa60d900 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 24 Oct 2025 16:00:06 +0100 Subject: [PATCH 03/69] Re #132 Looks like working change on a project level --- rascal2/ui/view.py | 26 +++++++-- rascal2/widgets/project/project.py | 84 +++++++++++++++++++++--------- rascal2/widgets/project/tables.py | 49 +++-------------- 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index cf862a8b..18954d76 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -23,6 +23,10 @@ class MainWindowView(QtWidgets.QMainWindow): def __init__(self): super().__init__() + #Public interface + self.disabled_elements = [] + self.display_sliders = False + self.setWindowTitle(MAIN_WINDOW_TITLE) window_icon = QtGui.QIcon(path_for("logo.png")) @@ -41,7 +45,12 @@ def __init__(self): self.controls_widget = ControlsWidget(self) self.project_widget = ProjectWidget(self) - self.disabled_elements = [] + ## protected interface and public properties construction + + # define menu controlling switch between table and slider views + self._sliders_menu_control = { + "ShowSliders":"&Show Table", # if state is show sliders, click will show table + "HideSliders":"&Show Sliders"} # if state is show table, click will show sliders self.create_actions() @@ -162,7 +171,11 @@ def create_actions(self): open_help_action.triggered.connect(self.open_docs) self.open_help_action = open_help_action - show_or_hide_slider_action = QtGui.QAction("&Show Sliders", self) + + if self.display_sliders: + show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control["ShowSliders"], self) + else: + show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control["HideSliders"], self) show_or_hide_slider_action.setStatusTip("Show or Hide Sliders") show_or_hide_slider_action.triggered.connect(self.show_or_hide_sliders) self._show_or_hide_slider_action = show_or_hide_slider_action @@ -256,10 +269,13 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): def show_or_hide_sliders(self): """Depending on current state, show or hide sliders submenu""" - if "Sliders" in self._show_or_hide_slider_action.text(): - self._show_or_hide_slider_action.setText("&Show Parameters") + self.display_sliders = not self.display_sliders + if self.display_sliders: + self._show_or_hide_slider_action.setText(self._sliders_menu_control["ShowSliders"]) else: - self._show_or_hide_slider_action.setText("&Show Sliders") + self._show_or_hide_slider_action.setText(self._sliders_menu_control["HideSliders"]) + + self.project_widget.select_list_or_sliders_view(self.display_sliders) def open_about_info(self): diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index e93aa770..2b3dad9d 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -17,6 +17,7 @@ DomainContrastWidget, LayerFieldWidget, ParameterFieldWidget, + ParameterSliderWidget, ProjectFieldWidget, ResolutionsFieldWidget, ) @@ -56,9 +57,14 @@ def __init__(self, parent): } # track which tabs are lists (for syncing) self.list_tabs = ["Contrasts", "Data"] - - self.view_tabs = {} - self.edit_tabs = {} + # define project tabs which have slider views + self._tab_has_sliders = ["Parameters"] + + self._view_tabs = {} + self._edit_tabs = {} + self._slider_tabs = {} + # public tab_stacks property to wire to global tab selection + self._tab_stacks = {} self.draft_project = None # for making model type changes non-destructive self.old_contrast_models = {} @@ -135,14 +141,31 @@ def create_project_view(self) -> None: self.project_tab = QtWidgets.QTabWidget() for tab, fields in self.tabs.items(): - widget = self.view_tabs[tab] = ProjectTabWidget(fields, self) - self.project_tab.addTab(widget, tab) + view_widget = self._view_tabs[tab] = ProjectTabWidget(fields, self,False,False) + if tab in self._tab_has_sliders: + slider_widget = self._slider_tabs[tab] = ProjectTabWidget(fields, self,False,True) + self._tab_stacks[tab] = QtWidgets.QStackedLayout() + self._tab_stacks[tab].addWidget(view_widget) + self._tab_stacks[tab].addWidget(slider_widget) + the_widget = QtWidgets.QWidget() + the_widget.setLayout(self._tab_stacks[tab]) + else: + the_widget = view_widget + self.project_tab.addTab(the_widget, tab) main_layout.addWidget(self.project_tab) project_widget.setLayout(main_layout) return project_widget + def select_list_or_sliders_view(self,select_sliders: bool = False) -> None : + """Depending on select_sliders variable, display tab with sliders view or list view""" + for tab in self._tab_stacks.values(): + if select_sliders: + tab.setCurrentIndex(1) + else: + tab.setCurrentIndex(0) + def create_edit_view(self) -> None: """Creates the project edit view""" @@ -215,7 +238,7 @@ def create_edit_view(self) -> None: self.calculation_combobox.currentTextChanged.connect(lambda s: self.handle_model_update(s)) self.calculation_combobox.currentTextChanged.connect(lambda: self.handle_tabs()) self.calculation_combobox.currentTextChanged.connect( - lambda s: self.edit_tabs["Contrasts"].tables["contrasts"].set_domains(s == Calculations.Domains) + lambda s: self._edit_tabs["Contrasts"].tables["contrasts"].set_domains(s == Calculations.Domains) ) # when model type changed, hide/show layers tab and change model field in contrasts @@ -226,15 +249,15 @@ def create_edit_view(self) -> None: self.edit_project_tab = QtWidgets.QTabWidget() for tab, fields in self.tabs.items(): - widget = self.edit_tabs[tab] = ProjectTabWidget(fields, self, edit_mode=True) + widget = self._edit_tabs[tab] = ProjectTabWidget(fields, self, edit_mode=True) self.edit_project_tab.addTab(widget, tab) self.edit_absorption_checkbox.checkStateChanged.connect( - lambda s: self.edit_tabs["Layers"].tables["layers"].set_absorption(s == QtCore.Qt.CheckState.Checked) + lambda s: self._edit_tabs["Layers"].tables["layers"].set_absorption(s == QtCore.Qt.CheckState.Checked) ) for tab in ["Experimental Parameters", "Layers", "Backgrounds", "Resolutions", "Data", "Domains"]: - for table in self.edit_tabs[tab].tables.values(): - table.edited.connect(lambda: self.edit_tabs["Contrasts"].tables["contrasts"].update_item_view()) + for table in self._edit_tabs[tab].tables.values(): + table.edited.connect(lambda: self._edit_tabs["Contrasts"].tables["contrasts"].update_item_view()) main_layout.addWidget(self.edit_project_tab) @@ -247,7 +270,7 @@ def update_project_view(self, update_tab_index=None) -> None: if update_tab_index is None: update_tab_index = self.stacked_widget.currentIndex() - tab_to_update = self.view_tabs if update_tab_index == 0 else self.edit_tabs + tab_to_update = self._view_tabs if update_tab_index == 0 else self._edit_tabs tab_indices = {} for tab in self.list_tabs: model = tab_to_update[tab].tables[tab.lower()].list.selectionModel() @@ -258,8 +281,8 @@ def update_project_view(self, update_tab_index=None) -> None: self.draft_project: dict = create_draft_project(self.parent_model.project) for tab in self.tabs: - self.view_tabs[tab].update_model(self.draft_project) - self.edit_tabs[tab].update_model(self.draft_project) + self._view_tabs[tab].update_model(self.draft_project) + self._edit_tabs[tab].update_model(self.draft_project) self.absorption_checkbox.setChecked(self.parent_model.project.absorption) self.calculation_type.setText(self.parent_model.project.calculation) @@ -275,7 +298,7 @@ def update_project_view(self, update_tab_index=None) -> None: self.handle_controls_update() for tab in self.list_tabs: - tab_widget = self.view_tabs[tab].tables[tab.lower()] + tab_widget = self._view_tabs[tab].tables[tab.lower()] idx = tab_indices[tab] tab_widget.list.selectionModel().setCurrentIndex( tab_widget.model.index(idx, 0), QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect @@ -296,18 +319,18 @@ def update_draft_project(self, new_values: dict) -> None: def handle_tabs(self) -> None: """Displays or hides tabs as relevant.""" # the domains tab should only be visible if calculating domains - domain_tab_index = list(self.view_tabs).index("Domains") + domain_tab_index = list(self._view_tabs).index("Domains") is_domains = self.calculation_combobox.currentText() == Calculations.Domains self.project_tab.setTabVisible(domain_tab_index, is_domains) self.edit_project_tab.setTabVisible(domain_tab_index, is_domains) # the layers tab and domain contrasts table should only be visible in standard layers - layers_tab_index = list(self.view_tabs).index("Layers") + layers_tab_index = list(self._view_tabs).index("Layers") is_layers = self.model_combobox.currentText() == LayerModels.StandardLayers self.project_tab.setTabVisible(layers_tab_index, is_layers) self.edit_project_tab.setTabVisible(layers_tab_index, is_layers) - self.view_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) - self.edit_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) + self._view_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) + self._edit_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) def handle_controls_update(self): """Handle updates to Controls that need to be reflected in the project.""" @@ -317,8 +340,8 @@ def handle_controls_update(self): controls = self.parent_model.controls for tab in self.tabs: - self.view_tabs[tab].handle_controls_update(controls) - self.edit_tabs[tab].handle_controls_update(controls) + self._view_tabs[tab].handle_controls_update(controls) + self._edit_tabs[tab].handle_controls_update(controls) def handle_model_update(self, new_entry): """Handle updates to the model type. @@ -350,7 +373,7 @@ def handle_model_update(self, new_entry): contrast.model = self.old_contrast_models.get(contrast.name, []) self.old_contrast_models = old_contrast_models - self.edit_tabs["Contrasts"].tables["contrasts"].update_item_view() + self._edit_tabs["Contrasts"].tables["contrasts"].update_item_view() def show_project_view(self) -> None: """Show project view""" @@ -497,7 +520,7 @@ def set_editing_enabled(self, enabled: bool): for tab_name, tab_items in self.tabs.items(): for table in tab_items: if table in ratapi.project.parameter_class_lists: - self.view_tabs[tab_name].tables[table].setEnabled(enabled) + self._view_tabs[tab_name].tables[table].setEnabled(enabled) class ProjectTabWidget(QtWidgets.QWidget): @@ -510,21 +533,30 @@ class ProjectTabWidget(QtWidgets.QWidget): fields : list[str] The fields to display in the tab. parent : QtWidgets.QWidget - The parent to this widget. - + ProjectWidget which holds multiple TabWidgets and controls their appearance. + edit_mode : bool + Build Tab widget in edit mode + slider_mode : bool + Build Tab widget in slider mode, i.e. generate slider instead of lists. + The mode is something intermediate between edit and view mode, as allows + changing some values but not adding new properties """ - def __init__(self, fields: list[str], parent, edit_mode: bool = False): + def __init__(self, fields: list[str], parent, edit_mode: bool = False, slider_mode: bool = False): super().__init__(parent) self.parent = parent self.fields = fields self.edit_mode = edit_mode self.tables = {} + layout = QtWidgets.QVBoxLayout() for field in self.fields: if field in ratapi.project.parameter_class_lists: - self.tables[field] = ParameterFieldWidget(field, self) + if slider_mode: + self.tables[field] = ParameterSliderWidget(field, self) + else: + self.tables[field] = ParameterFieldWidget(field, self) elif field == "backgrounds": self.tables[field] = BackgroundsFieldWidget(field, self) elif field == "resolutions": diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index f9db8832..f7fcb4ae 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -381,49 +381,16 @@ def edit(self): if i in self.model.protected_indices: self.table.setIndexWidget(self.model.index(i, 0), None) -class ParameterSliderWidget(ProjectFieldWidget): +class ParameterSliderWidget(QtWidgets.QWidget): """Subclass of field widgets for slider view""" + def __init__(self, field: str, parent): + super().__init__(parent) + self.field = field + layout = QtWidgets.QVBoxLayout() + contents = QtWidgets.QLabel(" slides for {} have not been implemented".format(field)) + layout.addWidget(contents) + self.setLayout(layout) - classlist_model = ParametersModel - - def set_item_delegates(self): - for i, header in enumerate(self.model.headers): - if header in ["min", "value", "max"]: - self.table.setItemDelegateForColumn(i + 1, delegates.ValueSpinBoxDelegate(header, self.table)) - else: - self.table.setItemDelegateForColumn( - i + 1, delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table) - ) - - def update_model(self, classlist): - super().update_model(classlist) - header = self.table.horizontalHeader() - header.setSectionResizeMode( - self.model.headers.index("fit") + 1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents - ) - - def handle_bayesian_columns(self, procedure: Procedures): - """Hide or show Bayes-related columns based on procedure. - - Parameters - ---------- - procedure : Procedure - The procedure in Controls. - """ - is_bayesian = procedure in ["ns", "dream"] - bayesian_columns = ["prior_type", "mu", "sigma"] - for item in bayesian_columns: - index = self.model.headers.index(item) - if is_bayesian: - self.table.showColumn(index + 1) - else: - self.table.hideColumn(index + 1) - - def edit(self): - super().edit() - for i in range(0, self.model.rowCount()): - if i in self.model.protected_indices: - self.table.setIndexWidget(self.model.index(i, 0), None) class LayersModel(ClassListTableModel): From 53395690cc2c7c4eae7858d6b2d6a252e1749663 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 24 Oct 2025 21:08:45 +0100 Subject: [PATCH 04/69] Re #132 working test for MainWindowView changes state of the Project view (with qtbot) --- rascal2/ui/view.py | 10 +++++--- tests/ui/test_view.py | 57 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 18954d76..69d4cadd 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -49,13 +49,13 @@ def __init__(self): # define menu controlling switch between table and slider views self._sliders_menu_control = { - "ShowSliders":"&Show Table", # if state is show sliders, click will show table + "ShowSliders":"&Show Tables", # if state is show sliders, click will show table "HideSliders":"&Show Sliders"} # if state is show table, click will show sliders self.create_actions() - self.main_menu = self.menuBar() - self.add_submenus(self.main_menu) + main_menu = self.menuBar() + self.add_submenus(main_menu) self.create_toolbar() self.create_status_bar() @@ -268,7 +268,9 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): help_menu.addAction(self.open_help_action) def show_or_hide_sliders(self): - """Depending on current state, show or hide sliders submenu""" + """Depending on current state, show or hide sliders for + table properties within Project class view. + """ self.display_sliders = not self.display_sliders if self.display_sliders: self._show_or_hide_slider_action.setText(self._sliders_menu_control["ShowSliders"]) diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 3dd4e7e7..b6f2f046 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -123,18 +123,12 @@ def change_dir(*args, **kwargs): mock_overwrite.assert_called_once() -def test_menu_bar_present(test_view): - """Test menu bar is present""" - - assert hasattr(test_view, "main_menu") - assert isinstance(test_view.main_menu, QtWidgets.QMenuBar) - @pytest.mark.parametrize("submenu_name", ["&File", "&Edit", "&Windows", "&Tools", "&Help"]) def test_menu_element_present(test_view, submenu_name): """Test requested menu items are present""" - main_menu = test_view.main_menu + main_menu = test_view.menuBar() elements = main_menu.children() assert any(hasattr(submenu, "title") and submenu.title() == submenu_name for submenu in elements) @@ -163,16 +157,61 @@ 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", ["Clear Terminal", "", "Setup MATLAB"]), + ("&Tools", ["&Show Sliders","","Clear Terminal", "", "Setup MATLAB"]), ("&Help", ["&About", "&Help"]), ], ) def test_help_menu_actions_present(test_view, submenu_name, action_names_and_layout): """Test if menu actions are available and their layouts are as specified in parameterize""" - main_menu = test_view.main_menu + main_menu = test_view.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, submenu_name) 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 + +@patch("rascal2.ui.view.ProjectWidget.select_list_or_sliders_view") +def test_click_on_select_sliders_works_as_expected(mock_select_view,test_view,qtbot): + """Test if click on menu in the state "Show Slider" changes text appropriately + and initiates correct callback + """ + qtbot.addWidget(test_view) + + # check initial state -- defined now but needs to be refactored when + # this may be included in configuration + assert test_view.display_sliders == False + + main_menu = test_view.menuBar() + submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") + all_actions = submenu.actions() + + # Trigger the action + all_actions[0].trigger() + assert all_actions[0].text() == "&Show Tables" + assert test_view.display_sliders == True + assert mock_select_view.call_count == 1 + +@patch("rascal2.ui.view.ProjectWidget.select_list_or_sliders_view") +def test_click_on_select_tabs_works_as_expected(mock_select_view,test_view,qtbot): + """Test if click on menu in the state "Show Tabs" changes text appropriately + and initiates correct callback + """ + qtbot.addWidget(test_view) + # check initial state -- defined now but needs to be refactored when + # this may be included in configuration + assert test_view.display_sliders == False + + main_menu = test_view.menuBar() + submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") + all_actions = submenu.actions() + + # Trigger the action + all_actions[0].trigger() + assert test_view.display_sliders == True + # check if next click returns to initial state + all_actions[0].trigger() + + assert all_actions[0].text() == "&Show Sliders" + assert test_view.display_sliders == False + assert mock_select_view.call_count == 2 # 2 as second click returned to initial state From 7dac8367cf3cff7ebe58d1195677a422c0f132f3 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 24 Oct 2025 21:12:03 +0100 Subject: [PATCH 05/69] Re #132 removed qtbot from test in favour of generic mock --- tests/ui/test_view.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index b6f2f046..b75a3291 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -172,11 +172,10 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay assert action.text() == name @patch("rascal2.ui.view.ProjectWidget.select_list_or_sliders_view") -def test_click_on_select_sliders_works_as_expected(mock_select_view,test_view,qtbot): +def test_click_on_select_sliders_works_as_expected(mock_select_view,test_view): """Test if click on menu in the state "Show Slider" changes text appropriately and initiates correct callback """ - qtbot.addWidget(test_view) # check initial state -- defined now but needs to be refactored when # this may be included in configuration @@ -193,11 +192,11 @@ def test_click_on_select_sliders_works_as_expected(mock_select_view,test_view,qt assert mock_select_view.call_count == 1 @patch("rascal2.ui.view.ProjectWidget.select_list_or_sliders_view") -def test_click_on_select_tabs_works_as_expected(mock_select_view,test_view,qtbot): +def test_click_on_select_tabs_works_as_expected(mock_select_view,test_view): """Test if click on menu in the state "Show Tabs" changes text appropriately and initiates correct callback """ - qtbot.addWidget(test_view) + # check initial state -- defined now but needs to be refactored when # this may be included in configuration assert test_view.display_sliders == False From 0578650f5f609ab153fa1499e5a9c49c9bb6fd06 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 27 Oct 2025 15:16:17 +0000 Subject: [PATCH 06/69] Re #164 Revert project widget to its initial state before adding multitab view option --- rascal2/widgets/project/project.py | 84 +++++++++--------------------- 1 file changed, 26 insertions(+), 58 deletions(-) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index 2b3dad9d..e93aa770 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -17,7 +17,6 @@ DomainContrastWidget, LayerFieldWidget, ParameterFieldWidget, - ParameterSliderWidget, ProjectFieldWidget, ResolutionsFieldWidget, ) @@ -57,14 +56,9 @@ def __init__(self, parent): } # track which tabs are lists (for syncing) self.list_tabs = ["Contrasts", "Data"] - # define project tabs which have slider views - self._tab_has_sliders = ["Parameters"] - - self._view_tabs = {} - self._edit_tabs = {} - self._slider_tabs = {} - # public tab_stacks property to wire to global tab selection - self._tab_stacks = {} + + self.view_tabs = {} + self.edit_tabs = {} self.draft_project = None # for making model type changes non-destructive self.old_contrast_models = {} @@ -141,31 +135,14 @@ def create_project_view(self) -> None: self.project_tab = QtWidgets.QTabWidget() for tab, fields in self.tabs.items(): - view_widget = self._view_tabs[tab] = ProjectTabWidget(fields, self,False,False) - if tab in self._tab_has_sliders: - slider_widget = self._slider_tabs[tab] = ProjectTabWidget(fields, self,False,True) - self._tab_stacks[tab] = QtWidgets.QStackedLayout() - self._tab_stacks[tab].addWidget(view_widget) - self._tab_stacks[tab].addWidget(slider_widget) - the_widget = QtWidgets.QWidget() - the_widget.setLayout(self._tab_stacks[tab]) - else: - the_widget = view_widget - self.project_tab.addTab(the_widget, tab) + widget = self.view_tabs[tab] = ProjectTabWidget(fields, self) + self.project_tab.addTab(widget, tab) main_layout.addWidget(self.project_tab) project_widget.setLayout(main_layout) return project_widget - def select_list_or_sliders_view(self,select_sliders: bool = False) -> None : - """Depending on select_sliders variable, display tab with sliders view or list view""" - for tab in self._tab_stacks.values(): - if select_sliders: - tab.setCurrentIndex(1) - else: - tab.setCurrentIndex(0) - def create_edit_view(self) -> None: """Creates the project edit view""" @@ -238,7 +215,7 @@ def create_edit_view(self) -> None: self.calculation_combobox.currentTextChanged.connect(lambda s: self.handle_model_update(s)) self.calculation_combobox.currentTextChanged.connect(lambda: self.handle_tabs()) self.calculation_combobox.currentTextChanged.connect( - lambda s: self._edit_tabs["Contrasts"].tables["contrasts"].set_domains(s == Calculations.Domains) + lambda s: self.edit_tabs["Contrasts"].tables["contrasts"].set_domains(s == Calculations.Domains) ) # when model type changed, hide/show layers tab and change model field in contrasts @@ -249,15 +226,15 @@ def create_edit_view(self) -> None: self.edit_project_tab = QtWidgets.QTabWidget() for tab, fields in self.tabs.items(): - widget = self._edit_tabs[tab] = ProjectTabWidget(fields, self, edit_mode=True) + widget = self.edit_tabs[tab] = ProjectTabWidget(fields, self, edit_mode=True) self.edit_project_tab.addTab(widget, tab) self.edit_absorption_checkbox.checkStateChanged.connect( - lambda s: self._edit_tabs["Layers"].tables["layers"].set_absorption(s == QtCore.Qt.CheckState.Checked) + lambda s: self.edit_tabs["Layers"].tables["layers"].set_absorption(s == QtCore.Qt.CheckState.Checked) ) for tab in ["Experimental Parameters", "Layers", "Backgrounds", "Resolutions", "Data", "Domains"]: - for table in self._edit_tabs[tab].tables.values(): - table.edited.connect(lambda: self._edit_tabs["Contrasts"].tables["contrasts"].update_item_view()) + for table in self.edit_tabs[tab].tables.values(): + table.edited.connect(lambda: self.edit_tabs["Contrasts"].tables["contrasts"].update_item_view()) main_layout.addWidget(self.edit_project_tab) @@ -270,7 +247,7 @@ def update_project_view(self, update_tab_index=None) -> None: if update_tab_index is None: update_tab_index = self.stacked_widget.currentIndex() - tab_to_update = self._view_tabs if update_tab_index == 0 else self._edit_tabs + tab_to_update = self.view_tabs if update_tab_index == 0 else self.edit_tabs tab_indices = {} for tab in self.list_tabs: model = tab_to_update[tab].tables[tab.lower()].list.selectionModel() @@ -281,8 +258,8 @@ def update_project_view(self, update_tab_index=None) -> None: self.draft_project: dict = create_draft_project(self.parent_model.project) for tab in self.tabs: - self._view_tabs[tab].update_model(self.draft_project) - self._edit_tabs[tab].update_model(self.draft_project) + self.view_tabs[tab].update_model(self.draft_project) + self.edit_tabs[tab].update_model(self.draft_project) self.absorption_checkbox.setChecked(self.parent_model.project.absorption) self.calculation_type.setText(self.parent_model.project.calculation) @@ -298,7 +275,7 @@ def update_project_view(self, update_tab_index=None) -> None: self.handle_controls_update() for tab in self.list_tabs: - tab_widget = self._view_tabs[tab].tables[tab.lower()] + tab_widget = self.view_tabs[tab].tables[tab.lower()] idx = tab_indices[tab] tab_widget.list.selectionModel().setCurrentIndex( tab_widget.model.index(idx, 0), QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect @@ -319,18 +296,18 @@ def update_draft_project(self, new_values: dict) -> None: def handle_tabs(self) -> None: """Displays or hides tabs as relevant.""" # the domains tab should only be visible if calculating domains - domain_tab_index = list(self._view_tabs).index("Domains") + domain_tab_index = list(self.view_tabs).index("Domains") is_domains = self.calculation_combobox.currentText() == Calculations.Domains self.project_tab.setTabVisible(domain_tab_index, is_domains) self.edit_project_tab.setTabVisible(domain_tab_index, is_domains) # the layers tab and domain contrasts table should only be visible in standard layers - layers_tab_index = list(self._view_tabs).index("Layers") + layers_tab_index = list(self.view_tabs).index("Layers") is_layers = self.model_combobox.currentText() == LayerModels.StandardLayers self.project_tab.setTabVisible(layers_tab_index, is_layers) self.edit_project_tab.setTabVisible(layers_tab_index, is_layers) - self._view_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) - self._edit_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) + self.view_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) + self.edit_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers) def handle_controls_update(self): """Handle updates to Controls that need to be reflected in the project.""" @@ -340,8 +317,8 @@ def handle_controls_update(self): controls = self.parent_model.controls for tab in self.tabs: - self._view_tabs[tab].handle_controls_update(controls) - self._edit_tabs[tab].handle_controls_update(controls) + self.view_tabs[tab].handle_controls_update(controls) + self.edit_tabs[tab].handle_controls_update(controls) def handle_model_update(self, new_entry): """Handle updates to the model type. @@ -373,7 +350,7 @@ def handle_model_update(self, new_entry): contrast.model = self.old_contrast_models.get(contrast.name, []) self.old_contrast_models = old_contrast_models - self._edit_tabs["Contrasts"].tables["contrasts"].update_item_view() + self.edit_tabs["Contrasts"].tables["contrasts"].update_item_view() def show_project_view(self) -> None: """Show project view""" @@ -520,7 +497,7 @@ def set_editing_enabled(self, enabled: bool): for tab_name, tab_items in self.tabs.items(): for table in tab_items: if table in ratapi.project.parameter_class_lists: - self._view_tabs[tab_name].tables[table].setEnabled(enabled) + self.view_tabs[tab_name].tables[table].setEnabled(enabled) class ProjectTabWidget(QtWidgets.QWidget): @@ -533,30 +510,21 @@ class ProjectTabWidget(QtWidgets.QWidget): fields : list[str] The fields to display in the tab. parent : QtWidgets.QWidget - ProjectWidget which holds multiple TabWidgets and controls their appearance. - edit_mode : bool - Build Tab widget in edit mode - slider_mode : bool - Build Tab widget in slider mode, i.e. generate slider instead of lists. - The mode is something intermediate between edit and view mode, as allows - changing some values but not adding new properties + The parent to this widget. + """ - def __init__(self, fields: list[str], parent, edit_mode: bool = False, slider_mode: bool = False): + def __init__(self, fields: list[str], parent, edit_mode: bool = False): super().__init__(parent) self.parent = parent self.fields = fields self.edit_mode = edit_mode self.tables = {} - layout = QtWidgets.QVBoxLayout() for field in self.fields: if field in ratapi.project.parameter_class_lists: - if slider_mode: - self.tables[field] = ParameterSliderWidget(field, self) - else: - self.tables[field] = ParameterFieldWidget(field, self) + self.tables[field] = ParameterFieldWidget(field, self) elif field == "backgrounds": self.tables[field] = BackgroundsFieldWidget(field, self) elif field == "resolutions": From e28bb5bbfeadccffa6a1df8c09cf095aab397ed2 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 27 Oct 2025 21:20:48 +0000 Subject: [PATCH 07/69] Re #164 stub for sliders_view. Just ugly class and couple of buttons --- rascal2/ui/view.py | 7 +- rascal2/widgets/__init__.py | 2 + rascal2/widgets/sliders_view.py | 110 ++++++++++++++++++++++++++++++++ tests/test_ui.py | 1 + 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 rascal2/widgets/sliders_view.py diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 69d4cadd..e97ee36e 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -9,7 +9,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, TerminalWidget +from rascal2.widgets import ControlsWidget, PlotWidget, TerminalWidget, SlidersViewWidget from rascal2.widgets.project import ProjectWidget from rascal2.widgets.startup import StartUpWidget @@ -44,6 +44,7 @@ def __init__(self): self.terminal_widget = TerminalWidget() self.controls_widget = ControlsWidget(self) self.project_widget = ProjectWidget(self) + self.sliders_view_widget = SlidersViewWidget(self) ## protected interface and public properties construction @@ -274,10 +275,12 @@ def show_or_hide_sliders(self): self.display_sliders = not self.display_sliders if self.display_sliders: self._show_or_hide_slider_action.setText(self._sliders_menu_control["ShowSliders"]) + self.sliders_view_widget.show() else: self._show_or_hide_slider_action.setText(self._sliders_menu_control["HideSliders"]) + self.sliders_view_widget.hide() + - self.project_widget.select_list_or_sliders_view(self.display_sliders) def open_about_info(self): diff --git a/rascal2/widgets/__init__.py b/rascal2/widgets/__init__.py index 2ed2d4ff..08f0134c 100644 --- a/rascal2/widgets/__init__.py +++ b/rascal2/widgets/__init__.py @@ -1,6 +1,7 @@ from rascal2.widgets.controls import ControlsWidget from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, MultiSelectComboBox, get_validated_input from rascal2.widgets.plot import PlotWidget +from rascal2.widgets.sliders_view import SlidersViewWidget from rascal2.widgets.terminal import TerminalWidget __all__ = [ @@ -10,4 +11,5 @@ "MultiSelectComboBox", "PlotWidget", "TerminalWidget", + "SlidersViewWidget" ] diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py new file mode 100644 index 00000000..c38cc673 --- /dev/null +++ b/rascal2/widgets/sliders_view.py @@ -0,0 +1,110 @@ +"""Widget for the Project window.""" + +from collections.abc import Generator +from copy import deepcopy + +import ratapi +from pydantic import ValidationError +from PyQt6 import QtCore, QtGui, QtWidgets +from ratapi.utils.custom_errors import custom_pydantic_validation_error +from ratapi.utils.enums import Calculations, Geometries, LayerModels + +from rascal2.widgets.project.lists import ContrastWidget, DataWidget +from rascal2.widgets.project.tables import ( + BackgroundsFieldWidget, + CustomFileWidget, + DomainContrastWidget, + LayerFieldWidget, + ParameterFieldWidget, + ProjectFieldWidget, + ResolutionsFieldWidget, +) + +class SlidersViewWidget(QtWidgets.QWidget): + """ + The sliders view Widget + """ + + def __init__(self, parent): + """ + Initialize widget. + + Parameters + ---------- + parent: MainWindowView + An instance of the MainWindowView + """ + super().__init__() + self._parent = parent + #self._parent_model = self.parent.presenter.model + + main_layout = QtWidgets.QVBoxLayout() + #main_layout.setSpacing(20) + + self._accept_button = QtWidgets.QPushButton("Accept", self, objectName="Accept") + self._accept_button.clicked.connect(self.save_changes) + self._cancel_button = QtWidgets.QPushButton("Cancel", self, objectName="Cancel") + self._cancel_button.clicked.connect(QtWidgets.QDialog.accept) + + button_layout = QtWidgets.QHBoxLayout() + button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + button_layout.addWidget(self._accept_button) + button_layout.addWidget(self._cancel_button) + + main_layout.addLayout(button_layout) + + + #self.parent_model.project_updated.connect(self.update_project_view) + #self.parent_model.controls_updated.connect(self.handle_controls_update) + + #project_view = self.create_project_view() + + #self.project_tab.currentChanged.connect(self.edit_project_tab.setCurrentIndex) + #self.edit_project_tab.currentChanged.connect(self.project_tab.setCurrentIndex) + + + self.setLayout(main_layout) + + def create_project_view(self) -> None: + + return + + def update_project_view(self, update_tab_index=None) -> None: + """Updates the project view.""" + + + def handle_controls_update(self): + """Handle updates to Controls that need to be reflected in the project.""" + + def handle_model_update(self, new_entry): + """Handle updates to the model type. + + Parameters + ---------- + new_entry : LayerModels | Calculations + The new layer model or calculation. + + """ + + def show_project_view(self) -> None: + """Show project view""" + + + def save_changes(self) -> None: + """Save changes to the project.""" + # sync list items (wrap around update_project_view() which sets them to zero by default) + # the list can lose focus when a contrast is edited... default to first item if this happens + errors = "\n ".join(self.validate_draft_project()) + if errors: + self.parent.terminal_widget.write_error(f"Could not save draft project:\n {errors}") + else: + # catch errors from Pydantic as fallback rather than crashing + try: + self.parent.presenter.edit_project(self.draft_project) + except ValidationError as err: + custom_error_list = custom_pydantic_validation_error(err.errors(include_url=False)) + custom_errors = ValidationError.from_exception_data(err.title, custom_error_list, hide_input=True) + self.parent.terminal_widget.write_error(f"Could not save draft project:\n {custom_errors}") + else: + self.show_project_view() + diff --git a/tests/test_ui.py b/tests/test_ui.py index 5c27f046..98399a13 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -44,3 +44,4 @@ def test_integration(qt_application, make_main_window): # Work through the different sections of the UI window.close() + From 4ff0e70dfd7637c6da72f07be175cf26d431e73c Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 28 Oct 2025 15:09:21 +0000 Subject: [PATCH 08/69] Re #164 Stub for sliders_view and its connection with view widget --- rascal2/ui/view.py | 37 ++++++----- rascal2/widgets/project/project.py | 4 +- rascal2/widgets/sliders_view.py | 94 +++++++++++++++++++++------- tests/ui/test_view.py | 21 ++++--- tests/widgets/test_sliders_widget.py | 10 +++ 5 files changed, 120 insertions(+), 46 deletions(-) create mode 100644 tests/widgets/test_sliders_widget.py diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index e97ee36e..2d84e43e 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -25,7 +25,7 @@ def __init__(self): super().__init__() #Public interface self.disabled_elements = [] - self.display_sliders = False + self.display_sliders = False # no one displays sliders initially self.setWindowTitle(MAIN_WINDOW_TITLE) @@ -49,9 +49,9 @@ def __init__(self): ## protected interface and public properties construction # define menu controlling switch between table and slider views - self._sliders_menu_control = { - "ShowSliders":"&Show Tables", # if state is show sliders, click will show table - "HideSliders":"&Show Sliders"} # if state is show table, click will show sliders + 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.create_actions() @@ -172,13 +172,16 @@ def create_actions(self): open_help_action.triggered.connect(self.open_docs) self.open_help_action = open_help_action - + # done this way expecting the value "display_sliders" being stored + # in configuration in a future. if self.display_sliders: - show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control["ShowSliders"], self) + # if display_sliders state is True, action will be hide + show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control_text["HideSliders"], self) else: - show_or_hide_slider_action = QtGui.QAction(self._sliders_menu_control["HideSliders"], self) + # 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(self.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 open_about_action = QtGui.QAction("&About", self) @@ -268,21 +271,27 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): help_menu.addAction(self.open_about_action) help_menu.addAction(self.open_help_action) - def show_or_hide_sliders(self): + def show_or_hide_sliders(self,do_display_sliders = None): """Depending on current state, show or hide sliders for table properties within Project class view. + + do_display_sliders -- if provided, sets self.display_sliders logical variable + into the requested state (True/False), forcing sliders + widget to appear/disappear """ - self.display_sliders = not self.display_sliders + if do_display_sliders is None: + self.display_sliders = not self.display_sliders + else: + self.display_sliders = do_display_sliders + if self.display_sliders: - self._show_or_hide_slider_action.setText(self._sliders_menu_control["ShowSliders"]) + self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["HideSliders"]) self.sliders_view_widget.show() else: - self._show_or_hide_slider_action.setText(self._sliders_menu_control["HideSliders"]) + self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) self.sliders_view_widget.hide() - - def open_about_info(self): """Opens about menu containing information about RASCAL gui""" self.about_dialog.update_rascal_info(self) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index e93aa770..3827c27f 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -77,7 +77,7 @@ def __init__(self, parent): layout.addWidget(self.stacked_widget) self.setLayout(layout) - def create_project_view(self) -> None: + def create_project_view(self) -> QtWidgets.QWidget: """Creates the project (non-edit) view""" project_widget = QtWidgets.QWidget() main_layout = QtWidgets.QVBoxLayout() @@ -143,7 +143,7 @@ def create_project_view(self) -> None: return project_widget - def create_edit_view(self) -> None: + def create_edit_view(self) -> QtWidgets.QWidget: """Creates the project edit view""" edit_project_widget = QtWidgets.QWidget() diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index c38cc673..1f6595ea 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -6,6 +6,7 @@ import ratapi from pydantic import ValidationError from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt from ratapi.utils.custom_errors import custom_pydantic_validation_error from ratapi.utils.enums import Calculations, Geometries, LayerModels @@ -36,22 +37,9 @@ def __init__(self, parent): """ super().__init__() self._parent = parent - #self._parent_model = self.parent.presenter.model - - main_layout = QtWidgets.QVBoxLayout() - #main_layout.setSpacing(20) - - self._accept_button = QtWidgets.QPushButton("Accept", self, objectName="Accept") - self._accept_button.clicked.connect(self.save_changes) - self._cancel_button = QtWidgets.QPushButton("Cancel", self, objectName="Cancel") - self._cancel_button.clicked.connect(QtWidgets.QDialog.accept) - button_layout = QtWidgets.QHBoxLayout() - button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) - button_layout.addWidget(self._accept_button) - button_layout.addWidget(self._cancel_button) - - main_layout.addLayout(button_layout) + self.create_sliders_layout() + #self._parent_model = self.parent.presenter.model #self.parent_model.project_updated.connect(self.update_project_view) @@ -63,8 +51,31 @@ def __init__(self, parent): #self.edit_project_tab.currentChanged.connect(self.project_tab.setCurrentIndex) + def create_sliders_layout(self) -> None: + """ Create sliders layout with all necessary controls and connections """ + + self.setWindowTitle("Slider view") + main_layout = QtWidgets.QVBoxLayout() + main_layout.setSpacing(20) + + accept_button = QtWidgets.QPushButton("Accept", self, objectName="AcceptButton") + accept_button.clicked.connect(self.save_changes) + 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) + + slider = LabeledSlider() + main_layout.addWidget(slider) + self.setLayout(main_layout) + def create_project_view(self) -> None: return @@ -86,25 +97,66 @@ def handle_model_update(self, new_entry): """ - def show_project_view(self) -> None: + def show_sliders_view(self) -> None: """Show project view""" + self._parent.show_or_hide_sliders(True) + def cancel_changes_from_sliders(self): + """Cancel changes to properties obtained from sliders + and hide sliders view. + """ + self._parent.show_or_hide_sliders(False) def save_changes(self) -> None: """Save changes to the project.""" # sync list items (wrap around update_project_view() which sets them to zero by default) # the list can lose focus when a contrast is edited... default to first item if this happens - errors = "\n ".join(self.validate_draft_project()) + #errors = "\n ".join(self.validate_draft_project()) + self._parent.show_or_hide_sliders(False) + return + errors = None if errors: - self.parent.terminal_widget.write_error(f"Could not save draft project:\n {errors}") + self._parent.terminal_widget.write_error(f"Could not save draft project:\n {errors}") else: # catch errors from Pydantic as fallback rather than crashing try: - self.parent.presenter.edit_project(self.draft_project) + self._parent.presenter.edit_project(self.draft_project) except ValidationError as err: custom_error_list = custom_pydantic_validation_error(err.errors(include_url=False)) custom_errors = ValidationError.from_exception_data(err.title, custom_error_list, hide_input=True) - self.parent.terminal_widget.write_error(f"Could not save draft project:\n {custom_errors}") + self._parent.terminal_widget.write_error(f"Could not save draft project:\n {custom_errors}") else: - self.show_project_view() + self._parent.show_or_hide_sliders(False) + + +class LabeledSlider(QtWidgets.QWidget): + def __init__(self, minimum=0, maximum=100, step=10, parent=None): + super().__init__(parent) + + self.slider = QtWidgets.QSlider(Qt.Orientation.Horizontal) + self.slider.setMinimum(minimum) + self.slider.setMaximum(maximum) + self.slider.setTickInterval(step) + self.slider.setSingleStep(step) + self.slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBelow) + self.slider.setValue((maximum + minimum) // 2) + + self.value_label = QtWidgets.QLabel(str(self.slider.value()), alignment=Qt.AlignmentFlag.AlignCenter) + + # layout for numeric scale below + self.scale_layout = QtWidgets.QHBoxLayout() + for i in range(minimum, maximum + 1, step): + label = QtWidgets.QLabel(str(i)) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.scale_layout.addWidget(label) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.slider) + layout.addLayout(self.scale_layout) + layout.addWidget(self.value_label) + + # signal to update label dynamically + self.slider.valueChanged.connect(self._update_value) + def _update_value(self, val): + self.value_label.setText(f"Value: {val}") diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index b75a3291..eb459bb0 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -171,8 +171,9 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay for action, name in zip(actions, action_names_and_layout, strict=True): assert action.text() == name -@patch("rascal2.ui.view.ProjectWidget.select_list_or_sliders_view") -def test_click_on_select_sliders_works_as_expected(mock_select_view,test_view): +@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): """Test if click on menu in the state "Show Slider" changes text appropriately and initiates correct callback """ @@ -187,14 +188,15 @@ def test_click_on_select_sliders_works_as_expected(mock_select_view,test_view): # Trigger the action all_actions[0].trigger() - assert all_actions[0].text() == "&Show Tables" + assert all_actions[0].text() == "&Hide Sliders" assert test_view.display_sliders == True - assert mock_select_view.call_count == 1 + assert mock_show.call_count == 1 -@patch("rascal2.ui.view.ProjectWidget.select_list_or_sliders_view") -def test_click_on_select_tabs_works_as_expected(mock_select_view,test_view): - """Test if click on menu in the state "Show Tabs" changes text appropriately - and initiates correct callback +@patch("rascal2.ui.view.SlidersViewWidget.show") +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view): + """Test if click on menu in the state "Show Sliders" changes text appropriately + and initiates correct callback """ # check initial state -- defined now but needs to be refactored when @@ -208,9 +210,10 @@ def test_click_on_select_tabs_works_as_expected(mock_select_view,test_view): # Trigger the action all_actions[0].trigger() assert test_view.display_sliders == True + assert mock_show.call_count == 1 # this would show sliders widget # check if next click returns to initial state all_actions[0].trigger() assert all_actions[0].text() == "&Show Sliders" assert test_view.display_sliders == False - assert mock_select_view.call_count == 2 # 2 as second click returned to initial state + assert mock_hide.call_count == 1 # this would hide sliders widget diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py new file mode 100644 index 00000000..869ad45e --- /dev/null +++ b/tests/widgets/test_sliders_widget.py @@ -0,0 +1,10 @@ +from unittest.mock import MagicMock, patch + +import pytest +import ratapi +from PyQt6 import QtWidgets + +from rascal2.widgets.sliders_view import ( + SlidersViewWidget, + LabeledSlider +) \ No newline at end of file From e4d26ec0b572174806b1eded04dd1c91b959517f Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 29 Oct 2025 17:10:34 +0000 Subject: [PATCH 09/69] Re #164 Made satisfactory layout for sliders view --- rascal2/ui/view.py | 76 ++++++++++++++++++++++----------- rascal2/widgets/sliders_view.py | 32 +++++++++++++- tests/test_ui.py | 2 +- tests/ui/test_view.py | 11 ++--- 4 files changed, 90 insertions(+), 31 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 2d84e43e..bbe6404e 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -25,7 +25,7 @@ def __init__(self): super().__init__() #Public interface self.disabled_elements = [] - self.display_sliders = False # no one displays sliders initially + self.show_sliders = False # no one displays sliders initially self.setWindowTitle(MAIN_WINDOW_TITLE) @@ -70,6 +70,14 @@ def __init__(self): self.setCentralWidget(self.startup_dlg) self.about_dialog = AboutDialog(self) + # dictionary of main widgets present in the main operation area + # There are other widgets (sliders_view) which are initially hidden. + self._main_window_widgets = { + "Plots": self.plot_widget, + "Project": self.project_widget, + "Terminal": self.terminal_widget, + "Fitting Controls": self.controls_widget + } def closeEvent(self, event): if self.presenter.ask_to_save_project(): @@ -174,8 +182,8 @@ def create_actions(self): # done this way expecting the value "display_sliders" being stored # in configuration in a future. - if self.display_sliders: - # if display_sliders state is True, action will be hide + 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 @@ -271,27 +279,30 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): help_menu.addAction(self.open_about_action) help_menu.addAction(self.open_help_action) - def show_or_hide_sliders(self,do_display_sliders = None): + 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. - do_display_sliders -- if provided, sets self.display_sliders logical variable + do_show_sliders -- if provided, sets self.show_sliders logical variable into the requested state (True/False), forcing sliders widget to appear/disappear """ - if do_display_sliders is None: - self.display_sliders = not self.display_sliders + if do_show_sliders is None: + self.show_sliders = not self.show_sliders else: - self.display_sliders = do_display_sliders + self.show_sliders = do_show_sliders - if self.display_sliders: + # ignore show/hide operations if project and its mdi container are not defined + if self.sliders_view_widget.mdi_holder is None: + return + + if self.show_sliders: self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["HideSliders"]) self.sliders_view_widget.show() else: self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) self.sliders_view_widget.hide() - def open_about_info(self): """Opens about menu containing information about RASCAL gui""" self.about_dialog.update_rascal_info(self) @@ -331,43 +342,61 @@ def setup_mdi(self): """Creates the multi-document interface""" # if windows are already created, don't set them up again, # just refresh the widget data - if len(self.mdi.subWindowList()) == 4: - self.setup_mdi_widgets() + if len(self.mdi.subWindowList()) == 5: + self.init_mdi_widgets() return - widgets = { - "Plots": self.plot_widget, - "Project": self.project_widget, - "Terminal": self.terminal_widget, - "Fitting Controls": self.controls_widget, - } - self.setup_mdi_widgets() + self.init_mdi_widgets() - for title, widget in reversed(widgets.items()): + for title, widget in reversed(self._main_window_widgets.items()): widget.setWindowTitle(title) window = self.mdi.addSubWindow( widget, QtCore.Qt.WindowType.WindowMinMaxButtonsHint | QtCore.Qt.WindowType.WindowTitleHint ) window.setWindowTitle(title) + # Add sliders view widget separately, as it will behave and is controlled differently from other + # mdi windows + self.mdi.addSubWindow( + self.sliders_view_widget, QtCore.Qt.WindowType.WindowMinMaxButtonsHint | QtCore.Qt.WindowType.WindowTitleHint + ) + self.sliders_view_widget.setWindowTitle("Sliders View") + self.reset_mdi_layout() self.startup_dlg = self.takeCentralWidget() self.setCentralWidget(self.mdi) - def setup_mdi_widgets(self): - """Performs setup of MDI widgets that relies on the Project existing.""" + def init_mdi_widgets(self): + """Performs initialization of MDI widgets that relies on the Project existing.""" self.controls_widget.setup_controls() self.project_widget.show_project_view() self.plot_widget.clear() self.terminal_widget.clear() self.terminal_widget.write_startup() + self.sliders_view_widget.init() def reset_mdi_layout(self): """Reset MDI layout to the default.""" + + main_widget_names = self._main_window_widgets.keys() if self.settings.mdi_defaults is None: + # logic expects "Sliders View" the only widget in the mdi list + slider_view_wrapper = None for window in self.mdi.subWindowList(): - window.showNormal() + if window.windowTitle() in main_widget_names: + window.showNormal() + else: + window.hide() + slider_view_wrapper = window self.mdi.tileSubWindows() + self.sliders_view_widget.mdi_holder = slider_view_wrapper else: + # reliability check. Can we have project saved previously with mdi defaults + # and initialized here newer entering the "if mdi_defaults" loop above? + if self.sliders_view_widget.mdi_holder is None: + for window in self.mdi.subWindowList(): + if window.windowTitle() == "Sliders View": + self.sliders_view_widget.mdi_holder = window + for window in self.mdi.subWindowList(): # get corresponding MDIGeometries entry for the widget widget_name = window.windowTitle().lower().split(" ")[-1] @@ -376,7 +405,6 @@ def reset_mdi_layout(self): window.showMinimized() else: window.showNormal() - window.setGeometry(x, y, width, height) def save_mdi_layout(self): diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 1f6595ea..3c5846b7 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -36,6 +36,10 @@ def __init__(self, parent): An instance of the MainWindowView """ super().__init__() + self.mdi_holder = None # the variable contains reference to mdi container holding this window + self._view_geometry = None # holder for slider view geometry, created to store slider view location + # 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 self.create_sliders_layout() @@ -50,11 +54,32 @@ def __init__(self, parent): #self.project_tab.currentChanged.connect(self.edit_project_tab.setCurrentIndex) #self.edit_project_tab.currentChanged.connect(self.project_tab.setCurrentIndex) + def show(self): + """Overload parent show method to deal with mdi container""" + if self.mdi_holder is None: + self._view_geometry = None + super().show() + else: + if self._view_geometry is None: # inherit geometry from project view + for window in self._parent.mdi.subWindowList(): + if window.windowTitle() == "Project": + self._view_geometry = window.geometry() + self.mdi_holder.setGeometry(self._view_geometry) + self.mdi_holder.show() + + def hide(self): + """Overload parent hide method to deal with mdi container""" + if self.mdi_holder is None: + super().hide() + else: + # store sliders geometry which may be user changed for the following view + self._view_geometry = self.mdi_holder.geometry() + self.mdi_holder.hide() + def create_sliders_layout(self) -> None: """ Create sliders layout with all necessary controls and connections """ - self.setWindowTitle("Slider view") main_layout = QtWidgets.QVBoxLayout() main_layout.setSpacing(20) @@ -75,6 +100,11 @@ def create_sliders_layout(self) -> None: self.setLayout(main_layout) + def init(self) -> None: + """Initialize state, position and construction of the sliders widget + i.e. set up everything except its show/hide state + """ + def create_project_view(self) -> None: diff --git a/tests/test_ui.py b/tests/test_ui.py index 98399a13..bfd9a491 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -39,7 +39,7 @@ def test_integration(qt_application, make_main_window): window.presenter.create_project("project", ".") names = [win.windowTitle() for win in window.mdi.subWindowList()] # QMDIArea is first in last out hence the reversed list - assert names == ["Fitting Controls", "Terminal", "Project", "Plots"] + assert names == ["Fitting Controls", "Terminal", "Project", "Plots","Sliders View"] # Work through the different sections of the UI diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index eb459bb0..2f62375f 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -171,6 +171,7 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay for action, name in zip(actions, action_names_and_layout, strict=True): assert action.text() == name + @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): @@ -180,7 +181,7 @@ def test_click_on_select_sliders_works_as_expected(mock_hide,mock_show,test_view # check initial state -- defined now but needs to be refactored when # this may be included in configuration - assert test_view.display_sliders == False + assert test_view.show_sliders == False main_menu = test_view.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") @@ -189,7 +190,7 @@ def test_click_on_select_sliders_works_as_expected(mock_hide,mock_show,test_view # Trigger the action all_actions[0].trigger() assert all_actions[0].text() == "&Hide Sliders" - assert test_view.display_sliders == True + assert test_view.show_sliders == True assert mock_show.call_count == 1 @patch("rascal2.ui.view.SlidersViewWidget.show") @@ -201,7 +202,7 @@ def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view): # check initial state -- defined now but needs to be refactored when # this may be included in configuration - assert test_view.display_sliders == False + assert test_view.show_sliders == False main_menu = test_view.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") @@ -209,11 +210,11 @@ def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view): # Trigger the action all_actions[0].trigger() - assert test_view.display_sliders == True + assert test_view.show_sliders == True assert mock_show.call_count == 1 # this would show sliders widget # check if next click returns to initial state all_actions[0].trigger() assert all_actions[0].text() == "&Show Sliders" - assert test_view.display_sliders == False + assert test_view.show_sliders == False assert mock_hide.call_count == 1 # this would hide sliders widget From a1431917a6b785f63c126a167b853dbc92cbf869 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 29 Oct 2025 18:56:56 +0000 Subject: [PATCH 10/69] Re #164 Fixed tests for view/hide sliders --- tests/ui/test_view.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 2f62375f..4e86e0b5 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -172,49 +172,62 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay 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 + """ + with patch("rascal2.widgets.plot.FigureCanvas", return_value=MockFigureCanvas()): + mw = MainWindowView() + mw.mdi.addSubWindow(mw.sliders_view_widget) + mdi_windows = mw.mdi.subWindowList() + mw.sliders_view_widget.mdi_holder = mdi_windows[0] + yield 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): +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 """ # check initial state -- defined now but needs to be refactored when # this may be included in configuration - assert test_view.show_sliders == False + assert test_view_with_mdi.show_sliders == False - main_menu = test_view.menuBar() + 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.show_sliders == True + assert test_view_with_mdi.show_sliders == True assert mock_show.call_count == 1 @patch("rascal2.ui.view.SlidersViewWidget.show") @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view): +def test_click_on_select_tabs_works_as_expected(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 """ # check initial state -- defined now but needs to be refactored when # this may be included in configuration - assert test_view.show_sliders == False + assert test_view_with_mdi.show_sliders == False - main_menu = test_view.menuBar() + 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.show_sliders == True + assert test_view_with_mdi.show_sliders == True assert mock_show.call_count == 1 # this would show sliders widget # check if next click returns to initial state all_actions[0].trigger() assert all_actions[0].text() == "&Show Sliders" - assert test_view.show_sliders == False + assert test_view_with_mdi.show_sliders == False assert mock_hide.call_count == 1 # this would hide sliders widget From d5da3d910263fc5f3cbeec2265d2fb349e346ffb Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 29 Oct 2025 18:58:18 +0000 Subject: [PATCH 11/69] Re #164 added view sliders to exclusions list until project is loaded --- rascal2/ui/view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index bbe6404e..9df47bcc 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -70,7 +70,8 @@ def __init__(self): self.setCentralWidget(self.startup_dlg) self.about_dialog = AboutDialog(self) - # dictionary of main widgets present in the main operation area + # dictionary of main widgets present in the main operation area controlled + # by mdi interface. # There are other widgets (sliders_view) which are initially hidden. self._main_window_widgets = { "Plots": self.plot_widget, @@ -191,6 +192,8 @@ def create_actions(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") From 3777b79b91e96161f34ac635f04e6a09fa79d53d Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 30 Oct 2025 14:49:01 +0000 Subject: [PATCH 12/69] Re #164 fixed test_view and modified Settings including Slivers View widget --- rascal2/settings.py | 10 +++++----- rascal2/ui/view.py | 4 ++-- tests/ui/test_view.py | 11 ++++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/rascal2/settings.py b/rascal2/settings.py index 0257dead..bbb28166 100644 --- a/rascal2/settings.py +++ b/rascal2/settings.py @@ -88,11 +88,11 @@ def _missing_(cls, value): class MDIGeometries(BaseModel): """Model for storing window positions and sizes.""" - - plots: WindowGeometry = Field(max_length=5, min_length=5) - project: WindowGeometry = Field(max_length=5, min_length=5) - terminal: WindowGeometry = Field(max_length=5, min_length=5) - controls: WindowGeometry = Field(max_length=5, min_length=5) + Plots: WindowGeometry = Field(max_length=5, min_length=5) + Project: WindowGeometry = Field(max_length=5, min_length=5) + Terminal: WindowGeometry = Field(max_length=5, min_length=5) + FittingControls: WindowGeometry = Field(max_length=5, min_length=5) + SlidersView: WindowGeometry = Field(max_length=5, min_length=5) class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True): diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 9df47bcc..294b9a3a 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -402,7 +402,7 @@ def reset_mdi_layout(self): for window in self.mdi.subWindowList(): # get corresponding MDIGeometries entry for the widget - widget_name = window.windowTitle().lower().split(" ")[-1] + widget_name = window.windowTitle().replace(" ", "") x, y, width, height, minimized = getattr(self.settings.mdi_defaults, widget_name) if minimized: window.showMinimized() @@ -415,7 +415,7 @@ def save_mdi_layout(self): geoms = {} for window in self.mdi.subWindowList(): # get corresponding MDIGeometries entry for the widget - widget_name = window.windowTitle().lower().split(" ")[-1] + widget_name = window.windowTitle().replace(" ","") geom = window.geometry() geoms[widget_name] = (geom.x(), geom.y(), geom.width(), geom.height(), window.isMinimized()) diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 4e86e0b5..52bcfccd 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -28,8 +28,8 @@ def test_view(): @pytest.mark.parametrize( "geometry", [ - ((1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True)), - ((1, 2, 196, 24, True), (3, 78, 196, 24, True), (1, 2, 204, 66, False), (12, 342, 196, 24, True)), + ((1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True)), + ((1, 2, 196, 24, True), (3, 78, 196, 24, True), (1, 2, 204, 66, False), (12, 342, 196, 24, True), (5, 6, 200, 28, True)), ], ) @patch("rascal2.ui.view.ProjectWidget.show_project_view") @@ -41,12 +41,12 @@ def test_reset_mdi(self, mock1, mock2, mock3, test_view, geometry): test_view.settings = Settings() test_view.setup_mdi() test_view.settings.mdi_defaults = MDIGeometries( - plots=geometry[0], project=geometry[1], terminal=geometry[2], controls=geometry[3] + Plots=geometry[0], Project=geometry[1], Terminal=geometry[2], FittingControls=geometry[3], SlidersView=geometry[4] ) test_view.reset_mdi_layout() for window in test_view.mdi.subWindowList(): # get corresponding MDIGeometries entry for the widget - widget_name = window.windowTitle().lower().split(" ")[-1] + widget_name = window.windowTitle().replace(" ", "") w_geom = window.geometry() assert getattr(test_view.settings.mdi_defaults, widget_name) == ( w_geom.x(), @@ -63,7 +63,7 @@ def test_set_mdi(self, mock1, mock2, mock3, test_view, geometry): widgets_in_order = [] for i, window in enumerate(test_view.mdi.subWindowList()): - widgets_in_order.append(window.windowTitle().lower().split(" ")[-1]) + widgets_in_order.append(window.windowTitle().replace(" ", "")) window.setGeometry(*geometry[i][0:4]) if geometry[i][4] is True: window.showMinimized() @@ -182,6 +182,7 @@ def test_view_with_mdi(): mw.mdi.addSubWindow(mw.sliders_view_widget) mdi_windows = mw.mdi.subWindowList() mw.sliders_view_widget.mdi_holder = mdi_windows[0] + mw.enable_elements() yield mw From d3597ee31b173b466c8d3b03822fff6c007c026d Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 31 Oct 2025 21:15:57 +0000 Subject: [PATCH 13/69] Re #148 sliders which work with properties. Not yet connected to sliders_view correctly --- rascal2/widgets/sliders_view.py | 90 +++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 3c5846b7..b8bb429e 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -1,9 +1,10 @@ -"""Widget for the Project window.""" +"""Widget for the Sliders View window.""" from collections.abc import Generator from copy import deepcopy import ratapi +import ratapi.models from pydantic import ValidationError from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt @@ -81,7 +82,7 @@ def create_sliders_layout(self) -> None: """ Create sliders layout with all necessary controls and connections """ main_layout = QtWidgets.QVBoxLayout() - main_layout.setSpacing(20) + #main_layout.setSpacing(20) accept_button = QtWidgets.QPushButton("Accept", self, objectName="AcceptButton") accept_button.clicked.connect(self.save_changes) @@ -95,8 +96,9 @@ def create_sliders_layout(self) -> None: main_layout.addLayout(button_layout) - slider = LabeledSlider() - main_layout.addWidget(slider) + self._create_project_view(main_layout) + #slider = LabeledSlider() + #main_layout.addWidget(slider) self.setLayout(main_layout) @@ -106,9 +108,15 @@ def init(self) -> None: """ - def create_project_view(self) -> None: + def _create_project_view(self,main_layout) -> None: + prop_dictionary = self._parent.project_widget.create_draft_project() + for obj in prop_dictionary.values(): + if isinstance(obj, ratapi.ClassList): + for prop in obj: + if prop.fittable: + slider = LabeledSlider(prop) + main_layout.addWidget(slider) - return def update_project_view(self, update_tab_index=None) -> None: """Updates the project view.""" @@ -160,33 +168,63 @@ def save_changes(self) -> None: class LabeledSlider(QtWidgets.QWidget): - def __init__(self, minimum=0, maximum=100, step=10, parent=None): + def __init__(self, param: ratapi.models.Parameter, parent=None): super().__init__(parent) + self._prop = param # hold the property controlled by slider + + # Properties of slider widget: + self._slider_min_idx = 0 + self._slider_max_idx = 100 # defines accuracy of slider motion + self._ticks_step = 10 # sliders ticks - self.slider = QtWidgets.QSlider(Qt.Orientation.Horizontal) - self.slider.setMinimum(minimum) - self.slider.setMaximum(maximum) - self.slider.setTickInterval(step) - self.slider.setSingleStep(step) - self.slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBelow) - self.slider.setValue((maximum + minimum) // 2) + # Characteristics of the property value to display + self._value_min = self._prop.min + self._value_range = (self._prop.max-self._value_min) + # the change in property value per single step slider move + self._value_step = self._value_range/self._slider_max_idx - self.value_label = QtWidgets.QLabel(str(self.slider.value()), alignment=Qt.AlignmentFlag.AlignCenter) + # Build all sliders widget and arrange them as expected + self._slider = self._build_slider(param.value) + + name_label = QtWidgets.QLabel(param.name, alignment=QtCore.Qt.AlignmentFlag.AlignLeft) + self._value_label = QtWidgets.QLabel(str(f"{param.value:.3g}"), 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 - self.scale_layout = QtWidgets.QHBoxLayout() - for i in range(minimum, maximum + 1, step): - label = QtWidgets.QLabel(str(i)) - label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.scale_layout.addWidget(label) + scale_layout = QtWidgets.QHBoxLayout() + for idx in range(self._slider_min_idx, self._slider_max_idx, self._ticks_step): + tick_value = self._slider_pos_to_value(idx) + label = QtWidgets.QLabel(str(f"{tick_value:.2g}")) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + scale_layout.addWidget(label) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(self.slider) - layout.addLayout(self.scale_layout) - layout.addWidget(self.value_label) + layout.addLayout(lab_layout) + layout.addWidget(self._slider) + layout.addLayout(scale_layout) # signal to update label dynamically - self.slider.valueChanged.connect(self._update_value) + self._slider.valueChanged.connect(self._update_value) + + def _value_to_slider_pos(self, value: float) -> int: + """Convert double property value into slider position""" + 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 double property value into slider position""" + return self._value_min + index*self._value_step + + def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: + """Construct slider widget with integer scales""" + + slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + slider.setMinimum(self._slider_min_idx) + 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)) - def _update_value(self, val): - self.value_label.setText(f"Value: {val}") + return slider From 4e5504e0c24eebf9c48cb56b5024e2ff58411bd0 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 3 Nov 2025 13:49:42 +0000 Subject: [PATCH 14/69] Re #148 sliders_view widget populated with sliders --- rascal2/widgets/sliders_view.py | 39 +++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index b8bb429e..38f53044 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -11,6 +11,9 @@ from ratapi.utils.custom_errors import custom_pydantic_validation_error from ratapi.utils.enums import Calculations, Geometries, LayerModels + +from rascal2.widgets.project.project import create_draft_project + from rascal2.widgets.project.lists import ContrastWidget, DataWidget from rascal2.widgets.project.tables import ( BackgroundsFieldWidget, @@ -43,7 +46,7 @@ def __init__(self, parent): # inherits project geometry on the first view. self._parent = parent - self.create_sliders_layout() + self._create_slider_view_layout() #self._parent_model = self.parent.presenter.model @@ -61,10 +64,13 @@ def show(self): self._view_geometry = None super().show() else: + main_layout = self.layout() + self._create_sliders_view(main_layout) if self._view_geometry is None: # inherit geometry from project view for window in self._parent.mdi.subWindowList(): if window.windowTitle() == "Project": self._view_geometry = window.geometry() + self.mdi_holder.setGeometry(self._view_geometry) self.mdi_holder.show() @@ -78,7 +84,7 @@ def hide(self): self.mdi_holder.hide() - def create_sliders_layout(self) -> None: + def _create_slider_view_layout(self) -> None: """ Create sliders layout with all necessary controls and connections """ main_layout = QtWidgets.QVBoxLayout() @@ -96,10 +102,6 @@ def create_sliders_layout(self) -> None: main_layout.addLayout(button_layout) - self._create_project_view(main_layout) - #slider = LabeledSlider() - #main_layout.addWidget(slider) - self.setLayout(main_layout) def init(self) -> None: @@ -108,14 +110,24 @@ def init(self) -> None: """ - def _create_project_view(self,main_layout) -> None: - prop_dictionary = self._parent.project_widget.create_draft_project() + def _create_sliders_view(self,main_layout) -> None: + + 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) + + project = self._parent.presenter.model.project + prop_dictionary = create_draft_project(project) for obj in prop_dictionary.values(): if isinstance(obj, ratapi.ClassList): for prop in obj: - if prop.fittable: + if hasattr(prop,"fit") and prop.fit: slider = LabeledSlider(prop) - main_layout.addWidget(slider) + content_layout.addWidget(slider) def update_project_view(self, update_tab_index=None) -> None: @@ -166,7 +178,7 @@ def save_changes(self) -> None: else: self._parent.show_or_hide_sliders(False) - +#======================================================================================================================= class LabeledSlider(QtWidgets.QWidget): def __init__(self, param: ratapi.models.Parameter, parent=None): super().__init__(parent) @@ -228,3 +240,8 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: slider.setValue(self._value_to_slider_pos(initial_value)) return slider + + def _update_value(self, idx: int)->None: + val = self._slider_pos_to_value(idx) + self._value_label.setText(str(f"{val:.3g}")) + self._prop.value = val From bdf4c55a1373e529304bb3572f4a547f8bc92480 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 3 Nov 2025 20:44:13 +0000 Subject: [PATCH 15/69] Re #148 Acceptable add_sliders method --- rascal2/widgets/sliders_view.py | 180 ++++++++++++++++++++++---------- 1 file changed, 122 insertions(+), 58 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 38f53044..550adc08 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -12,22 +12,10 @@ from ratapi.utils.enums import Calculations, Geometries, LayerModels -from rascal2.widgets.project.project import create_draft_project - -from rascal2.widgets.project.lists import ContrastWidget, DataWidget -from rascal2.widgets.project.tables import ( - BackgroundsFieldWidget, - CustomFileWidget, - DomainContrastWidget, - LayerFieldWidget, - ParameterFieldWidget, - ProjectFieldWidget, - ResolutionsFieldWidget, -) - class SlidersViewWidget(QtWidgets.QWidget): """ - The sliders view Widget + 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): @@ -40,33 +28,31 @@ def __init__(self, parent): An instance of the MainWindowView """ super().__init__() - self.mdi_holder = None # the variable contains reference to mdi container holding this window - self._view_geometry = None # holder for slider view geometry, created to store slider view location + self.mdi_holder = None # the variable contains reference to mdi container holding this widget + self._view_geometry = None # holder for slider view geometry, created to store slider view location # 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 - - self._create_slider_view_layout() - #self._parent_model = self.parent.presenter.model + self._parent = parent # reference to main view widget which holds sliders view + self._prop_to_revert ={} # dictionary of original properties with fit parameter "true" but have to be restored + # back into original project if cancel button is pressed + self._prop_to_change = {} # dictionary of references to 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.parent_model.project_updated.connect(self.update_project_view) - #self.parent_model.controls_updated.connect(self.handle_controls_update) + self._sliders = {} # dictionary of the sliders used to display fittable values + # create initial slider view layout and everything else which depends on it + self.init() - #project_view = self.create_project_view() - - #self.project_tab.currentChanged.connect(self.edit_project_tab.setCurrentIndex) - #self.edit_project_tab.currentChanged.connect(self.project_tab.setCurrentIndex) def show(self): """Overload parent show method to deal with mdi container""" + self.init() if self.mdi_holder is None: self._view_geometry = None super().show() else: - main_layout = self.layout() - self._create_sliders_view(main_layout) - if self._view_geometry is None: # inherit geometry from project view + if self._view_geometry is None: + # inherit geometry from project view for window in self._parent.mdi.subWindowList(): if window.windowTitle() == "Project": self._view_geometry = window.geometry() @@ -83,9 +69,63 @@ def hide(self): self._view_geometry = self.mdi_holder.geometry() self.mdi_holder.hide() + def init(self) -> None: + """Initialize common state of the sliders widget + extract properties, used to build sliders and generate + list of sliders widgets to control these properties + """ + if self.findChild(QtWidgets.QWidget,'AcceptButton') is None: + self._create_slider_view_layout() + + proj = self._parent.presenter.model.project + update_sliders = self._init_properties_for_sliders(proj) + if update_sliders: + self._add_sliders_widgets() # call update sliders widget + else: + self._add_sliders_widgets() + + + def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: + """Take project and copy all properties which have attribute "Fit" == True + into dictionary used to build sliders for them. Also set back-up properties + dictionary used to reset properties back to their default values if "Cancel" + button is pressed. + + Input: + ------ + project: ratapi.Project -- project to get properties to change + + Returns: + -------- + update_properties -- true if all properties in the project have already + had sliders, generated for them so we may update existing widgets instead of generating + new ones. + """ + if project is None: + return False + + trial_properties = {} + n_existing_properties = 0 + for field_name in ratapi.Project.model_fields: + attr = getattr(project, field_name) + if isinstance(attr, ratapi.ClassList): + param_list = attr.data + for prop in param_list: + if isinstance(prop,ratapi.models.Parameter) and prop.fit: + trial_properties[prop.name] = prop + if prop.name in self._prop_to_change: + n_existing_properties += 1 + + update_properties = n_existing_properties == len(trial_properties) # if all properties of trial dictionary + # are in existing dictionary, we will update widgets instead of adding the new one + self._prop_to_change = trial_properties # References to project properties + self._prop_to_revert = deepcopy(self._prop_to_change) # Copy of initial values of project properties + return update_properties def _create_slider_view_layout(self) -> None: - """ Create sliders layout with all necessary controls and connections """ + """ Create sliders layout with all necessary controls and connections + but without sliders themselves. + """ main_layout = QtWidgets.QVBoxLayout() #main_layout.setSpacing(20) @@ -104,34 +144,47 @@ def _create_slider_view_layout(self) -> None: self.setLayout(main_layout) - def init(self) -> None: - """Initialize state, position and construction of the sliders widget - i.e. set up everything except its show/hide state - """ - - - def _create_sliders_view(self,main_layout) -> None: - - 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) - - project = self._parent.presenter.model.project - prop_dictionary = create_draft_project(project) - for obj in prop_dictionary.values(): - if isinstance(obj, ratapi.ClassList): - for prop in obj: - if hasattr(prop,"fit") and prop.fit: - slider = LabeledSlider(prop) - content_layout.addWidget(slider) + 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 + """ + scroll = self.findChild(QtWidgets.QScrollArea, "Scroll") + if scroll is None: + main_layout = self.layout() + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) # important: resize content to fit area + scroll.setObjectName("Scroll") + main_layout.addWidget(scroll) + content = QtWidgets.QWidget() + content.setObjectName("Scroll_content") + scroll.setWidget(content) + # --- Add content layout + content_layout = QtWidgets.QVBoxLayout(content) + else: + content = scroll.findChild(QtWidgets.QWidget, "Scroll_content") + content_layout = content.layout() + + # We are adding new sliders, so delete all previous ones + for slider in self._sliders.values(): + slider.deleteLater() + self._sliders = {} + + if len(self._prop_to_change) == 0: + no_label = QtWidgets.QLabel("No properties to fit, Nothing to view here") + no_label.setObjectName("No_properties_to_fit") + content_layout.addWidget(no_label) + self._sliders['No_properties_to_fit_label'] = no_label + else: + for prop in self._prop_to_change.values(): + slider = LabeledSlider(prop) + self._sliders[prop.name] = slider + content_layout.addWidget(slider) - def update_project_view(self, update_tab_index=None) -> None: - """Updates the project view.""" + def _update_sliders_widgets(self, update_tab_index=None) -> None: + """Updates the sliders given the project properties to fit are the same + but their values may be upgraded + """ def handle_controls_update(self): @@ -155,6 +208,12 @@ def cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders and hide sliders view. """ + + # as here our properties to change refer directly to project properties + # we modify their values directly + for key,prop in self._prop_to_revert.items(): + self._prop_to_change[key].value = prop.value + self._parent.show_or_hide_sliders(False) def save_changes(self) -> None: @@ -179,10 +238,11 @@ def save_changes(self) -> None: self._parent.show_or_hide_sliders(False) #======================================================================================================================= -class LabeledSlider(QtWidgets.QWidget): +class LabeledSlider(QtWidgets.QFrame): def __init__(self, param: ratapi.models.Parameter, parent=None): super().__init__(parent) self._prop = param # hold the property controlled by slider + self.slider_name = param.name # name the slider as the property it refers to # Properties of slider widget: self._slider_min_idx = 0 @@ -198,7 +258,7 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): # Build all sliders widget and arrange them as expected self._slider = self._build_slider(param.value) - name_label = QtWidgets.QLabel(param.name, alignment=QtCore.Qt.AlignmentFlag.AlignLeft) + name_label = QtWidgets.QLabel(self.slider_name, alignment=QtCore.Qt.AlignmentFlag.AlignLeft) self._value_label = QtWidgets.QLabel(str(f"{param.value:.3g}"), alignment=QtCore.Qt.AlignmentFlag.AlignRight) lab_layout = QtWidgets.QHBoxLayout() lab_layout.addWidget(name_label) @@ -220,6 +280,10 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): # signal to update label dynamically self._slider.valueChanged.connect(self._update_value) + self.setObjectName(self.slider_name) + self.setFrameShape(QtWidgets.QFrame.Shape.Box) + self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + def _value_to_slider_pos(self, value: float) -> int: """Convert double property value into slider position""" return int(round(self._slider_max_idx*(value-self._value_min)/self._value_range,0)) From 3dcb096e824986c6695bbf459fd8d143cc861ce7 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 4 Nov 2025 09:45:41 +0000 Subject: [PATCH 16/69] Re #148 Sliders work, change project's properties, on calculate graphs change and accept/cancel change in properties --- rascal2/widgets/sliders_view.py | 115 +++++++++++++++----------------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 550adc08..d5c99048 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -1,15 +1,10 @@ """Widget for the Sliders View window.""" -from collections.abc import Generator from copy import deepcopy import ratapi import ratapi.models -from pydantic import ValidationError -from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import Qt -from ratapi.utils.custom_errors import custom_pydantic_validation_error -from ratapi.utils.enums import Calculations, Geometries, LayerModels +from PyQt6 import QtCore,QtWidgets class SlidersViewWidget(QtWidgets.QWidget): @@ -45,7 +40,11 @@ def __init__(self, parent): def show(self): - """Overload parent show method to deal with mdi container""" + """Overload parent show method to deal with mdi container + showing sliders widget window. Also sets up or updates sliders + widget list depending on previous state of the widget + """ + self.init() if self.mdi_holder is None: self._view_geometry = None @@ -61,7 +60,9 @@ def show(self): self.mdi_holder.show() def hide(self): - """Overload parent hide method to deal with mdi container""" + """Overload parent hide method to deal with mdi container + hiding slider widgets window + """ if self.mdi_holder is None: super().hide() else: @@ -70,9 +71,11 @@ def hide(self): self.mdi_holder.hide() def init(self) -> None: - """Initialize common state of the sliders widget - extract properties, used to build sliders and generate - list of sliders widgets to control these properties + """The main Widget window is ready so this method initializes + general contents (buttons) of the sliders widget. + If project is defined it extracts properties, used to build + sliders and generates list of sliders widgets to + control the properties. """ if self.findChild(QtWidgets.QWidget,'AcceptButton') is None: self._create_slider_view_layout() @@ -80,11 +83,10 @@ def init(self) -> None: proj = self._parent.presenter.model.project update_sliders = self._init_properties_for_sliders(proj) if update_sliders: - self._add_sliders_widgets() # call update sliders widget + self._update_sliders_widgets() else: self._add_sliders_widgets() - def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: """Take project and copy all properties which have attribute "Fit" == True into dictionary used to build sliders for them. Also set back-up properties @@ -131,9 +133,9 @@ def _create_slider_view_layout(self) -> None: #main_layout.setSpacing(20) accept_button = QtWidgets.QPushButton("Accept", self, objectName="AcceptButton") - accept_button.clicked.connect(self.save_changes) + accept_button.clicked.connect(self._apply_changes_from_sliders) cancel_button = QtWidgets.QPushButton("Cancel", self, objectName="CancelButton") - cancel_button.clicked.connect(self.cancel_changes_from_sliders) + cancel_button.clicked.connect(self._cancel_changes_from_sliders) button_layout = QtWidgets.QHBoxLayout() button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) @@ -181,34 +183,21 @@ def _add_sliders_widgets(self) -> None: self._sliders[prop.name] = slider content_layout.addWidget(slider) - def _update_sliders_widgets(self, update_tab_index=None) -> None: + def _update_sliders_widgets(self) -> None: """Updates the sliders given the project properties to fit are the same but their values may be upgraded """ - - - def handle_controls_update(self): - """Handle updates to Controls that need to be reflected in the project.""" - - def handle_model_update(self, new_entry): - """Handle updates to the model type. - - Parameters - ---------- - new_entry : LayerModels | Calculations - The new layer model or calculation. - - """ + for name,slider in self._sliders.items(): + self._sliders[name].update_slider_parameters(self._prop_to_change[name]) def show_sliders_view(self) -> None: """Show project view""" self._parent.show_or_hide_sliders(True) - def cancel_changes_from_sliders(self): + def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders and hide sliders view. """ - # as here our properties to change refer directly to project properties # we modify their values directly for key,prop in self._prop_to_revert.items(): @@ -216,27 +205,16 @@ def cancel_changes_from_sliders(self): self._parent.show_or_hide_sliders(False) - def save_changes(self) -> None: - """Save changes to the project.""" - # sync list items (wrap around update_project_view() which sets them to zero by default) - # the list can lose focus when a contrast is edited... default to first item if this happens - #errors = "\n ".join(self.validate_draft_project()) + def _apply_changes_from_sliders(self) -> None: + """Apply changes obtained from sliders to the project + and make them permanent + """ + for key,prop in self._prop_to_change.items(): + self._prop_to_revert[key].value = prop.value + + # TODO: update project view: self._parent.show_or_hide_sliders(False) return - errors = None - if errors: - self._parent.terminal_widget.write_error(f"Could not save draft project:\n {errors}") - else: - # catch errors from Pydantic as fallback rather than crashing - try: - self._parent.presenter.edit_project(self.draft_project) - except ValidationError as err: - custom_error_list = custom_pydantic_validation_error(err.errors(include_url=False)) - custom_errors = ValidationError.from_exception_data(err.title, custom_error_list, hide_input=True) - self._parent.terminal_widget.write_error(f"Could not save draft project:\n {custom_errors}") - else: - self._parent.show_or_hide_sliders(False) - #======================================================================================================================= class LabeledSlider(QtWidgets.QFrame): def __init__(self, param: ratapi.models.Parameter, parent=None): @@ -244,20 +222,22 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): self._prop = param # hold the property controlled by slider self.slider_name = param.name # name the slider as the property it refers to + self._value_min = 0 # default minimal value property may have + self._value_range = 100 # default maximal value the property may have + # the change in property value per single step slider move + self._value_step = 1 # Properties of slider widget: self._slider_min_idx = 0 - self._slider_max_idx = 100 # defines accuracy of slider motion - self._ticks_step = 10 # sliders ticks + self._slider_max_idx = 100 # defines accuracy of slider motion + self._ticks_step = 10 # sliders ticks + self._labels = [] # number of slider labels can not change too - # Characteristics of the property value to display - self._value_min = self._prop.min - self._value_range = (self._prop.max-self._value_min) - # the change in property value per single step slider move - self._value_step = self._value_range/self._slider_max_idx + self.update_slider_parameters(param,True) # 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(str(f"{param.value:.3g}"), alignment=QtCore.Qt.AlignmentFlag.AlignRight) lab_layout = QtWidgets.QHBoxLayout() @@ -266,11 +246,12 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): # layout for numeric scale below scale_layout = QtWidgets.QHBoxLayout() - for idx in range(self._slider_min_idx, self._slider_max_idx, self._ticks_step): + for idx in range(self._slider_min_idx, self._slider_max_idx+1, self._ticks_step): tick_value = self._slider_pos_to_value(idx) label = QtWidgets.QLabel(str(f"{tick_value:.2g}")) - label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) scale_layout.addWidget(label) + self._labels.append(label) layout = QtWidgets.QVBoxLayout(self) layout.addLayout(lab_layout) @@ -284,6 +265,19 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): self.setFrameShape(QtWidgets.QFrame.Shape.Box) self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + def update_slider_parameters(self, param: ratapi.models.Parameter, in_constructor = False): + """Modifies slider values which may change for this slider from his parent property""" + + # Characteristics of the property value to display + self._value_min = self._prop.min + self._value_range = (self._prop.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 labels + def _value_to_slider_pos(self, value: float) -> int: """Convert double property value into slider position""" return int(round(self._slider_max_idx*(value-self._value_min)/self._value_range,0)) @@ -292,6 +286,7 @@ def _slider_pos_to_value(self,index: int) -> float: """convert double property value into slider position""" return self._value_min + index*self._value_step + def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: """Construct slider widget with integer scales""" From 1d219f15c66c17fc7ba3124619466ad4f1726c4e Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 4 Nov 2025 10:34:07 +0000 Subject: [PATCH 17/69] Re #148 Bug in update_slider_parameters --- rascal2/widgets/sliders_view.py | 34 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index d5c99048..ffeb6e7d 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -212,7 +212,7 @@ def _apply_changes_from_sliders(self) -> None: for key,prop in self._prop_to_change.items(): self._prop_to_revert[key].value = prop.value - # TODO: update project view: + # TODO: Re #149 update project view: self._parent.show_or_hide_sliders(False) return #======================================================================================================================= @@ -222,15 +222,18 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): self._prop = param # hold the property controlled by slider self.slider_name = param.name # name the slider as the property it refers to + # Defaults for property min/max. Will be overwritten self._value_min = 0 # default minimal value property may have self._value_range = 100 # default maximal value the property may have - # the change in property value per single step slider move - self._value_step = 1 + self._value_step = 1 # the change in property value per single step slider move + # Properties of slider widget: - self._slider_min_idx = 0 + self._num_slider_ticks = 11 self._slider_max_idx = 100 # defines accuracy of slider motion self._ticks_step = 10 # sliders ticks self._labels = [] # number of slider labels can not change too + self._value_label_format = "{:.3g}" # format to display slider value + self._tick_label_format = "{:.2g}" # format to display numbers under the sliders ticks self.update_slider_parameters(param,True) @@ -239,16 +242,18 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): # 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(str(f"{param.value:.3g}"), alignment=QtCore.Qt.AlignmentFlag.AlignRight) + self._value_label = QtWidgets.QLabel(self._value_label_format.format(param.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() - for idx in range(self._slider_min_idx, self._slider_max_idx+1, self._ticks_step): - tick_value = self._slider_pos_to_value(idx) - label = QtWidgets.QLabel(str(f"{tick_value:.2g}")) + + 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 + label = QtWidgets.QLabel(self._tick_label_format.format(tick_value)) label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) scale_layout.addWidget(label) self._labels.append(label) @@ -265,6 +270,7 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): self.setFrameShape(QtWidgets.QFrame.Shape.Box) self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + def update_slider_parameters(self, param: ratapi.models.Parameter, in_constructor = False): """Modifies slider values which may change for this slider from his parent property""" @@ -276,7 +282,13 @@ def update_slider_parameters(self, param: ratapi.models.Parameter, in_constructo if in_constructor: return - # otherwise, update slider labels + # otherwise, update slider's labels + self._value_label.setText(self._value_label_format.format(param.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 _value_to_slider_pos(self, value: float) -> int: """Convert double property value into slider position""" @@ -291,7 +303,7 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: """Construct slider widget with integer scales""" slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) - slider.setMinimum(self._slider_min_idx) + slider.setMinimum(0) slider.setMaximum(self._slider_max_idx) slider.setTickInterval(self._ticks_step) slider.setSingleStep(self._slider_max_idx) @@ -302,5 +314,5 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: def _update_value(self, idx: int)->None: val = self._slider_pos_to_value(idx) - self._value_label.setText(str(f"{val:.3g}")) + self._value_label.setText(self._value_label_format.format(val)) self._prop.value = val From 625610e2faa8a1cf35cace940a60fb7d21fc64b3 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 4 Nov 2025 10:54:11 +0000 Subject: [PATCH 18/69] Re #148 Decrease distance between slider widgets --- rascal2/widgets/sliders_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index ffeb6e7d..1456bf7d 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -177,6 +177,7 @@ def _add_sliders_widgets(self) -> None: content_layout.addWidget(no_label) self._sliders['No_properties_to_fit_label'] = no_label else: + content_layout.setSpacing(0) for prop in self._prop_to_change.values(): slider = LabeledSlider(prop) @@ -300,7 +301,7 @@ def _slider_pos_to_value(self,index: int) -> float: def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: - """Construct slider widget with integer scales""" + """Construct slider widget with integer scales and ticks in integer positions """ slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) slider.setMinimum(0) From 2f6278331aaf770c16b16be981bbc3d8803462c1 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 5 Nov 2025 12:02:03 +0000 Subject: [PATCH 19/69] Re #148 Slightly better way of defining sliders labels and initial unit tests for LabeledSlider --- rascal2/widgets/sliders_view.py | 20 +++++++++++++++++--- tests/widgets/test_sliders_widget.py | 27 ++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 1456bf7d..7c3893a3 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -45,6 +45,10 @@ def show(self): widget list depending on previous state of the widget """ + # avoid running init view more than once if sliders are visible anyway + if self.isVisible(): + return + self.init() if self.mdi_holder is None: self._view_geometry = None @@ -63,6 +67,7 @@ def hide(self): """Overload parent hide method to deal with mdi container hiding slider widgets window """ + if self.mdi_holder is None: super().hide() else: @@ -229,10 +234,10 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): self._value_step = 1 # the change in property value per single step slider move # Properties of slider widget: - self._num_slider_ticks = 11 + self._num_slider_ticks = 10 self._slider_max_idx = 100 # defines accuracy of slider motion self._ticks_step = 10 # sliders ticks - self._labels = [] # number of slider labels can not change too + self._labels = [] # list of slider labels describing sliders axis self._value_label_format = "{:.3g}" # format to display slider value self._tick_label_format = "{:.2g}" # format to display numbers under the sliders ticks @@ -252,10 +257,19 @@ def __init__(self, param: ratapi.models.Parameter, parent=None): 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 label = QtWidgets.QLabel(self._tick_label_format.format(tick_value)) - label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) + 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) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 869ad45e..45bc4dbe 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -7,4 +7,29 @@ from rascal2.widgets.sliders_view import ( SlidersViewWidget, LabeledSlider -) \ No newline at end of file +) +@pytest.fixture +def slider(): + param = ratapi.models.Parameter(name = "Test Slider", min=1, max=10, value = 2.1, fit=True) + return LabeledSlider(param) + + +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_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) + + From 87edc5be679544c6b684e8ddef0dc4d525930d14 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 5 Nov 2025 12:55:39 +0000 Subject: [PATCH 20/69] Re #148 better treatment for slider widget when edit project is involved --- rascal2/widgets/project/project.py | 2 ++ rascal2/widgets/project/tables.py | 15 ++------------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index f52d7d50..c5b5b49b 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -360,6 +360,8 @@ def show_project_view(self) -> None: def show_edit_view(self) -> None: """Show edit view""" + + self.parent.show_or_hide_sliders(False) # when you show it again it contents will be updated according to edit changes self.update_project_view(0) self.setWindowTitle("Edit Project") self.parent.controls_widget.run_button.setEnabled(False) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index d76d8fe9..ce0cd3db 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -231,7 +231,7 @@ def resize_columns(self): header.setStretchLastSection(True) - def update_model(self, classlist): + def update_model(self, classlist: ratapi.classlist.ClassList): """Update the table model to synchronise with the project field.""" self.model = self.classlist_model(classlist, self) @@ -360,7 +360,7 @@ def set_item_delegates(self): i + 1, delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table) ) - def update_model(self, classlist): + def update_model(self, classlist: ratapi.classlist.ClassList): super().update_model(classlist) header = self.table.horizontalHeader() header.setSectionResizeMode( @@ -391,17 +391,6 @@ def edit(self): if i in self.model.protected_indices: self.table.setIndexWidget(self.model.index(i, 0), None) -class ParameterSliderWidget(QtWidgets.QWidget): - """Subclass of field widgets for slider view""" - def __init__(self, field: str, parent): - super().__init__(parent) - self.field = field - layout = QtWidgets.QVBoxLayout() - contents = QtWidgets.QLabel(" slides for {} have not been implemented".format(field)) - layout.addWidget(contents) - self.setLayout(layout) - - class LayersModel(ClassListTableModel): """Classlist model for Layers.""" From 4b93618d2b9dbe85f73039280349d5caffcb4d89 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 5 Nov 2025 17:09:46 +0000 Subject: [PATCH 21/69] Re #148 fixed test_project and check if edit button indeed hides sliders view window --- rascal2/widgets/project/project.py | 2 +- rascal2/widgets/sliders_view.py | 32 +++++++++++++-------------- tests/ui/test_presenter.py | 1 + tests/widgets/project/test_project.py | 20 ++++++++++++----- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index c5b5b49b..5777354a 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -361,7 +361,7 @@ def show_project_view(self) -> None: def show_edit_view(self) -> None: """Show edit view""" - self.parent.show_or_hide_sliders(False) # when you show it again it contents will be updated according to edit changes + self.parent.show_or_hide_sliders(do_show_sliders=False) # when you show it again it contents will be updated according to edit changes self.update_project_view(0) self.setWindowTitle("Edit Project") self.parent.controls_widget.run_button.setEnabled(False) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 7c3893a3..584a4198 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -23,7 +23,8 @@ def __init__(self, parent): An instance of the MainWindowView """ super().__init__() - self.mdi_holder = None # the variable contains reference to mdi container holding this widget + self.mdi_holder = None # the variable contains reference to mdi container holding this widget + # Will be set up in the presenter, which arranges mdi windows for all MainWindow widgets self._view_geometry = None # holder for slider view geometry, created to store slider view location # within the main window for subsequent calls to show sliders. Not yet restored from hdd properly # inherits project geometry on the first view. @@ -135,7 +136,6 @@ def _create_slider_view_layout(self) -> None: """ main_layout = QtWidgets.QVBoxLayout() - #main_layout.setSpacing(20) accept_button = QtWidgets.QPushButton("Accept", self, objectName="AcceptButton") accept_button.clicked.connect(self._apply_changes_from_sliders) @@ -225,23 +225,23 @@ def _apply_changes_from_sliders(self) -> None: class LabeledSlider(QtWidgets.QFrame): def __init__(self, param: ratapi.models.Parameter, parent=None): super().__init__(parent) + # Defaults for property min/max. Will be overwritten + self._value_min : float | None = 0 # minimal value property may have + self._value_range : float | None = 100 # value range (difference between maximal and minimal values of the property) + self._value_step : float | None = 1 # the change in property value per single step slider move + self._prop = param # hold the property controlled by slider self.slider_name = param.name # name the slider as the property it refers to - # Defaults for property min/max. Will be overwritten - self._value_min = 0 # default minimal value property may have - self._value_range = 100 # default maximal value the property may have - self._value_step = 1 # the change in property value per single step slider move - - # Properties of slider widget: - self._num_slider_ticks = 10 - self._slider_max_idx = 100 # defines accuracy of slider motion - self._ticks_step = 10 # sliders ticks - self._labels = [] # list of slider labels describing sliders axis - self._value_label_format = "{:.3g}" # format to display slider value - self._tick_label_format = "{:.2g}" # format to display numbers under the sliders ticks - - self.update_slider_parameters(param,True) + # Internal properties of slider widget: + self._num_slider_ticks : int = 10 + self._slider_max_idx : int = 100 # defines accuracy of slider motion + self._ticks_step : int = 10 # Number of sliders ticks + self._labels : list = [] # list of slider labels describing sliders axis + self._value_label_format : str = "{:.3g}" # format to display slider value + self._tick_label_format : str = "{:.2g}" # format to display numbers under the sliders ticks + + 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) diff --git a/tests/ui/test_presenter.py b/tests/ui/test_presenter.py index b3582396..66f8cb81 100644 --- a/tests/ui/test_presenter.py +++ b/tests/ui/test_presenter.py @@ -44,6 +44,7 @@ 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/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 4e37919d..7517c836 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pydantic import pytest @@ -6,6 +6,7 @@ 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, @@ -36,6 +37,13 @@ def __init__(self): super().__init__() self.presenter = MockPresenter() self.controls_widget = MagicMock() + 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() class DataModel(pydantic.BaseModel, validate_assignment=True): @@ -151,8 +159,8 @@ def test_project_widget_initial_state(setup_project_widget): assert project_widget.project_tab.currentIndex() == 0 assert project_widget.edit_project_tab.currentIndex() == 0 - -def test_edit_cancel_button_toggle(setup_project_widget): +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_edit_cancel_button_toggle(mock_hide,setup_project_widget): """ Tests clicking the edit button causes the stacked widget to change state. """ @@ -161,6 +169,7 @@ def test_edit_cancel_button_toggle(setup_project_widget): assert project_widget.stacked_widget.currentIndex() == 0 project_widget.edit_project_button.click() assert project_widget.stacked_widget.currentIndex() == 1 + assert mock_hide.call_count == 1 assert project_widget.geometry_combobox.currentText() == Geometries.AirSubstrate assert project_widget.model_combobox.currentText() == LayerModels.StandardLayers @@ -173,14 +182,15 @@ def test_edit_cancel_button_toggle(setup_project_widget): assert project_widget.model_type.text() == LayerModels.StandardLayers assert project_widget.calculation_type.text() == Calculations.Normal - -def test_save_changes_to_model_project(setup_project_widget): +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_save_changes_to_model_project(mock_hide,setup_project_widget): """ Tests that making changes to the project settings """ project_widget = setup_project_widget project_widget.edit_project_button.click() + assert mock_hide.call_count == 1 project_widget.calculation_combobox.setCurrentText(Calculations.Domains) project_widget.geometry_combobox.setCurrentText(Geometries.SubstrateLiquid) From 8938f38e1ef8b4c9ee2a73be1f9bb065c605e0c8 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 6 Nov 2025 19:35:31 +0000 Subject: [PATCH 22/69] Re #148 looks like sliders updating project calculations. Unexpected. Needs to do proper undo process --- rascal2/widgets/sliders_view.py | 44 ++++++++++++++++++++++----- tests/widgets/project/test_project.py | 19 ++++++------ 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 584a4198..6506d491 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -6,6 +6,11 @@ import ratapi.models from PyQt6 import QtCore,QtWidgets +import rascal2.widgets.project +from rascal2.widgets.project.tables import ( + ParametersModel +) + class SlidersViewWidget(QtWidgets.QWidget): """ @@ -34,6 +39,8 @@ def __init__(self, parent): # back into original project if cancel button is pressed self._prop_to_change = {} # dictionary of references to 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._gui_modifiers = {} # dictionary containing lamda functions which modifies child of QTAbstractDataModel + # and causes self._sliders = {} # dictionary of the sliders used to display fittable values # create initial slider view layout and everything else which depends on it @@ -116,7 +123,7 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: n_existing_properties = 0 for field_name in ratapi.Project.model_fields: attr = getattr(project, field_name) - if isinstance(attr, ratapi.ClassList): + if isinstance(attr, ratapi.ClassList): # ClassList contains the parameters which may be fittable param_list = attr.data for prop in param_list: if isinstance(prop,ratapi.models.Parameter) and prop.fit: @@ -128,6 +135,19 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: # are in existing dictionary, we will update widgets instead of adding the new one self._prop_to_change = trial_properties # References to project properties self._prop_to_revert = deepcopy(self._prop_to_change) # Copy of initial values of project properties + + proj1 = self._parent.project_widget + for widget in proj1.view_tabs.values(): + for param in widget.tables.values(): + mod = param.model + if isinstance(mod, ParametersModel): + row = 0 + for model_param in mod.classlist: + if model_param.fit: + # value is defined in column 4 so we do need to change it eventually + self._gui_modifiers[model_param.name] = lambda val, model = mod, the_row = row: model.setData(model.index(the_row, 4),val,QtCore.Qt.ItemDataRole.EditRole) + row += 1 + return update_properties def _create_slider_view_layout(self) -> None: @@ -183,8 +203,8 @@ def _add_sliders_widgets(self) -> None: self._sliders['No_properties_to_fit_label'] = no_label else: content_layout.setSpacing(0) - for prop in self._prop_to_change.values(): - slider = LabeledSlider(prop) + for name,prop in self._prop_to_change.items(): + slider = LabeledSlider(prop,self._gui_modifiers[name]) self._sliders[prop.name] = slider content_layout.addWidget(slider) @@ -198,7 +218,7 @@ def _update_sliders_widgets(self) -> None: def show_sliders_view(self) -> None: """Show project view""" - self._parent.show_or_hide_sliders(True) + self._parent.show_or_hide_sliders(do_show_sliders=True) def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders @@ -209,7 +229,7 @@ def _cancel_changes_from_sliders(self): for key,prop in self._prop_to_revert.items(): self._prop_to_change[key].value = prop.value - self._parent.show_or_hide_sliders(False) + self._parent.show_or_hide_sliders(do_show_sliders=False) def _apply_changes_from_sliders(self) -> None: """Apply changes obtained from sliders to the project @@ -223,8 +243,16 @@ def _apply_changes_from_sliders(self) -> None: return #======================================================================================================================= class LabeledSlider(QtWidgets.QFrame): - def __init__(self, param: ratapi.models.Parameter, parent=None): - super().__init__(parent) + def __init__(self, param: ratapi.models.Parameter, table_modifier=None): + """Construct LabeledSlider for a particular property + Inputs: + ------ + param -- the property, which value will be modified by slider + table_modifier -- Lambda function which sets value obtained from slider into QTModel using overloaded model's + setData method. This method, in turn, recalculates model and display resulting graphics + """ + super().__init__() + self._gui_modifier = table_modifier # Defaults for property min/max. Will be overwritten self._value_min : float | None = 0 # minimal value property may have self._value_range : float | None = 100 # value range (difference between maximal and minimal values of the property) @@ -331,3 +359,5 @@ def _update_value(self, idx: int)->None: val = self._slider_pos_to_value(idx) self._value_label.setText(self._value_label_format.format(val)) self._prop.value = val + if not self._gui_modifier is None: + self._gui_modifier(val) diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 7517c836..bb0466d4 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -187,21 +187,20 @@ def test_save_changes_to_model_project(mock_hide,setup_project_widget): """ Tests that making changes to the project settings """ - project_widget = setup_project_widget - project_widget.edit_project_button.click() + setup_project_widget.edit_project_button.click() assert mock_hide.call_count == 1 - project_widget.calculation_combobox.setCurrentText(Calculations.Domains) - project_widget.geometry_combobox.setCurrentText(Geometries.SubstrateLiquid) - project_widget.model_combobox.setCurrentText(LayerModels.CustomXY) + setup_project_widget.calculation_combobox.setCurrentText(Calculations.Domains) + setup_project_widget.geometry_combobox.setCurrentText(Geometries.SubstrateLiquid) + setup_project_widget.model_combobox.setCurrentText(LayerModels.CustomXY) - assert project_widget.draft_project["geometry"] == Geometries.SubstrateLiquid - assert project_widget.draft_project["model"] == LayerModels.CustomXY - assert project_widget.draft_project["calculation"] == Calculations.Domains + assert setup_project_widget.draft_project["geometry"] == Geometries.SubstrateLiquid + assert setup_project_widget.draft_project["model"] == LayerModels.CustomXY + assert setup_project_widget.draft_project["calculation"] == Calculations.Domains - project_widget.save_changes() - assert project_widget.parent.presenter.edit_project.call_count == 1 + setup_project_widget.save_changes() + assert setup_project_widget.parent.presenter.edit_project.call_count == 1 def test_cancel_changes_to_model_project(setup_project_widget): From 28f900e8898ea68e12fdb2a42fae88a5e3910ee3 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 7 Nov 2025 13:39:53 +0000 Subject: [PATCH 23/69] Re #148 Looks like working project logic. Looks like some bugs in processing properties --- rascal2/widgets/project/tables.py | 4 +- rascal2/widgets/sliders_view.py | 138 +++++++++++++++++++----------- 2 files changed, 89 insertions(+), 53 deletions(-) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index ce0cd3db..9c18de65 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -75,7 +75,7 @@ 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, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool: + def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole,recalculate_proj = True) -> bool: if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole: row = index.row() param = self.index_header(index) @@ -93,7 +93,7 @@ def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool: return False if not self.edit_mode: # recalculate plots if value was changed - recalculate = self.index_header(index) == "value" + recalculate = self.index_header(index) == "value" and recalculate_proj self.parent.update_project(recalculate) self.dataChanged.emit(index, index) return True diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 6506d491..b5974b08 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -2,11 +2,9 @@ from copy import deepcopy -import ratapi import ratapi.models from PyQt6 import QtCore,QtWidgets -import rascal2.widgets.project from rascal2.widgets.project.tables import ( ParametersModel ) @@ -35,12 +33,11 @@ def __init__(self, parent): # inherits project geometry on the first view. self._parent = parent # reference to main view widget which holds sliders view - self._prop_to_revert ={} # dictionary of original properties with fit parameter "true" but have to be restored - # back into original project if cancel button is pressed - self._prop_to_change = {} # dictionary of references to 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._gui_modifiers = {} # dictionary containing lamda functions which modifies child of QTAbstractDataModel - # and causes + 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 SliderUpdateHoler 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 # create initial slider view layout and everything else which depends on it @@ -119,35 +116,28 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: if project is None: return False - trial_properties = {} - n_existing_properties = 0 - for field_name in ratapi.Project.model_fields: - attr = getattr(project, field_name) - if isinstance(attr, ratapi.ClassList): # ClassList contains the parameters which may be fittable - param_list = attr.data - for prop in param_list: - if isinstance(prop,ratapi.models.Parameter) and prop.fit: - trial_properties[prop.name] = prop - if prop.name in self._prop_to_change: - n_existing_properties += 1 - - update_properties = n_existing_properties == len(trial_properties) # if all properties of trial dictionary - # are in existing dictionary, we will update widgets instead of adding the new one - self._prop_to_change = trial_properties # References to project properties - self._prop_to_revert = deepcopy(self._prop_to_change) # Copy of initial values of project properties - proj1 = self._parent.project_widget + n_updated_properties = 0 + trial_properties = {} for widget in proj1.view_tabs.values(): for param in widget.tables.values(): - mod = param.model - if isinstance(mod, ParametersModel): + vis_model = param.model + if isinstance(vis_model, ParametersModel): row = 0 - for model_param in mod.classlist: + for model_param in vis_model.classlist: if model_param.fit: - # value is defined in column 4 so we do need to change it eventually - self._gui_modifiers[model_param.name] = lambda val, model = mod, the_row = row: model.setData(model.index(the_row, 4),val,QtCore.Qt.ItemDataRole.EditRole) + trial_properties[model_param.name] = SliderChangeHolder(row_number=row,model=vis_model,param=model_param) + if model_param.name in self._prop_to_change: + n_updated_properties += 1 row += 1 + update_properties = n_updated_properties == len(trial_properties) # 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. + self._prop_to_change = trial_properties # References to project properties + # remember values for properties controlled by sliders in case if you may want to revert them 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: @@ -204,14 +194,14 @@ def _add_sliders_widgets(self) -> None: else: content_layout.setSpacing(0) for name,prop in self._prop_to_change.items(): - slider = LabeledSlider(prop,self._gui_modifiers[name]) + slider = LabeledSlider(prop) self._sliders[prop.name] = slider content_layout.addWidget(slider) def _update_sliders_widgets(self) -> None: """Updates the sliders given the project properties to fit are the same - but their values may be upgraded + but their values may be modified """ for name,slider in self._sliders.items(): self._sliders[name].update_slider_parameters(self._prop_to_change[name]) @@ -222,12 +212,15 @@ def show_sliders_view(self) -> None: def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders - and hide sliders view. + and hide sliders view. """ - # as here our properties to change refer directly to project properties - # we modify their values directly - for key,prop in self._prop_to_revert.items(): - self._prop_to_change[key].value = prop.value + last_key = next(reversed(self._values_to_revert)) + for key,val in self._values_to_revert.items(): + self._sliders[key].set_slider_position(val) + if key == last_key: + self._prop_to_change[key].update_value_representation(val,recalculate_project=True) + else: + self._prop_to_change[key].update_value_representation(val,recalculate_project=False) self._parent.show_or_hide_sliders(do_show_sliders=False) @@ -236,14 +229,54 @@ def _apply_changes_from_sliders(self) -> None: and make them permanent """ for key,prop in self._prop_to_change.items(): - self._prop_to_revert[key].value = prop.value + self._values_to_revert[key] = prop.value - # TODO: Re #149 update project view: self._parent.show_or_hide_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: + Inputs: + ------ + row_number: int - the number of the row in the project table which should be changed + model: rascal2.widgets.project.tables.ParametersModel - parameters model 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 + self._param_value = param.value + + @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 + setattr(self.param,"value",value) + + def update_value_representation(self,val : float, recalculate_project = True) -> None: + # 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) + # this is probably unnecessary, as have been already set through the model + self.param.value = val + + class LabeledSlider(QtWidgets.QFrame): - def __init__(self, param: ratapi.models.Parameter, table_modifier=None): + def __init__(self, param: SliderChangeHolder): """Construct LabeledSlider for a particular property Inputs: ------ @@ -252,15 +285,14 @@ def __init__(self, param: ratapi.models.Parameter, table_modifier=None): setData method. This method, in turn, recalculates model and display resulting graphics """ super().__init__() - self._gui_modifier = table_modifier + self._prop = param # hold the property controlled by slider + self.slider_name = param.name # name the slider as the property it refers to + # Defaults for property min/max. Will be overwritten self._value_min : float | None = 0 # minimal value property may have self._value_range : float | None = 100 # value range (difference between maximal and minimal values of the property) self._value_step : float | None = 1 # the change in property value per single step slider move - self._prop = param # hold the property controlled by slider - self.slider_name = param.name # name the slider as the property it refers to - # Internal properties of slider widget: self._num_slider_ticks : int = 10 self._slider_max_idx : int = 100 # defines accuracy of slider motion @@ -313,13 +345,20 @@ def __init__(self, param: ratapi.models.Parameter, table_modifier=None): self.setFrameShape(QtWidgets.QFrame.Shape.Box) self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + def set_slider_position(self,value : float) -> None: + """Set specified slider position programmatically """ + idx = self._value_to_slider_pos(value) + self._slider.setValue(idx) + self._value_label.setText(self._value_label_format.format(value)) - def update_slider_parameters(self, param: ratapi.models.Parameter, in_constructor = False): + + def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): """Modifies slider values which may change for this slider from his parent property""" + self._prop = param # Characteristics of the property value to display - self._value_min = self._prop.min - self._value_range = (self._prop.max - self._value_min) + self._value_min = self._prop.param.min + self._value_range = (self._prop.param.max - self._value_min) # the change in property value per single step slider move self._value_step = self._value_range / self._slider_max_idx @@ -341,7 +380,6 @@ def _slider_pos_to_value(self,index: int) -> float: """convert double property value into slider position""" return self._value_min + index*self._value_step - def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: """Construct slider widget with integer scales and ticks in integer positions """ @@ -358,6 +396,4 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: def _update_value(self, idx: int)->None: val = self._slider_pos_to_value(idx) self._value_label.setText(self._value_label_format.format(val)) - self._prop.value = val - if not self._gui_modifier is None: - self._gui_modifier(val) + self._prop.update_value_representation(val) From 5253082537fc1a3c3d34de6670451d6767031dd7 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 7 Nov 2025 14:55:53 +0000 Subject: [PATCH 24/69] Re #148 fixing empty sliders, incomplete --- rascal2/widgets/sliders_view.py | 72 +++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index b5974b08..573c432c 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -99,8 +99,8 @@ def init(self) -> None: def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: """Take project and copy all properties which have attribute "Fit" == True - into dictionary used to build sliders for them. Also set back-up properties - dictionary used to reset properties back to their default values if "Cancel" + 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. Input: @@ -116,10 +116,13 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: if project is None: return False - proj1 = self._parent.project_widget + proj = self._parent.project_widget + if proj is None: + return False + n_updated_properties = 0 trial_properties = {} - for widget in proj1.view_tabs.values(): + for widget in proj.view_tabs.values(): for param in widget.tables.values(): vis_model = param.model if isinstance(vis_model, ParametersModel): @@ -131,10 +134,13 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: n_updated_properties += 1 row += 1 - update_properties = n_updated_properties == len(trial_properties) # if all properties of trial dictionary - # are in existing dictionary and the number of properties are the same no new/deleted sliders have appeared. + # 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. - self._prop_to_change = trial_properties # References to project properties + update_properties = n_updated_properties == len(trial_properties) and len(self._prop_to_change) == n_updated_properties + + # store properties + self._prop_to_change = trial_properties # remember values for properties controlled by sliders in case if you may want to revert them later self._values_to_revert = {name: prop.value for name, prop in trial_properties.items()} @@ -187,10 +193,9 @@ def _add_sliders_widgets(self) -> None: self._sliders = {} if len(self._prop_to_change) == 0: - no_label = QtWidgets.QLabel("No properties to fit, Nothing to view here") - no_label.setObjectName("No_properties_to_fit") + no_label = EmptySlider() content_layout.addWidget(no_label) - self._sliders['No_properties_to_fit_label'] = no_label + self._sliders[no_label.slider_name] = no_label else: content_layout.setSpacing(0) for name,prop in self._prop_to_change.items(): @@ -203,8 +208,8 @@ 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,slider in self._sliders.items(): - self._sliders[name].update_slider_parameters(self._prop_to_change[name]) + for name,prop in self._prop_to_change.items(): + self._sliders[name].update_slider_parameters(prop) def show_sliders_view(self) -> None: """Show project view""" @@ -279,14 +284,14 @@ class LabeledSlider(QtWidgets.QFrame): def __init__(self, param: SliderChangeHolder): """Construct LabeledSlider for a particular property Inputs: - ------ - param -- the property, which value will be modified by slider - table_modifier -- Lambda function which sets value obtained from slider into QTModel using overloaded model's - setData method. This method, in turn, recalculates model and display resulting graphics + ------- + param -- 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__() + self._prop = param # hold the property controlled by slider - self.slider_name = param.name # name the slider as the property it refers to # Defaults for property min/max. Will be overwritten self._value_min : float | None = 0 # minimal value property may have @@ -300,7 +305,10 @@ def __init__(self, param: SliderChangeHolder): self._labels : list = [] # list of slider labels describing sliders axis self._value_label_format : str = "{:.3g}" # format to display slider value self._tick_label_format : str = "{:.2g}" # format to display numbers under the sliders ticks + if param is None: + return + self.slider_name = param.name # name the slider as the property it refers to self.update_slider_parameters(param,in_constructor=True) # Retrieve slider's parameters from input property # Build all sliders widget and arrange them as expected @@ -397,3 +405,33 @@ def _update_value(self, idx: int)->None: val = self._slider_pos_to_value(idx) self._value_label.setText(self._value_label_format.format(val)) self._prop.update_value_representation(val) + +class EmptySlider(LabeledSlider): + def __init__(self): + """Construct empty slider which have interface of LabeledSlider but no properties + associated with it + Inputs: + ------ + ignored + """ + super().__init__(None) + # Build all sliders widget and arrange them as expected + self._slider = self._build_slider(0) + + name_label = QtWidgets.QLabel("No property to fit. No sliders here", 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_position(self,value : float) -> None: + return + + def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): + return + + def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: + return None + + def _update_value(self, idx: int)->None: + return \ No newline at end of file From 1633a0d1445efb8adee5fa98c69a79e5a043f4fb Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 7 Nov 2025 15:49:21 +0000 Subject: [PATCH 25/69] Re #148 fixed empty slider issue and hung up due to slider out of range --- rascal2/widgets/sliders_view.py | 43 ++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 573c432c..509142ec 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -136,10 +136,10 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: # 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. + # 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 - # store properties + # store information about sliders self._prop_to_change = trial_properties # remember values for properties controlled by sliders in case if you may want to revert them later self._values_to_revert = {name: prop.value for name, prop in trial_properties.items()} @@ -187,7 +187,7 @@ def _add_sliders_widgets(self) -> None: content = scroll.findChild(QtWidgets.QWidget, "Scroll_content") content_layout = content.layout() - # We are adding new sliders, so delete all previous ones + # We are adding new sliders, so delete all previous ones. Update is done in another branch. for slider in self._sliders.values(): slider.deleteLater() self._sliders = {} @@ -219,7 +219,12 @@ def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders and hide sliders view. """ - last_key = next(reversed(self._values_to_revert)) + if len(self._values_to_revert) == 0: + last_key = None + else: # does not work with empty dictionary + last_key = next(reversed(self._values_to_revert)) + + for key,val in self._values_to_revert.items(): self._sliders[key].set_slider_position(val) if key == last_key: @@ -251,7 +256,7 @@ def __init__(self, row_number: int,model : ParametersModel, param : ratapi.model ------ row_number: int - the number of the row in the project table which should be changed model: rascal2.widgets.project.tables.ParametersModel - parameters model participating in ParametersTableView - and containing the parameter (below) to modify here. + 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 @@ -272,11 +277,17 @@ def value(self, value: float) -> None: setattr(self.param,"value",value) def update_value_representation(self,val : float, recalculate_project = True) -> None: + """ given new value, update project table and property representations + No check are necessary as value comes from slider or back-up cache + + recalculate_project -- if True, run ratapi calculations and updates + results representation. + """ # 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) - # this is probably unnecessary, as have been already set through the model + # this is probably unnecessary, as have been already set through the model. But it's fast. self.param.value = val @@ -293,8 +304,9 @@ def __init__(self, param: SliderChangeHolder): self._prop = param # hold the property controlled by slider - # Defaults for property min/max. Will be overwritten + # Defaults for property min/max. Will be overwritten from actual input property self._value_min : float | None = 0 # minimal value property may have + self._value_max : float | None = 100 # maximal value property may have self._value_range : float | None = 100 # value range (difference between maximal and minimal values of the property) self._value_step : float | None = 1 # the change in property value per single step slider move @@ -303,7 +315,7 @@ def __init__(self, param: SliderChangeHolder): self._slider_max_idx : int = 100 # defines accuracy of slider motion self._ticks_step : int = 10 # Number of sliders ticks self._labels : list = [] # list of slider labels describing sliders axis - self._value_label_format : str = "{:.3g}" # format to display slider value + self._value_label_format : str = "{:.4g}" # format to display slider value. Should be not too accurate as slider is not very accurate. self._tick_label_format : str = "{:.2g}" # format to display numbers under the sliders ticks if param is None: return @@ -359,14 +371,14 @@ def set_slider_position(self,value : float) -> None: self._slider.setValue(idx) self._value_label.setText(self._value_label_format.format(value)) - def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): """Modifies slider values which may change for this slider from his parent property""" self._prop = param # Characteristics of the property value to display self._value_min = self._prop.param.min - self._value_range = (self._prop.param.max - self._value_min) + self._value_max = self._prop.param.max + 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 @@ -379,14 +391,16 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F tick_value = self._value_min+idx*tick_step self._labels[idx].setText(self._tick_label_format.format(tick_value)) - def _value_to_slider_pos(self, value: float) -> int: """Convert double property value into slider position""" 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 double property value into slider position""" - return self._value_min + index*self._value_step + value = self._value_min + index*self._value_step + if value > self._value_max: # This should not happen but may 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 """ @@ -406,6 +420,7 @@ def _update_value(self, idx: int)->None: self._value_label.setText(self._value_label_format.format(val)) self._prop.update_value_representation(val) + class EmptySlider(LabeledSlider): def __init__(self): """Construct empty slider which have interface of LabeledSlider but no properties @@ -418,7 +433,7 @@ def __init__(self): # Build all sliders widget and arrange them as expected self._slider = self._build_slider(0) - name_label = QtWidgets.QLabel("No property to fit. No sliders here", alignment=QtCore.Qt.AlignmentFlag.AlignCenter) + name_label = QtWidgets.QLabel("No property to fit within the project. No sliders constructed", alignment=QtCore.Qt.AlignmentFlag.AlignCenter) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(name_label) self.slider_name = "Empty Slider" @@ -434,4 +449,4 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: return None def _update_value(self, idx: int)->None: - return \ No newline at end of file + return From 1391a4b4d92561b50e86725f5313c71b5c1aa558 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 7 Nov 2025 21:56:58 +0000 Subject: [PATCH 26/69] Re #148 starting write mocks for slider view tests --- rascal2/widgets/project/tables.py | 6 +++- tests/widgets/test_sliders_widget.py | 53 ++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 9c18de65..a18732c2 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -75,7 +75,11 @@ 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, value, role=QtCore.Qt.ItemDataRole.EditRole,recalculate_proj = True) -> bool: + def setData(self, index : QtCore.QModelIndex, value : float, role=QtCore.Qt.ItemDataRole.EditRole,recalculate_proj = True) -> bool: + """ Implement abstract setData method of QAbstractTableModel with additional variable + recalculate_proj -- Set it to False when modifying a bunch of properties in a loop, setting + it to True for the last value to recalculate project and update all table dependent widgets. + """ if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole: row = index.row() param = self.index_header(index) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 45bc4dbe..39ac3cbe 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -1,17 +1,66 @@ from unittest.mock import MagicMock, patch +import pydantic import pytest import ratapi -from PyQt6 import QtWidgets + +from PyQt6 import QtWidgets, QtCore from rascal2.widgets.sliders_view import ( SlidersViewWidget, + SliderChangeHolder, LabeledSlider ) +from rascal2.widgets.project.tables import ( + ParametersModel +) + +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) - return LabeledSlider(param) + parent = QtWidgets.QWidget() + class_view = ratapi.ClassList( + [ + DataModel(name="A", min = 0,value=1,max=100,fit=True,show_priors = False), + DataModel(name="B", min = 0, value=6,max = 200,fit=True,show_priors = False), + DataModel(name="C", min = 0,value=18,max=300,fit=True,show_priors = False) + ] + ) + model = ParametersModelMock(class_view, parent) + inputs = SliderChangeHolder(row_number=2,model=model, param=param) + return LabeledSlider(inputs) def test_a_slider_construction(slider): From 82abb27a87f13d0395d8f204705e21dd5a5220c5 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 7 Nov 2025 21:58:10 +0000 Subject: [PATCH 27/69] Re #148 Playing with code of LabeledSlider -- modifying according to recent Python fashion --- rascal2/widgets/sliders_view.py | 50 ++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 509142ec..723770f3 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -254,7 +254,7 @@ def __init__(self, row_number: int,model : ParametersModel, param : ratapi.model """ Class Initialization function: Inputs: ------ - row_number: int - the number of the row in the project table which should be changed + row_number: int - the number of the row in the project table, which should be changed model: rascal2.widgets.project.tables.ParametersModel - parameters model 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 @@ -262,6 +262,7 @@ def __init__(self, row_number: int,model : ParametersModel, param : ratapi.model self.param = param self._vis_model = model self._row_number = row_number + # auxiliary helper variable self._param_value = param.value @property @@ -287,11 +288,34 @@ def update_value_representation(self,val : float, recalculate_project = True) -> # 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) - # this is probably unnecessary, as have been already set through the model. But it's fast. + # this is probably unnecessary, as have been already set through the model. But it's fast and much more + # convenient for testing self.param.value = val class LabeledSlider(QtWidgets.QFrame): + """ Class describes slider widget which + allows modifying rascal property value and its representation + in project table view + """ + # Instance attributes generator + # Defaults for property min/max. Will be overwritten from actual input property + _value_min: float | None = 0 # minimal value property may have + _value_max: float | None = 100 # maximal value property may have + _value_range: float | None = 100 # value range (difference between maximal and minimal values of the property) + _value_step: float | None = 1 # the change in property value per single step slider move + + _labels: list = [] # list of slider labels describing sliders axis + + # Class attributes of slider widget which usually remain the same for all classes. Do not override unless in __init__ + # method + _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 is not very accurate. + _tick_label_format: str = "{:.2g}" # format to display numbers under the sliders ticks + + def __init__(self, param: SliderChangeHolder): """Construct LabeledSlider for a particular property Inputs: @@ -301,22 +325,7 @@ def __init__(self, param: SliderChangeHolder): property in the correspondent project table. """ super().__init__() - self._prop = param # hold the property controlled by slider - - # Defaults for property min/max. Will be overwritten from actual input property - self._value_min : float | None = 0 # minimal value property may have - self._value_max : float | None = 100 # maximal value property may have - self._value_range : float | None = 100 # value range (difference between maximal and minimal values of the property) - self._value_step : float | None = 1 # the change in property value per single step slider move - - # Internal properties of slider widget: - self._num_slider_ticks : int = 10 - self._slider_max_idx : int = 100 # defines accuracy of slider motion - self._ticks_step : int = 10 # Number of sliders ticks - self._labels : list = [] # list of slider labels describing sliders axis - self._value_label_format : str = "{:.4g}" # format to display slider value. Should be not too accurate as slider is not very accurate. - self._tick_label_format : str = "{:.2g}" # format to display numbers under the sliders ticks if param is None: return @@ -366,15 +375,18 @@ def __init__(self, param: SliderChangeHolder): self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) def set_slider_position(self,value : float) -> None: - """Set specified slider position programmatically """ + """Set specified slider GUI position programmatically """ idx = self._value_to_slider_pos(value) self._slider.setValue(idx) self._value_label.setText(self._value_label_format.format(value)) def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): """Modifies slider values which may change for this slider from his parent property""" - 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: + raise RuntimeError("Existing slider may be responsible for only one property") # Characteristics of the property value to display self._value_min = self._prop.param.min self._value_max = self._prop.param.max From 2465ae8bf846f241d93cb9da3160d3ae5d84aca3 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 10 Nov 2025 16:03:18 +0000 Subject: [PATCH 28/69] Re #148 looks like all delegates connections have been set correctly. Modifying sliders view to work efficiently and accept changes from tables correctly. Incomplete --- rascal2/widgets/delegates.py | 9 ++++ rascal2/widgets/project/project.py | 1 + rascal2/widgets/project/tables.py | 37 +++++++++++++- rascal2/widgets/sliders_view.py | 81 ++++++++++++++++++++++-------- 4 files changed, 106 insertions(+), 22 deletions(-) diff --git a/rascal2/widgets/delegates.py b/rascal2/widgets/delegates.py index 2dfc5f05..f5dea9f6 100644 --- a/rascal2/widgets/delegates.py +++ b/rascal2/widgets/delegates.py @@ -11,6 +11,10 @@ class ValidatedInputDelegate(QtWidgets.QStyledItemDelegate): """Item delegate for validated inputs.""" + # create custom signal to send to labelled sliders when contents of a cell in + # a table class have been changed + editingFinished_InformSliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) + def __init__(self, field_info, parent, remove_items: list[int] = None, open_on_show: bool = False): super().__init__(parent) self.table = parent @@ -50,6 +54,7 @@ def setEditorData(self, _editor: QtWidgets.QWidget, index): def setModelData(self, _editor, model, index): data = self.widget.get_data() model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole) + self.editingFinished_InformSliders.emit(index, self.field_info) class CustomFileFunctionDelegate(QtWidgets.QStyledItemDelegate): @@ -96,6 +101,9 @@ class ValueSpinBoxDelegate(QtWidgets.QStyledItemDelegate): The field of the parameter """ + # create custom signal to send to labelled sliders when contents of a cell in + # a table cell attached to sliders have been changed + editingFinished_InformSliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) def __init__(self, field: Literal["min", "value", "max"], parent): super().__init__(parent) @@ -130,6 +138,7 @@ def setEditorData(self, editor: AdaptiveDoubleSpinBox, index): def setModelData(self, editor, model, index): data = editor.value() model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole) + self.editingFinished_InformSliders.emit(index, self.field) class ProjectFieldDelegate(QtWidgets.QStyledItemDelegate): diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index 5777354a..b9adf768 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -542,6 +542,7 @@ def __init__(self, fields: list[str], parent, edit_mode: bool = False): self.tables[field] = DataWidget(field, self) else: self.tables[field] = ProjectFieldWidget(field, self) + self.tables[field].setObjectName(field) layout.addWidget(self.tables[field]) scroll_area = QtWidgets.QScrollArea() diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index a18732c2..d2933755 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -167,11 +167,35 @@ class ProjectFieldWidget(QtWidgets.QWidget): """ classlist_model = ClassListTableModel + # Expose delegates which react on changes in a table to sliders which depend on these change + tables_changed_delegate_for_sliders = {} # set it in the set_delegates method # the model can change and disconnect, so we re-connect it # to a signal here on each change edited = QtCore.pyqtSignal() + + @classmethod + def add_sliders_delegate(cls,table_name: str, var_name: str, delegate) -> None: + """Add the delegate responsible for editing the fields which are reflected by sliders to + the dictionary of delegates exposed to sliders widget + Inputs: + table_name : str -- the name of the table the delegate is for + var_name : str -- the name of variable exposed to sliders (min,max, value) + delegate : ValueSpinBox or ValidatedInputDelegate + -- the reference to the appropriate delegate itself + """ + if table_name not in cls.tables_changed_delegate_for_sliders.keys(): + slider_params = {} + else: + slider_params = cls.tables_changed_delegate_for_sliders[table_name] + # why do we need to talk to all delegates and can not emit proper signals from a global class method? + if var_name not in slider_params.keys(): + slider_params[var_name] = [delegate] + else: + slider_params[var_name].append(delegate) + cls.tables_changed_delegate_for_sliders[table_name] = slider_params + def __init__(self, field: str, parent): super().__init__(parent) self.field = field @@ -179,6 +203,7 @@ def __init__(self, field: str, parent): self.parent = parent self.project_widget = parent.parent self.table = QtWidgets.QTableView(parent) + self.table.horizontalHeader().setCascadingSectionResizes(True) self.table.setMinimumHeight(100) @@ -252,10 +277,14 @@ 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, - delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table), + delegate ) + if header in ["min", "value", "max"]: + ProjectFieldWidget.add_sliders_delegate(self.field, header, delegate) + def append_item(self): """Append an item to the model if the model exists.""" @@ -356,9 +385,13 @@ class ParameterFieldWidget(ProjectFieldWidget): classlist_model = ParametersModel def set_item_delegates(self): + for i, header in enumerate(self.model.headers): if header in ["min", "value", "max"]: - self.table.setItemDelegateForColumn(i + 1, delegates.ValueSpinBoxDelegate(header, self.table)) + delegate = delegates.ValueSpinBoxDelegate(header, self.table) + self.table.setItemDelegateForColumn(i + 1, delegate) + + ProjectFieldWidget.add_sliders_delegate(self.field, header, delegate) else: self.table.setItemDelegateForColumn( i + 1, delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 723770f3..799a7d13 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -6,7 +6,9 @@ from PyQt6 import QtCore,QtWidgets from rascal2.widgets.project.tables import ( - ParametersModel + ParametersModel, + ProjectFieldWidget, + ParameterFieldWidget ) @@ -41,6 +43,7 @@ def __init__(self, parent): self._sliders = {} # dictionary of the sliders used to display fittable values # create initial slider view layout and everything else which depends on it + self.init() @@ -128,8 +131,17 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: if isinstance(vis_model, ParametersModel): row = 0 for model_param in vis_model.classlist: - if model_param.fit: - trial_properties[model_param.name] = SliderChangeHolder(row_number=row,model=vis_model,param=model_param) + if hasattr(model_param,"fit") and model_param.fit: # Parameters model should probably always have fit attribute, but let's be on the safe side. + slider_info = SliderChangeHolder(row_number=row,model=vis_model,param=model_param) + trial_properties[model_param.name] = slider_info + this_prop_change_delegates = ProjectFieldWidget.tables_changed_delegate_for_sliders[param.objectName()] + # connect delegates which propagate parameters changed in tables to correspondant sliders + for key,delegate_list in this_prop_change_delegates.items(): + for delegate in delegate_list: + delegate.editingFinished_InformSliders.connect( + lambda index,field, slider_name = model_param.name: + self._table_edit_finished_change_slider(index,field,slider_name) + ) if model_param.name in self._prop_to_change: n_updated_properties += 1 row += 1 @@ -146,6 +158,23 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: return update_properties + def _table_edit_finished_change_slider(self, index , field_name : str,slider_name : str) -> None: + # Method to bind with tables delegates and change slider appearance accordingly. + # + # Signal about slider parameters changed is sent to sliders widget last after all other signals on + # table parameters have been processed. + # At this stage, rascal properties have already been modified, so we just modify appropriate slider appearance + # index -- QtCore.Table index of appropriate rascal property in correspondent GUI table. Duplicates slider name here. + # field_name + # -- string indicating changed min/max/value fields of property. May be used later to optimize changes + # but benefit of that is minuscules. + # slider_name + # -- name of the property, slider describes and key, which defines slider position in the dictionary + # of sliders + if self.isVisible(): # Do not bother otherwise, as slider appearance will be modified when made visible and + # slider itself may have even been deleted + self._sliders[slider_name].update_slider_display_from_property(in_constructor=False) + def _create_slider_view_layout(self) -> None: """ Create sliders layout with all necessary controls and connections but without sliders themselves. @@ -155,6 +184,7 @@ def _create_slider_view_layout(self) -> None: accept_button = QtWidgets.QPushButton("Accept", self, objectName="AcceptButton") accept_button.clicked.connect(self._apply_changes_from_sliders) + cancel_button = QtWidgets.QPushButton("Cancel", self, objectName="CancelButton") cancel_button.clicked.connect(self._cancel_changes_from_sliders) @@ -211,10 +241,6 @@ def _update_sliders_widgets(self) -> None: for name,prop in self._prop_to_change.items(): self._sliders[name].update_slider_parameters(prop) - def show_sliders_view(self) -> None: - """Show project view""" - self._parent.show_or_hide_sliders(do_show_sliders=True) - def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders and hide sliders view. @@ -262,8 +288,6 @@ def __init__(self, row_number: int,model : ParametersModel, param : ratapi.model self.param = param self._vis_model = model self._row_number = row_number - # auxiliary helper variable - self._param_value = param.value @property def name(self): @@ -271,10 +295,9 @@ def name(self): @property def value(self) -> float: - return self._param_value + return self.param.value @value.setter def value(self, value: float) -> None: - self._param_value = value setattr(self.param,"value",value) def update_value_representation(self,val : float, recalculate_project = True) -> None: @@ -288,10 +311,6 @@ def update_value_representation(self,val : float, recalculate_project = True) -> # 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) - # this is probably unnecessary, as have been already set through the model. But it's fast and much more - # convenient for testing - self.param.value = val - class LabeledSlider(QtWidgets.QFrame): """ Class describes slider widget which @@ -300,8 +319,9 @@ class LabeledSlider(QtWidgets.QFrame): """ # Instance attributes generator # Defaults for property min/max. Will be overwritten from actual input property - _value_min: float | None = 0 # minimal value property may have + _value_min: float | None = 0 # minimal value property may have _value_max: float | None = 100 # maximal value property may have + _value: float | None = 50 # cache for property value _value_range: float | None = 100 # value range (difference between maximal and minimal values of the property) _value_step: float | None = 1 # the change in property value per single step slider move @@ -329,7 +349,7 @@ def __init__(self, param: SliderChangeHolder): if param is None: return - self.slider_name = param.name # name the slider as the property it refers to + 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 @@ -386,10 +406,15 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F # 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 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: # Characteristics of the property value to display - self._value_min = self._prop.param.min - self._value_max = self._prop.param.max + 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 @@ -397,12 +422,28 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F if in_constructor: return # otherwise, update slider's labels - self._value_label.setText(self._value_label_format.format(param.value)) + self._value_label.setText(self._value_label_format.format(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. + Update them if they are and return True. False if they have not been changed. + """ + 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.values + updated = True + return updated + def _value_to_slider_pos(self, value: float) -> int: """Convert double property value into slider position""" return int(round(self._slider_max_idx*(value-self._value_min)/self._value_range,0)) From 2cafa034f03f015c65aeac09d4ef976da8155a27 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 10 Nov 2025 16:48:13 +0000 Subject: [PATCH 29/69] Re #148 Properly established connection between table and sliders --- rascal2/widgets/sliders_view.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 799a7d13..63dba45e 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -325,8 +325,6 @@ class LabeledSlider(QtWidgets.QFrame): _value_range: float | None = 100 # value range (difference between maximal and minimal values of the property) _value_step: float | None = 1 # the change in property value per single step slider move - _labels: list = [] # list of slider labels describing sliders axis - # Class attributes of slider widget which usually remain the same for all classes. Do not override unless in __init__ # method _num_slider_ticks: int = 10 @@ -348,6 +346,7 @@ def __init__(self, param: SliderChangeHolder): self._prop = param # hold the property controlled by slider if param is None: return + self._labels = [] # list of slider labels describing sliders axis 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 @@ -357,7 +356,7 @@ def __init__(self, param: SliderChangeHolder): # 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(param.value), alignment=QtCore.Qt.AlignmentFlag.AlignRight) + 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) @@ -370,7 +369,7 @@ def __init__(self, param: SliderChangeHolder): 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 + 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) @@ -406,12 +405,14 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F # 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 + # 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: - # Characteristics of the property value to display + """Change internal sliders parameters and their representation in GUI + if internal sliders parameters have changed + """ if not (self._updated_from_rascal_property() or in_constructor): return @@ -422,7 +423,7 @@ def update_slider_display_from_property(self,in_constructor: bool) -> None: if in_constructor: return # otherwise, update slider's labels - self._value_label.setText(self._value_label_format.format(self._value)) + self.set_slider_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 @@ -440,7 +441,7 @@ def _updated_from_rascal_property(self) -> bool: self._value_max = self._prop.param.max updated = True if self._value != self._prop.param.value: - self._value = self._prop.param.values + self._value = self._prop.param.value updated = True return updated From d730b89120d0298f2b2ac2d645e13016b27757b3 Mon Sep 17 00:00:00 2001 From: abuts Date: Mon, 10 Nov 2025 20:30:37 +0000 Subject: [PATCH 30/69] Re #148 Tests for LabeledSlider --- rascal2/widgets/sliders_view.py | 25 ++++++++----- tests/widgets/test_sliders_widget.py | 56 ++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 63dba45e..0b36bc27 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -135,7 +135,7 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: slider_info = SliderChangeHolder(row_number=row,model=vis_model,param=model_param) trial_properties[model_param.name] = slider_info this_prop_change_delegates = ProjectFieldWidget.tables_changed_delegate_for_sliders[param.objectName()] - # connect delegates which propagate parameters changed in tables to correspondant sliders + # connect delegates which propagate parameters changed in tables to correspondent sliders for key,delegate_list in this_prop_change_delegates.items(): for delegate in delegate_list: delegate.editingFinished_InformSliders.connect( @@ -252,7 +252,7 @@ def _cancel_changes_from_sliders(self): for key,val in self._values_to_revert.items(): - self._sliders[key].set_slider_position(val) + self._sliders[key].set_slider_gui_position(val) if key == last_key: self._prop_to_change[key].update_value_representation(val,recalculate_project=True) else: @@ -315,7 +315,9 @@ def update_value_representation(self,val : float, recalculate_project = True) -> class LabeledSlider(QtWidgets.QFrame): """ Class describes slider widget which allows modifying rascal property value and its representation - in project table view + in project table view. + It also connects with table view and accepts changes in min/max/value + obtained from property. """ # Instance attributes generator # Defaults for property min/max. Will be overwritten from actual input property @@ -330,8 +332,8 @@ class LabeledSlider(QtWidgets.QFrame): _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 is not very accurate. - _tick_label_format: str = "{:.2g}" # format to display numbers under the 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): @@ -393,7 +395,7 @@ def __init__(self, param: SliderChangeHolder): self.setFrameShape(QtWidgets.QFrame.Shape.Box) self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - def set_slider_position(self,value : float) -> None: + def set_slider_gui_position(self,value : float) -> None: """Set specified slider GUI position programmatically """ idx = self._value_to_slider_pos(value) self._slider.setValue(idx) @@ -411,8 +413,10 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F def update_slider_display_from_property(self,in_constructor: bool) -> None: """Change internal sliders parameters and their representation in GUI - if internal sliders parameters have changed + if property, underlying sliders parameters have changed. """ + # 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 @@ -423,7 +427,7 @@ def update_slider_display_from_property(self,in_constructor: bool) -> None: if in_constructor: return # otherwise, update slider's labels - self.set_slider_position(self._value) + 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 @@ -471,8 +475,11 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: def _update_value(self, idx: int)->None: 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 setters above + self._prop.param.value = val # but fast and nice for tests class EmptySlider(LabeledSlider): @@ -493,7 +500,7 @@ def __init__(self): self.slider_name = "Empty Slider" self.setObjectName(self.slider_name) - def set_slider_position(self,value : float) -> None: + def set_slider_gui_position(self,value : float) -> None: return def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 39ac3cbe..2d6bde4d 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -53,9 +53,9 @@ def slider(): parent = QtWidgets.QWidget() class_view = ratapi.ClassList( [ - DataModel(name="A", min = 0,value=1,max=100,fit=True,show_priors = False), - DataModel(name="B", min = 0, value=6,max = 200,fit=True,show_priors = False), - DataModel(name="C", min = 0,value=18,max=300,fit=True,show_priors = False) + DataModel(name="Slider_A", min = 0,value=1,max=100,fit=True,show_priors = False), + DataModel(name="Slider_B", min = 0, value=6,max = 200,fit=True,show_priors = False), + DataModel(name="Slider_C", min = 0,value=18,max=300,fit=True,show_priors = False) ] ) model = ParametersModelMock(class_view, parent) @@ -68,6 +68,7 @@ def test_a_slider_construction(slider): 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 @@ -81,4 +82,51 @@ 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 From df8259ebb3901914e6224cd5d0560baf26ad5532 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 11 Nov 2025 12:19:23 +0000 Subject: [PATCH 31/69] Re #148 formal changes. Formally separated test for slider class and sliders widget (currently empty) Use set instead of list to collect editing delegates. Does not make any difference. --- rascal2/widgets/project/tables.py | 4 +- rascal2/widgets/sliders_view.py | 32 ++--- tests/widgets/test_labeled_slider_class.py | 129 +++++++++++++++++++++ tests/widgets/test_sliders_widget.py | 69 ----------- 4 files changed, 148 insertions(+), 86 deletions(-) create mode 100644 tests/widgets/test_labeled_slider_class.py diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index d2933755..863302b5 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -191,9 +191,9 @@ def add_sliders_delegate(cls,table_name: str, var_name: str, delegate) -> None: slider_params = cls.tables_changed_delegate_for_sliders[table_name] # why do we need to talk to all delegates and can not emit proper signals from a global class method? if var_name not in slider_params.keys(): - slider_params[var_name] = [delegate] + slider_params[var_name] = {delegate} else: - slider_params[var_name].append(delegate) + slider_params[var_name].add(delegate) cls.tables_changed_delegate_for_sliders[table_name] = slider_params def __init__(self, field: str, parent): diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 0b36bc27..ac0e2f4f 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -46,7 +46,6 @@ def __init__(self, parent): self.init() - def show(self): """Overload parent show method to deal with mdi container showing sliders widget window. Also sets up or updates sliders @@ -136,8 +135,8 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: trial_properties[model_param.name] = slider_info this_prop_change_delegates = ProjectFieldWidget.tables_changed_delegate_for_sliders[param.objectName()] # connect delegates which propagate parameters changed in tables to correspondent sliders - for key,delegate_list in this_prop_change_delegates.items(): - for delegate in delegate_list: + for key,delegates_set in this_prop_change_delegates.items(): + for delegate in delegates_set: delegate.editingFinished_InformSliders.connect( lambda index,field, slider_name = model_param.name: self._table_edit_finished_change_slider(index,field,slider_name) @@ -264,12 +263,11 @@ def _apply_changes_from_sliders(self) -> None: """Apply changes obtained from sliders to the project and make them permanent """ - for key,prop in self._prop_to_change.items(): - self._values_to_revert[key] = prop.value - + # Changes have already been applied so just hide sliders widget self._parent.show_or_hide_sliders(False) return + #======================================================================================================================= class SliderChangeHolder: """ Helper class containing information necessary for update @@ -312,6 +310,7 @@ def update_value_representation(self,val : float, recalculate_project = True) -> 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 @@ -396,7 +395,8 @@ def __init__(self, param: SliderChangeHolder): self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) def set_slider_gui_position(self,value : float) -> None: - """Set specified slider GUI position programmatically """ + """Set specified slider GUI position programmatically + """ idx = self._value_to_slider_pos(value) self._slider.setValue(idx) self._value_label.setText(self._value_label_format.format(value)) @@ -414,6 +414,8 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F 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. """ # note the order of methods in comparison. Should be as here, as may break # property updates in constructor otherwise. @@ -450,18 +452,20 @@ def _updated_from_rascal_property(self) -> bool: return updated def _value_to_slider_pos(self, value: float) -> int: - """Convert double property value into slider position""" + """Convert double value into slider position""" 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 double property value into slider position""" + """Convert slider GUI position (index) into double value""" value = self._value_min + index*self._value_step - if value > self._value_max: # This should not happen but may due to round-off errors + 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 """ + """Construct slider widget with integer scales and ticks in integer positions + Part of slider constructor + """ slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) slider.setMinimum(0) @@ -474,6 +478,7 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: return slider def _update_value(self, idx: int)->None: + """ Bound in constructor to GUI slider position changed event""" val = self._slider_pos_to_value(idx) self._value = val self._value_label.setText(self._value_label_format.format(val)) @@ -506,8 +511,5 @@ def set_slider_gui_position(self,value : float) -> None: def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): return - def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: - return None - - def _update_value(self, idx: int)->None: + def update_slider_display_from_property(self,in_constructor: bool) -> None: return diff --git a/tests/widgets/test_labeled_slider_class.py b/tests/widgets/test_labeled_slider_class.py new file mode 100644 index 00000000..9268ea38 --- /dev/null +++ b/tests/widgets/test_labeled_slider_class.py @@ -0,0 +1,129 @@ +from unittest.mock import MagicMock, patch + +import pydantic +import pytest +import ratapi + +from PyQt6 import QtWidgets, QtCore + +from rascal2.widgets.sliders_view import ( + SliderChangeHolder, + LabeledSlider +) +from rascal2.widgets.project.tables import ( + ParametersModel +) + + +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), + ] + ) + model = ParametersModelMock(class_view, parent) + 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 index 2d6bde4d..1e4f46fe 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -61,72 +61,3 @@ def slider(): model = ParametersModelMock(class_view, parent) 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 From 6d122972127425707fafca7ce1e2fa391a7e4490 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 11 Nov 2025 16:00:55 +0000 Subject: [PATCH 32/69] Re #148 Test changes in delegates and TableViewModels related to sliders (change in table_model is reflected by sliders) --- tests/widgets/project/test_models.py | 65 +++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 22ddc14d..90b5dd62 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -254,19 +254,72 @@ def test_parameter_flags(param_model, prior_type, protected): assert item_flags & QtCore.Qt.ItemFlag.ItemIsEditable -def test_param_item_delegates(param_classlist): - """Test that parameter models have the expected item delegates.""" +@pytest.fixture +def widget_with_delegates(): widget = ParameterFieldWidget("Test", parent) widget.parent = MagicMock() - widget.update_model(param_classlist([])) - for column, header in enumerate(widget.model.headers, start=1): + 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): if header in ["min", "value", "max"]: - assert isinstance(widget.table.itemDelegateForColumn(column), delegates.ValueSpinBoxDelegate) + assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), delegates.ValueSpinBoxDelegate) else: - assert isinstance(widget.table.itemDelegateForColumn(column), delegates.ValidatedInputDelegate) + assert isinstance(widget_with_delegates.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 """ + + assert len(widget_with_delegates.tables_changed_delegate_for_sliders) == 1 + assert list(widget_with_delegates.tables_changed_delegate_for_sliders.keys()) == ["Test"] + delegates_set = widget_with_delegates.tables_changed_delegate_for_sliders["Test"] + assert isinstance(delegates_set,dict) + assert list(delegates_set.keys()) == ["min","value","max"] + for delegate in delegates_set.values(): + assert isinstance(list(delegate)[0],delegates.ValueSpinBoxDelegate) +class fake_editor(object): + """A class with have only one method providing value. Used in test below""" + def value(self): + return 42 + +class MockReceiver(object): + """Test object which receives signals sent to slider """ + def __init__(self): + self.cache_state = [] + self.call_count = 0 + + def receive_signal(self,index,value): + self.call_count += 1 + self.cache_state = (index,value) + +def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): + """Test if edit_finished signals emitted to subscribing sliders """ + sr = MockReceiver() + + delegates_set = widget_with_delegates.tables_changed_delegate_for_sliders["Test"] + for delegate in delegates_set.values(): + the_delegate = list(delegate)[0] + the_delegate.editingFinished_InformSliders.connect(lambda idx,tab_name : sr.receive_signal(idx,tab_name)) + + index = widget_with_delegates.model.index(1,1) + fed = fake_editor() + n_calls = 0 + for col_name,delegate in delegates_set.items(): + the_delegate = list(delegate)[0] + the_delegate.setModelData(fed,widget_with_delegates.model, index) + n_calls += 1 + assert sr.call_count == n_calls + assert sr.cache_state == (index,col_name) + def test_hidden_bayesian_columns(param_classlist): """Test that Bayes columns are hidden when procedure is not Bayesian.""" widget = ParameterFieldWidget("Test", parent) From 06d2d5c4815074ca17505201a30391163c0dcc00 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 11 Nov 2025 18:22:12 +0000 Subject: [PATCH 33/69] Re #148 Better access to column delegates --- rascal2/widgets/project/tables.py | 39 ++++++-------------- rascal2/widgets/sliders_view.py | 17 +++++---- tests/widgets/project/test_models.py | 53 +++++++++++++--------------- 3 files changed, 46 insertions(+), 63 deletions(-) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 863302b5..63079666 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -167,35 +167,11 @@ class ProjectFieldWidget(QtWidgets.QWidget): """ classlist_model = ClassListTableModel - # Expose delegates which react on changes in a table to sliders which depend on these change - tables_changed_delegate_for_sliders = {} # set it in the set_delegates method # the model can change and disconnect, so we re-connect it # to a signal here on each change edited = QtCore.pyqtSignal() - - @classmethod - def add_sliders_delegate(cls,table_name: str, var_name: str, delegate) -> None: - """Add the delegate responsible for editing the fields which are reflected by sliders to - the dictionary of delegates exposed to sliders widget - Inputs: - table_name : str -- the name of the table the delegate is for - var_name : str -- the name of variable exposed to sliders (min,max, value) - delegate : ValueSpinBox or ValidatedInputDelegate - -- the reference to the appropriate delegate itself - """ - if table_name not in cls.tables_changed_delegate_for_sliders.keys(): - slider_params = {} - else: - slider_params = cls.tables_changed_delegate_for_sliders[table_name] - # why do we need to talk to all delegates and can not emit proper signals from a global class method? - if var_name not in slider_params.keys(): - slider_params[var_name] = {delegate} - else: - slider_params[var_name].add(delegate) - cls.tables_changed_delegate_for_sliders[table_name] = slider_params - def __init__(self, field: str, parent): super().__init__(parent) self.field = field @@ -282,9 +258,18 @@ def set_item_delegates(self): i + self.model.col_offset, delegate ) - if header in ["min", "value", "max"]: - ProjectFieldWidget.add_sliders_delegate(self.field, header, 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 def append_item(self): """Append an item to the model if the model exists.""" @@ -390,8 +375,6 @@ def set_item_delegates(self): if header in ["min", "value", "max"]: delegate = delegates.ValueSpinBoxDelegate(header, self.table) self.table.setItemDelegateForColumn(i + 1, delegate) - - ProjectFieldWidget.add_sliders_delegate(self.field, header, delegate) else: self.table.setItemDelegateForColumn( i + 1, delegates.ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index ac0e2f4f..cb622cde 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -124,6 +124,7 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: n_updated_properties = 0 trial_properties = {} + for widget in proj.view_tabs.values(): for param in widget.tables.values(): vis_model = param.model @@ -133,14 +134,16 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: if hasattr(model_param,"fit") and model_param.fit: # Parameters model should probably always have fit attribute, but let's be on the safe side. slider_info = SliderChangeHolder(row_number=row,model=vis_model,param=model_param) trial_properties[model_param.name] = slider_info - this_prop_change_delegates = ProjectFieldWidget.tables_changed_delegate_for_sliders[param.objectName()] + this_prop_change_delegates = param.get_item_delegates(["min","max","value"]) # connect delegates which propagate parameters changed in tables to correspondent sliders - for key,delegates_set in this_prop_change_delegates.items(): - for delegate in delegates_set: - delegate.editingFinished_InformSliders.connect( - lambda index,field, slider_name = model_param.name: - self._table_edit_finished_change_slider(index,field,slider_name) - ) + # Can be improved by using item index as these delegates emit "edited" signal for the whole + # column, but the signal contains the row index + for delegate in this_prop_change_delegates: + delegate.editingFinished_InformSliders.connect( + lambda index,field, slider_name = model_param.name: + self._table_edit_finished_change_slider(index,field,slider_name) + ) + if model_param.name in self._prop_to_change: n_updated_properties += 1 row += 1 diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 90b5dd62..683c1c8c 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -256,6 +256,7 @@ def test_parameter_flags(param_model, prior_type, protected): @pytest.fixture def widget_with_delegates(): + widget = ParameterFieldWidget("Test", parent) widget.parent = MagicMock() @@ -277,13 +278,11 @@ def test_param_item_delegates(widget_with_delegates): def test_param_item_delegates_exposed_to_sliders(widget_with_delegates): """Test that parameter models provides the item delegates related to slides """ - assert len(widget_with_delegates.tables_changed_delegate_for_sliders) == 1 - assert list(widget_with_delegates.tables_changed_delegate_for_sliders.keys()) == ["Test"] - delegates_set = widget_with_delegates.tables_changed_delegate_for_sliders["Test"] - assert isinstance(delegates_set,dict) - assert list(delegates_set.keys()) == ["min","value","max"] - for delegate in delegates_set.values(): - assert isinstance(list(delegate)[0],delegates.ValueSpinBoxDelegate) + 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) class fake_editor(object): @@ -302,44 +301,42 @@ def receive_signal(self,index,value): self.cache_state = (index,value) def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): - """Test if edit_finished signals emitted to subscribing sliders """ + """Test if edit_finished signals emitted to subscribed clients""" sr = MockReceiver() + selected_fields = ["min","value","max"] - delegates_set = widget_with_delegates.tables_changed_delegate_for_sliders["Test"] - for delegate in delegates_set.values(): - the_delegate = list(delegate)[0] - the_delegate.editingFinished_InformSliders.connect(lambda idx,tab_name : sr.receive_signal(idx,tab_name)) + # Expected order of delegates in the property should be is as in the list. + delegates_list = widget_with_delegates.get_item_delegates(selected_fields) + for delegate in delegates_list: + delegate.editingFinished_InformSliders.connect(lambda idx,tab_name : sr.receive_signal(idx,tab_name)) index = widget_with_delegates.model.index(1,1) fed = fake_editor() n_calls = 0 - for col_name,delegate in delegates_set.items(): - the_delegate = list(delegate)[0] - the_delegate.setModelData(fed,widget_with_delegates.model, index) + for delegate,field_name in zip(delegates_list,selected_fields): + delegate.setModelData(fed,widget_with_delegates.model, index) n_calls += 1 assert sr.call_count == n_calls - assert sr.cache_state == (index,col_name) + assert sr.cache_state == (index,field_name) -def test_hidden_bayesian_columns(param_classlist): +def test_hidden_bayesian_columns(widget_with_delegates): """Test that Bayes columns are hidden when procedure is not Bayesian.""" - widget = ParameterFieldWidget("Test", parent) - widget.parent = MagicMock() - widget.update_model(param_classlist([])) - mock_controls = widget.parent.parent.parent_model.controls = MagicMock() + + mock_controls = widget_with_delegates.parent.parent.parent_model.controls = MagicMock() mock_controls.procedure = "calculate" bayesian_columns = ["prior_type", "mu", "sigma"] - widget.handle_bayesian_columns("calculate") + widget_with_delegates.handle_bayesian_columns("calculate") for item in bayesian_columns: - index = widget.model.headers.index(item) - assert widget.table.isColumnHidden(index + 1) + index = widget_with_delegates.model.headers.index(item) + assert widget_with_delegates.table.isColumnHidden(index + 1) - widget.handle_bayesian_columns("dream") + widget_with_delegates.handle_bayesian_columns("dream") for item in bayesian_columns: - index = widget.model.headers.index(item) - assert not widget.table.isColumnHidden(index + 1) + index = widget_with_delegates.model.headers.index(item) + assert not widget_with_delegates.table.isColumnHidden(index + 1) def test_layer_model_init(): @@ -436,7 +433,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): From bf28e3d2dea0fe40bd14886c9ffda6414bc491be Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 12 Nov 2025 21:44:16 +0000 Subject: [PATCH 34/69] Re #148 first most annoying unit test for sliders widget --- rascal2/widgets/sliders_view.py | 85 +++++++++++++++++----------- tests/widgets/project/test_models.py | 1 + tests/widgets/test_sliders_widget.py | 80 ++++++++++++-------------- 3 files changed, 88 insertions(+), 78 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index cb622cde..8e4e45ba 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -41,7 +41,7 @@ def __init__(self, parent): # 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._sliders = {} # dictionary of the sliders used to display fitable values # create initial slider view layout and everything else which depends on it self.init() @@ -49,10 +49,10 @@ def __init__(self, parent): def show(self): """Overload parent show method to deal with mdi container showing sliders widget window. Also sets up or updates sliders - widget list depending on previous state of the widget + widget list depending on previous state of the widget. """ - # avoid running init view more than once if sliders are visible anyway + # avoid running init view more than once if sliders are visible. if self.isVisible(): return @@ -93,30 +93,38 @@ def init(self) -> None: self._create_slider_view_layout() proj = self._parent.presenter.model.project - update_sliders = self._init_properties_for_sliders(proj) + if proj is None: + return # Project may be not initialized at all + + 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,project : ratapi.Project) -> bool: - """Take project and copy all properties which have attribute "Fit" == True + def _init_properties_for_sliders(self) -> bool: + """Loop through project's widget view tabs and morels associated with them. + Select all ParametersModel 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. Input: ------ - project: ratapi.Project -- project to get properties to change + Picks up project: self._parent.project_widget -- project to get properties to change Returns: -------- update_properties -- true if all properties in the project have already had sliders, generated for them 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. """ - if project is None: - return False proj = self._parent.project_widget if proj is None: @@ -126,53 +134,62 @@ def _init_properties_for_sliders(self,project : ratapi.Project) -> bool: trial_properties = {} for widget in proj.view_tabs.values(): - for param in widget.tables.values(): - vis_model = param.model - if isinstance(vis_model, ParametersModel): - row = 0 - for model_param in vis_model.classlist: - if hasattr(model_param,"fit") and model_param.fit: # Parameters model should probably always have fit attribute, but let's be on the safe side. - slider_info = SliderChangeHolder(row_number=row,model=vis_model,param=model_param) - trial_properties[model_param.name] = slider_info - this_prop_change_delegates = param.get_item_delegates(["min","max","value"]) - # connect delegates which propagate parameters changed in tables to correspondent sliders - # Can be improved by using item index as these delegates emit "edited" signal for the whole - # column, but the signal contains the row index - for delegate in this_prop_change_delegates: - delegate.editingFinished_InformSliders.connect( - lambda index,field, slider_name = model_param.name: - self._table_edit_finished_change_slider(index,field,slider_name) - ) - - if model_param.name in self._prop_to_change: - n_updated_properties += 1 - row += 1 + 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 + row = 0 + for model_param in data_model.classlist: + if hasattr(model_param,"fit") and model_param.fit: # Parameters model should always + # have fit attribute, but let's be on the safe side. + # 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. + slider_info = SliderChangeHolder(row_number=row,model=data_model,param=model_param) + trial_properties[model_param.name] = slider_info + # Connect delegates which propagate parameters changed in tables to correspondent sliders. + # Can be improved by using item index as these delegates emit "edited" signal for the whole + # column, but row index is presented in signal itself. + this_prop_change_delegates = table_view.get_item_delegates(["min","max","value"]) + for delegate in this_prop_change_delegates: + delegate.editingFinished_InformSliders.connect( + lambda index,field, slider_name = model_param.name: + self._table_edit_finished_change_slider(index,field,slider_name) + ) + + if model_param.name in self._prop_to_change: + n_updated_properties += 1 + row += 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 - # store information about sliders + # store information about sliders properties self._prop_to_change = trial_properties - # remember values for properties controlled by sliders in case if you may want to revert them later + # 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 _table_edit_finished_change_slider(self, index , field_name : str,slider_name : str) -> None: - # Method to bind with tables delegates and change slider appearance accordingly. + """ Method to bind with tables delegates and change slider appearance accordingly to changes in tables. # # Signal about slider parameters changed is sent to sliders widget last after all other signals on # table parameters have been processed. # At this stage, rascal properties have already been modified, so we just modify appropriate slider appearance - # index -- QtCore.Table index of appropriate rascal property in correspondent GUI table. Duplicates slider name here. + # index -- QtCore.QtTable index of appropriate rascal property in correspondent GUI table. + # Duplicates slider name here so is not currently used. # field_name # -- string indicating changed min/max/value fields of property. May be used later to optimize changes # but benefit of that is minuscules. # slider_name # -- name of the property, slider describes and key, which defines slider position in the dictionary # of sliders + """ if self.isVisible(): # Do not bother otherwise, as slider appearance will be modified when made visible and # slider itself may have even been deleted self._sliders[slider_name].update_slider_display_from_property(in_constructor=False) diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 683c1c8c..a5da729a 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -297,6 +297,7 @@ def __init__(self): self.call_count = 0 def receive_signal(self,index,value): + """To bind to delegate signal""" self.call_count += 1 self.cache_state = (index,value) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 1e4f46fe..99b1d583 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -3,61 +3,53 @@ import pydantic import pytest import ratapi +from rascal2.ui.view import MainWindowView from PyQt6 import QtWidgets, QtCore +from rascal2.widgets.project.project import create_draft_project from rascal2.widgets.sliders_view import ( SlidersViewWidget, SliderChangeHolder, LabeledSlider ) from rascal2.widgets.project.tables import ( + ParameterFieldWidget, ParametersModel ) - -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( +class MockFigureCanvas(QtWidgets.QWidget): + """A mock figure canvas.""" + + def draw(*args, **kwargs): + pass + +#@pytest.fixture +def test_view_with_project(): + """An instance of MainWindowView with mdi property defined to some rubbish + for mimicking operations performed in MainWindowView.reset_mdi_layout + """ + #with patch("rascal2.widgets.plot.FigureCanvas", return_value=MockFigureCanvas()): + mw = MainWindowView() + + draft = create_draft_project(ratapi.Project()) + draft["parameters"] = ratapi.ClassList( [ - DataModel(name="Slider_A", min = 0,value=1,max=100,fit=True,show_priors = False), - DataModel(name="Slider_B", min = 0, value=6,max = 200,fit=True,show_priors = False), - DataModel(name="Slider_C", min = 0,value=18,max=300,fit=True,show_priors = False) + 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) ] ) - model = ParametersModelMock(class_view, parent) - inputs = SliderChangeHolder(row_number=2,model=model, param=param) - return LabeledSlider(inputs) + mw.project_widget.view_tabs["Parameters"].update_model(draft) + #yield mw + return mw + + +def test_extract_properties_for_sliders(): + tw = test_view_with_project() + update_sliders = tw.sliders_view_widget._init_properties_for_sliders() + assert update_sliders == False # its false as at first call sliders should be regenerated + assert len(tw.sliders_view_widget._prop_to_change) == 2 + assert list(tw.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] + assert list(tw.sliders_view_widget._values_to_revert.values()) == [2.1, 209.] + assert tw.sliders_view_widget._init_properties_for_sliders() # not its true as sliders should be available + # for update on second call From d4d795c32499df9c7607c12a1925ab69ab792a16 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 13:56:43 +0000 Subject: [PATCH 35/69] Re #148 more unit tests for SlidersViewWidget and couple of important bugs introduced yesterday fixed --- rascal2/widgets/sliders_view.py | 33 +++++--- tests/widgets/test_labeled_slider_class.py | 3 + tests/widgets/test_sliders_widget.py | 93 +++++++++++++++++----- 3 files changed, 96 insertions(+), 33 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 8e4e45ba..8abfa752 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -92,9 +92,8 @@ def init(self) -> None: if self.findChild(QtWidgets.QWidget,'AcceptButton') is None: self._create_slider_view_layout() - proj = self._parent.presenter.model.project - if proj is None: - return # Project may be not initialized at all + 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: @@ -161,7 +160,9 @@ def _init_properties_for_sliders(self) -> bool: if model_param.name in self._prop_to_change: n_updated_properties += 1 - row += 1 + + row += 1 # Calculate table rows regardless model has fit== True or not + # if all properties of trial dictionary are in existing dictionary and the number of properties are the same # no new/deleted sliders have appeared. @@ -264,18 +265,16 @@ def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders and hide sliders view. """ - if len(self._values_to_revert) == 0: - last_key = None - else: # does not work with empty dictionary - last_key = next(reversed(self._values_to_revert)) - - + last_call = len(self._values_to_revert)-1 + call_cnt = 0 for key,val in self._values_to_revert.items(): - self._sliders[key].set_slider_gui_position(val) - if key == last_key: + if call_cnt == last_call: # its important to update project at last call self._prop_to_change[key].update_value_representation(val,recalculate_project=True) else: self._prop_to_change[key].update_value_representation(val,recalculate_project=False) + self._sliders[key].set_slider_gui_position(val) + + call_cnt += 1 self._parent.show_or_hide_sliders(do_show_sliders=False) @@ -368,6 +367,7 @@ def __init__(self, param: SliderChangeHolder): 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 @@ -416,9 +416,16 @@ def __init__(self, param: SliderChangeHolder): 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 """ + self._value = 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 + self._value_label.setText(self._value_label_format.format(value)) def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): @@ -499,6 +506,8 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: def _update_value(self, idx: int)->None: """ Bound in constructor to GUI slider position changed event""" + 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)) diff --git a/tests/widgets/test_labeled_slider_class.py b/tests/widgets/test_labeled_slider_class.py index 9268ea38..2e6c4134 100644 --- a/tests/widgets/test_labeled_slider_class.py +++ b/tests/widgets/test_labeled_slider_class.py @@ -53,9 +53,12 @@ def slider(): 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 for row_number == 2 to work inputs = SliderChangeHolder(row_number=2,model=model, param=param) return LabeledSlider(inputs) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 99b1d583..998520a0 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -3,9 +3,11 @@ import pydantic import pytest import ratapi +from matplotlib.widgets import Slider + from rascal2.ui.view import MainWindowView -from PyQt6 import QtWidgets, QtCore +from PyQt6 import QtWidgets, QtCore,QtGui from rascal2.widgets.project.project import create_draft_project from rascal2.widgets.sliders_view import ( @@ -23,33 +25,82 @@ class MockFigureCanvas(QtWidgets.QWidget): def draw(*args, **kwargs): pass -#@pytest.fixture -def test_view_with_project(): - """An instance of MainWindowView with mdi property defined to some rubbish - for mimicking operations performed in MainWindowView.reset_mdi_layout +@pytest.fixture +def view_with_proj(): + """An instance of MainWindowView with project partially defined + for mimicking sliders generation from project tabs """ - #with patch("rascal2.widgets.plot.FigureCanvas", return_value=MockFigureCanvas()): 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 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) + ] + ) + draft["background_parameters"] = ratapi.ClassList( + [ratapi.models.Parameter(name="Background Param 1", min=0, max=1, value=0.2, fit=False),] ) mw.project_widget.view_tabs["Parameters"].update_model(draft) - #yield mw - return mw + mw.presenter.model.project = 1 # fake project to fool checks for project presence in GUI + # without it normal project GUI will not be defined properly. Not used by SlidersWidget except + # to check that project GUI is defined, assuming that if project is defined its GUI is also defined + yield mw -def test_extract_properties_for_sliders(): - tw = test_view_with_project() - update_sliders = tw.sliders_view_widget._init_properties_for_sliders() +def test_extract_properties_for_sliders(view_with_proj): + + update_sliders = view_with_proj.sliders_view_widget._init_properties_for_sliders() assert update_sliders == False # its false as at first call sliders should be regenerated - assert len(tw.sliders_view_widget._prop_to_change) == 2 - assert list(tw.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] - assert list(tw.sliders_view_widget._values_to_revert.values()) == [2.1, 209.] - assert tw.sliders_view_widget._init_properties_for_sliders() # not its true as sliders should be available - # for update on second call + assert len(view_with_proj.sliders_view_widget._prop_to_change) == 2 + assert list(view_with_proj.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] + assert list(view_with_proj.sliders_view_widget._values_to_revert.values()) == [2.1, 209.] + 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) == 2 + assert "Param 1" in view_with_proj.sliders_view_widget._sliders + assert "Param 3" 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"] + 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 + + +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 = [] + +#@patch("rascal2.widgets.project.tables.ParameterFieldWidget.update_project") +@patch.object(ParameterFieldWidget,"update_project",fake_update) +def test_cancel_button_called(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 + cancel_button = view_with_proj.sliders_view_widget.findChild(QtWidgets.QPushButton,"CancelButton") + + cancel_button.click() + + assert fake_update.num_calls == 2 + assert fake_update.project_updated == [False, True] \ No newline at end of file From 90f1f369bcf9624c578c21c9bf1b7fb131148e34 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 16:08:15 +0000 Subject: [PATCH 36/69] Re #148 Final tests for sliders widget --- rascal2/widgets/sliders_view.py | 11 +-- tests/widgets/test_labeled_slider_class.py | 2 +- tests/widgets/test_sliders_widget.py | 86 +++++++++++++++++----- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 8abfa752..7de259fd 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -1,14 +1,11 @@ """Widget for the Sliders View window.""" -from copy import deepcopy import ratapi.models from PyQt6 import QtCore,QtWidgets from rascal2.widgets.project.tables import ( - ParametersModel, - ProjectFieldWidget, - ParameterFieldWidget + ParametersModel ) @@ -420,14 +417,13 @@ def set_slider_gui_position(self,value : float) -> None: for change, associated with slider position change in GUI """ 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 - self._value_label.setText(self._value_label_format.format(value)) - def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): """Modifies slider values which may change for this slider from his parent property""" self._prop = param @@ -511,8 +507,9 @@ def _update_value(self, idx: int)->None: 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 setters above + # This should not be necessary as already done through setter above self._prop.param.value = val # but fast and nice for tests diff --git a/tests/widgets/test_labeled_slider_class.py b/tests/widgets/test_labeled_slider_class.py index 2e6c4134..8ea6dc3b 100644 --- a/tests/widgets/test_labeled_slider_class.py +++ b/tests/widgets/test_labeled_slider_class.py @@ -58,7 +58,7 @@ def slider(): ] ) model = ParametersModelMock(class_view, parent) - # note 3 elements in ratapi.ClassList for row_number == 2 to work + # 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) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 998520a0..c76b7672 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -1,23 +1,16 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import patch -import pydantic import pytest import ratapi -from matplotlib.widgets import Slider from rascal2.ui.view import MainWindowView from PyQt6 import QtWidgets, QtCore,QtGui from rascal2.widgets.project.project import create_draft_project -from rascal2.widgets.sliders_view import ( - SlidersViewWidget, - SliderChangeHolder, - LabeledSlider -) + from rascal2.widgets.project.tables import ( - ParameterFieldWidget, - ParametersModel + ParameterFieldWidget ) class MockFigureCanvas(QtWidgets.QWidget): """A mock figure canvas.""" @@ -35,18 +28,25 @@ def view_with_proj(): 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 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) ] ) 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 = 1 # fake project to fool checks for project presence in GUI - # without it normal project GUI will not be defined properly. Not used by SlidersWidget except - # to check that project GUI is defined, assuming that if project is defined its GUI is also defined + mw.presenter.model.project = project + + #project = self._parent.presenter.model.project + #prop_dictionary = create_draft_project(project) yield mw @@ -91,9 +91,12 @@ def fake_update(self,recalculate_project): fake_update.num_calls = 0 fake_update.project_updated = [] -#@patch("rascal2.widgets.project.tables.ParameterFieldWidget.update_project") + @patch.object(ParameterFieldWidget,"update_project",fake_update) def test_cancel_button_called(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 @@ -103,4 +106,51 @@ def test_cancel_button_called(view_with_proj): cancel_button.click() assert fake_update.num_calls == 2 - assert fake_update.project_updated == [False, True] \ No newline at end of file + # project update should be true for last property change + assert fake_update.project_updated == [False, True] + assert view_with_proj.show_sliders == False + 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 + +@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 fake_show_or_hide_sliders.call_param == False + + 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 fake_show_or_hide_sliders.call_param == False \ No newline at end of file From 7727e56d20f2a470d62546f55da2e653a5337b93 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:10:09 +0000 Subject: [PATCH 37/69] Re #148 ruff errors --- rascal2/ui/view.py | 20 +- rascal2/widgets/delegates.py | 8 +- rascal2/widgets/sliders_view.py | 292 +++++++++++---------- tests/ui/test_view.py | 22 +- tests/widgets/project/test_models.py | 17 +- tests/widgets/test_labeled_slider_class.py | 18 +- tests/widgets/test_sliders_widget.py | 17 +- 7 files changed, 201 insertions(+), 193 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 7344644a..6e99d355 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, TerminalWidget, SlidersViewWidget +from rascal2.widgets import ControlsWidget, PlotWidget, SlidersViewWidget, TerminalWidget from rascal2.widgets.project import ProjectWidget from rascal2.widgets.startup import StartUpWidget @@ -22,9 +22,9 @@ class MainWindowView(QtWidgets.QMainWindow): def __init__(self): super().__init__() - #Public interface + # Public interface self.disabled_elements = [] - self.show_sliders = False # no one displays sliders initially + self.show_sliders = False # no one displays sliders initially self.setWindowTitle(MAIN_WINDOW_TITLE) @@ -49,8 +49,9 @@ def __init__(self): # 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 + "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.create_actions() @@ -76,7 +77,7 @@ def __init__(self): "Plots": self.plot_widget, "Project": self.project_widget, "Terminal": self.terminal_widget, - "Fitting Controls": self.controls_widget + "Fitting Controls": self.controls_widget, } def closeEvent(self, event): @@ -286,7 +287,7 @@ def add_submenus(self, main_menu: QtWidgets.QMenuBar): help_menu.addAction(self.open_about_action) help_menu.addAction(self.open_help_action) - def show_or_hide_sliders(self,do_show_sliders = None): + 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. @@ -362,7 +363,8 @@ def setup_mdi(self): # Add sliders view widget separately, as it will behave and is controlled differently from other # mdi windows self.mdi.addSubWindow( - self.sliders_view_widget, QtCore.Qt.WindowType.WindowMinMaxButtonsHint | QtCore.Qt.WindowType.WindowTitleHint + self.sliders_view_widget, + QtCore.Qt.WindowType.WindowMinMaxButtonsHint | QtCore.Qt.WindowType.WindowTitleHint, ) self.sliders_view_widget.setWindowTitle("Sliders View") @@ -417,7 +419,7 @@ def save_mdi_layout(self): geoms = {} for window in self.mdi.subWindowList(): # get corresponding MDIGeometries entry for the widget - widget_name = window.windowTitle().replace(" ","") + widget_name = window.windowTitle().replace(" ", "") geom = window.geometry() geoms[widget_name] = (geom.x(), geom.y(), geom.width(), geom.height(), window.isMinimized()) diff --git a/rascal2/widgets/delegates.py b/rascal2/widgets/delegates.py index f5dea9f6..73d07fee 100644 --- a/rascal2/widgets/delegates.py +++ b/rascal2/widgets/delegates.py @@ -13,7 +13,7 @@ class ValidatedInputDelegate(QtWidgets.QStyledItemDelegate): # create custom signal to send to labelled sliders when contents of a cell in # a table class have been changed - editingFinished_InformSliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) + edit_finished_inform_sliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) def __init__(self, field_info, parent, remove_items: list[int] = None, open_on_show: bool = False): super().__init__(parent) @@ -54,7 +54,7 @@ def setEditorData(self, _editor: QtWidgets.QWidget, index): def setModelData(self, _editor, model, index): data = self.widget.get_data() model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole) - self.editingFinished_InformSliders.emit(index, self.field_info) + self.edit_finished_inform_sliders.emit(index, self.field_info) class CustomFileFunctionDelegate(QtWidgets.QStyledItemDelegate): @@ -103,7 +103,7 @@ class ValueSpinBoxDelegate(QtWidgets.QStyledItemDelegate): """ # create custom signal to send to labelled sliders when contents of a cell in # a table cell attached to sliders have been changed - editingFinished_InformSliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) + edit_finished_inform_sliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) def __init__(self, field: Literal["min", "value", "max"], parent): super().__init__(parent) @@ -138,7 +138,7 @@ def setEditorData(self, editor: AdaptiveDoubleSpinBox, index): def setModelData(self, editor, model, index): data = editor.value() model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole) - self.editingFinished_InformSliders.emit(index, self.field) + self.edit_finished_inform_sliders.emit(index, self.field) class ProjectFieldDelegate(QtWidgets.QStyledItemDelegate): diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 7de259fd..c81f764f 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -1,12 +1,9 @@ """Widget for the Sliders View window.""" - import ratapi.models -from PyQt6 import QtCore,QtWidgets +from PyQt6 import QtCore, QtWidgets -from rascal2.widgets.project.tables import ( - ParametersModel -) +from rascal2.widgets.project.tables import ParametersModel class SlidersViewWidget(QtWidgets.QWidget): @@ -26,32 +23,32 @@ def __init__(self, parent): """ super().__init__() self.mdi_holder = None # the variable contains reference to mdi container holding this widget - # Will be set up in the presenter, which arranges mdi windows for all MainWindow widgets + # Will be set up in the presenter, which arranges mdi windows for all MainWindow widgets self._view_geometry = None # holder for slider view geometry, created to store slider view location # 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" + 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 SliderUpdateHoler 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 fitable values + self._sliders = {} # dictionary of the sliders used to display fitable values # create initial slider view layout and everything else which depends on it self.init() def show(self): """Overload parent show method to deal with mdi container - showing sliders widget window. Also sets up or updates sliders - widget list depending on previous state of the widget. + showing sliders widget window. Also sets up or updates sliders + widget list depending on previous state of the widget. """ # avoid running init view more than once if sliders are visible. if self.isVisible(): - return + return self.init() if self.mdi_holder is None: @@ -69,7 +66,7 @@ def show(self): def hide(self): """Overload parent hide method to deal with mdi container - hiding slider widgets window + hiding slider widgets window """ if self.mdi_holder is None: @@ -81,16 +78,16 @@ def hide(self): def init(self) -> None: """The main Widget window is ready so this method initializes - general contents (buttons) of the sliders widget. - If project is defined it extracts properties, used to build - sliders and generates list of sliders widgets to - control the properties. - """ - if self.findChild(QtWidgets.QWidget,'AcceptButton') is None: + general contents (buttons) of the sliders widget. + If project is defined it extracts properties, used to build + sliders and generates list of sliders widgets to + control the properties. + """ + if self.findChild(QtWidgets.QWidget, "AcceptButton") 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 + 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: @@ -100,27 +97,27 @@ def init(self) -> None: def _init_properties_for_sliders(self) -> bool: """Loop through project's widget view tabs and morels associated with them. - Select all ParametersModel 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. - - Input: - ------ - Picks up project: self._parent.project_widget -- project to get properties to change - - Returns: - -------- - update_properties -- true if all properties in the project have already - had sliders, generated for them 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. - """ + Select all ParametersModel 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. + + Input: + ------ + Picks up project: self._parent.project_widget -- project to get properties to change + + Returns: + -------- + update_properties -- true if all properties in the project have already + had sliders, generated for them 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: @@ -131,40 +128,43 @@ def _init_properties_for_sliders(self) -> bool: 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 + 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 - row = 0 - for model_param in data_model.classlist: - if hasattr(model_param,"fit") and model_param.fit: # Parameters model should always + continue # data may be empty + + for row,model_param in enumerate(data_model.classlist): + if hasattr(model_param, "fit") and model_param.fit: # Parameters model should always # have fit attribute, but let's be on the safe side. # 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. - slider_info = SliderChangeHolder(row_number=row,model=data_model,param=model_param) + slider_info = SliderChangeHolder(row_number=row, model=data_model, param=model_param) trial_properties[model_param.name] = slider_info # Connect delegates which propagate parameters changed in tables to correspondent sliders. # Can be improved by using item index as these delegates emit "edited" signal for the whole # column, but row index is presented in signal itself. - this_prop_change_delegates = table_view.get_item_delegates(["min","max","value"]) + this_prop_change_delegates = table_view.get_item_delegates(["min", "max", "value"]) for delegate in this_prop_change_delegates: - delegate.editingFinished_InformSliders.connect( - lambda index,field, slider_name = model_param.name: - self._table_edit_finished_change_slider(index,field,slider_name) - ) + delegate.edit_finished_inform_sliders.connect( + lambda index, + field, + slider_name=model_param.name: self._table_edit_finished_change_slider( + index, field, slider_name + ) + ) if model_param.name in self._prop_to_change: - n_updated_properties += 1 - - row += 1 # Calculate table rows regardless model has fit== True or not + 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 + update_properties = ( + n_updated_properties == len(trial_properties) and len(self._prop_to_change) == n_updated_properties + ) # store information about sliders properties self._prop_to_change = trial_properties @@ -173,8 +173,8 @@ def _init_properties_for_sliders(self) -> bool: return update_properties - def _table_edit_finished_change_slider(self, index , field_name : str,slider_name : str) -> None: - """ Method to bind with tables delegates and change slider appearance accordingly to changes in tables. + def _table_edit_finished_change_slider(self, index, field_name: str, slider_name: str) -> None: + """Method to bind with tables delegates and change slider appearance accordingly to changes in tables. # # Signal about slider parameters changed is sent to sliders widget last after all other signals on # table parameters have been processed. @@ -188,13 +188,13 @@ def _table_edit_finished_change_slider(self, index , field_name : str,slider_nam # -- name of the property, slider describes and key, which defines slider position in the dictionary # of sliders """ - if self.isVisible(): # Do not bother otherwise, as slider appearance will be modified when made visible and + if self.isVisible(): # Do not bother otherwise, as slider appearance will be modified when made visible and # slider itself may have even been deleted self._sliders[slider_name].update_slider_display_from_property(in_constructor=False) def _create_slider_view_layout(self) -> None: - """ Create sliders layout with all necessary controls and connections - but without sliders themselves. + """Create sliders layout with all necessary controls and connections + but without sliders themselves. """ main_layout = QtWidgets.QVBoxLayout() @@ -215,8 +215,8 @@ def _create_slider_view_layout(self) -> None: 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 + """Given sliders view layout and list of properties which can be controlled by sliders + add appropriate sliders to sliders view Widget """ scroll = self.findChild(QtWidgets.QScrollArea, "Scroll") if scroll is None: @@ -245,53 +245,51 @@ def _add_sliders_widgets(self) -> None: self._sliders[no_label.slider_name] = no_label else: content_layout.setSpacing(0) - for name,prop in self._prop_to_change.items(): + for name, prop in self._prop_to_change.items(): slider = LabeledSlider(prop) - self._sliders[prop.name] = slider + self._sliders[name] = slider content_layout.addWidget(slider) def _update_sliders_widgets(self) -> None: """Updates the sliders given the project properties to fit are the same - but their values may be modified + but their values may be modified """ - for name,prop in self._prop_to_change.items(): + for name, prop in self._prop_to_change.items(): self._sliders[name].update_slider_parameters(prop) def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders - and hide sliders view. + and hide sliders view. """ - last_call = len(self._values_to_revert)-1 - call_cnt = 0 - for key,val in self._values_to_revert.items(): - if call_cnt == last_call: # its important to update project at last call - self._prop_to_change[key].update_value_representation(val,recalculate_project=True) - else: - self._prop_to_change[key].update_value_representation(val,recalculate_project=False) - self._sliders[key].set_slider_gui_position(val) + last_call = len(self._values_to_revert) - 1 - call_cnt += 1 + for call_cnt,(key, val) in enumerate(self._values_to_revert.items()): + self._prop_to_change[key].update_value_representation( + val, + recalculate_project=(call_cnt == last_call) # it is important to update project at last call only + ) self._parent.show_or_hide_sliders(do_show_sliders=False) def _apply_changes_from_sliders(self) -> None: """Apply changes obtained from sliders to the project - and make them permanent + and make them permanent """ # Changes have already been applied so just hide sliders widget self._parent.show_or_hide_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 + """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: + + def __init__(self, row_number: int, model: ParametersModel, param: ratapi.models.Parameter) -> None: + """Class Initialization function: Inputs: ------ row_number: int - the number of the row in the project table, which should be changed @@ -300,7 +298,7 @@ def __init__(self, row_number: int,model : ParametersModel, param : ratapi.model param: ratapi.models.Parameter - the parameter which value field may be changed by slider widget """ self.param = param - self._vis_model = model + self._vis_model = model self._row_number = row_number @property @@ -310,46 +308,49 @@ def name(self): @property def value(self) -> float: return self.param.value + @value.setter def value(self, value: float) -> None: - setattr(self.param,"value",value) + self.param.value = value - def update_value_representation(self,val : float, recalculate_project = True) -> None: - """ given new value, update project table and property representations - No check are necessary as value comes from slider or back-up cache + def update_value_representation(self, val: float, recalculate_project=True) -> None: + """given new value, update project table and property representations + No check are necessary as value comes from slider or back-up cache - recalculate_project -- if True, run ratapi calculations and updates - results representation. - """ + recalculate_project -- if True, run ratapi calculations and updates + results representation. + """ # 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) + 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 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. """ + # Instance attributes generator # Defaults for property min/max. Will be overwritten from actual input property - _value_min: float | None = 0 # minimal value property may have + _value_min: float | None = 0 # minimal value property may have _value_max: float | None = 100 # maximal value property may have - _value: float | None = 50 # cache for property value - _value_range: float | None = 100 # value range (difference between maximal and minimal values of the property) + _value: float | None = 50 # cache for property value + _value_range: float | None = 100 # difference between maximal and minimal values of the property _value_step: float | None = 1 # the change in property value per single step slider move - # Class attributes of slider widget which usually remain the same for all classes. Do not override unless in __init__ - # method + # Class attributes of slider widget which usually remain the same for all classes. + # Do not override unless in __init__ method _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 - + _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 @@ -366,15 +367,17 @@ def __init__(self, param: SliderChangeHolder): 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 + 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) + 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) @@ -383,11 +386,13 @@ def __init__(self, param: SliderChangeHolder): 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 + 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) @@ -411,10 +416,10 @@ def __init__(self, param: SliderChangeHolder): self.setFrameShape(QtWidgets.QFrame.Shape.Box) self.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - def set_slider_gui_position(self,value : float) -> None: + 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 + As value assumed to be already correct, block signal + for change, associated with slider position change in GUI """ self._value = value self._value_label.setText(self._value_label_format.format(value)) @@ -424,7 +429,7 @@ def set_slider_gui_position(self,value : float) -> None: self._slider.setValue(idx) self.__block_slider_value_changed_signal = False - def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): + def update_slider_parameters(self, param: SliderChangeHolder, in_constructor=False): """Modifies slider values which may change for this slider from his parent property""" self._prop = param # Changing RASCAL property this slider modifies is currently prohibited, @@ -434,18 +439,18 @@ def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = F 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: + 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. + if property, underlying sliders parameters have changed. - Bound to event received from delegate when table values are changed. + Bound to event received from delegate when table values are changed. """ # 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) + 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 @@ -454,13 +459,13 @@ def update_slider_display_from_property(self,in_constructor: bool) -> None: # 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 + 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. - Update them if they are and return True. False if they have not been changed. + """Check if rascal property values related to slider widget have changed. + Update them if they are and return True. False if they have not been changed. """ updated = False if self._value_min != self._prop.param.min: @@ -469,26 +474,26 @@ def _updated_from_rascal_property(self) -> bool: if self._value_max != self._prop.param.max: self._value_max = self._prop.param.max updated = True - if self._value != self._prop.param.value: + 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 value into slider position""" - return int(round(self._slider_max_idx*(value-self._value_min)/self._value_range,0)) + return int(round(self._slider_max_idx * (value - self._value_min) / self._value_range, 0)) - def _slider_pos_to_value(self,index: int) -> float: + def _slider_pos_to_value(self, index: int) -> float: """Convert slider GUI position (index) into double value""" - 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_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: + def _build_slider(self, initial_value: float) -> QtWidgets.QSlider: """Construct slider widget with integer scales and ticks in integer positions - Part of slider constructor - """ + Part of slider constructor + """ slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) slider.setMinimum(0) @@ -500,8 +505,8 @@ def _build_slider(self,initial_value: float) -> QtWidgets.QSlider: return slider - def _update_value(self, idx: int)->None: - """ Bound in constructor to GUI slider position changed event""" + def _update_value(self, idx: int) -> None: + """Bound in constructor to GUI slider position changed event""" if self.__block_slider_value_changed_signal: return val = self._slider_pos_to_value(idx) @@ -510,7 +515,7 @@ def _update_value(self, idx: int)->None: 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 + self._prop.param.value = val # but fast and nice for tests class EmptySlider(LabeledSlider): @@ -525,17 +530,20 @@ def __init__(self): # Build all sliders widget and arrange them as expected self._slider = self._build_slider(0) - name_label = QtWidgets.QLabel("No property to fit within the project. No sliders constructed", alignment=QtCore.Qt.AlignmentFlag.AlignCenter) + name_label = QtWidgets.QLabel( + "No property to fit within the project. No sliders constructed", + 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: + def set_slider_gui_position(self, value: float) -> None: return - def update_slider_parameters(self, param: SliderChangeHolder, in_constructor = False): + def update_slider_parameters(self, param: SliderChangeHolder, in_constructor=False): return - def update_slider_display_from_property(self,in_constructor: bool) -> None: + def update_slider_display_from_property(self, in_constructor: bool) -> None: return diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index bdd520c5..af28ecbd 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -39,8 +39,10 @@ def test_view(): @pytest.mark.parametrize( "geometry", [ - ((1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True)), - ((1, 2, 196, 24, True), (3, 78, 196, 24, True), (1, 2, 204, 66, False), (12, 342, 196, 24, True), (5, 6, 200, 28, True)), + ((1, 2, 196, 24, True), (1, 2, 196, 24, True), + (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True)), + ((1, 2, 196, 24, True), (3, 78, 196, 24, True), + (1, 2, 204, 66, False), (12, 342, 196, 24, True), (5, 6, 200, 28, True)), ], ) @patch("rascal2.ui.view.ProjectWidget.show_project_view") @@ -52,7 +54,11 @@ def test_reset_mdi(self, mock1, mock2, mock3, test_view, geometry): test_view.settings = Settings() test_view.setup_mdi() test_view.settings.mdi_defaults = MDIGeometries( - Plots=geometry[0], Project=geometry[1], Terminal=geometry[2], FittingControls=geometry[3], SlidersView=geometry[4] + Plots=geometry[0], + Project=geometry[1], + Terminal=geometry[2], + FittingControls=geometry[3], + SlidersView=geometry[4] ) test_view.reset_mdi_layout() for window in test_view.mdi.subWindowList(): @@ -206,7 +212,7 @@ def test_click_on_select_sliders_works_as_expected(mock_hide,mock_show,test_view # check initial state -- defined now but needs to be refactored when # this may be included in configuration - assert test_view_with_mdi.show_sliders == False + assert not test_view_with_mdi.show_sliders main_menu = test_view_with_mdi.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") @@ -215,7 +221,7 @@ def test_click_on_select_sliders_works_as_expected(mock_hide,mock_show,test_view # Trigger the action all_actions[0].trigger() assert all_actions[0].text() == "&Hide Sliders" - assert test_view_with_mdi.show_sliders == True + assert test_view_with_mdi.show_sliders assert mock_show.call_count == 1 @patch("rascal2.ui.view.SlidersViewWidget.show") @@ -227,7 +233,7 @@ def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view_wi # check initial state -- defined now but needs to be refactored when # this may be included in configuration - assert test_view_with_mdi.show_sliders == False + assert not test_view_with_mdi.show_sliders main_menu = test_view_with_mdi.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") @@ -235,11 +241,11 @@ def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view_wi # Trigger the action all_actions[0].trigger() - assert test_view_with_mdi.show_sliders == True + 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 all_actions[0].trigger() assert all_actions[0].text() == "&Show Sliders" - assert test_view_with_mdi.show_sliders == False + assert not test_view_with_mdi.show_sliders assert mock_hide.call_count == 1 # this would hide sliders widget diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index a5da729a..fc903049 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -271,9 +271,11 @@ def test_param_item_delegates(widget_with_delegates): for column, header in enumerate(widget_with_delegates.model.headers, start=1): if header in ["min", "value", "max"]: - assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), delegates.ValueSpinBoxDelegate) + assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), + delegates.ValueSpinBoxDelegate) else: - assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), delegates.ValidatedInputDelegate) + assert isinstance(widget_with_delegates.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 """ @@ -285,12 +287,12 @@ def test_param_item_delegates_exposed_to_sliders(widget_with_delegates): assert isinstance(delegate,delegates.ValueSpinBoxDelegate) -class fake_editor(object): +class MockEditor: """A class with have only one method providing value. Used in test below""" def value(self): return 42 -class MockReceiver(object): +class MockReceiver: """Test object which receives signals sent to slider """ def __init__(self): self.cache_state = [] @@ -309,14 +311,13 @@ def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): # Expected order of delegates in the property should be is as in the list. delegates_list = widget_with_delegates.get_item_delegates(selected_fields) for delegate in delegates_list: - delegate.editingFinished_InformSliders.connect(lambda idx,tab_name : sr.receive_signal(idx,tab_name)) + delegate.edit_finished_inform_sliders.connect(lambda idx,tab_name : sr.receive_signal(idx,tab_name)) index = widget_with_delegates.model.index(1,1) - fed = fake_editor() + fed = MockEditor() n_calls = 0 - for delegate,field_name in zip(delegates_list,selected_fields): + for n_calls,(delegate,field_name) in enumerate(zip(delegates_list,selected_fields)): delegate.setModelData(fed,widget_with_delegates.model, index) - n_calls += 1 assert sr.call_count == n_calls assert sr.cache_state == (index,field_name) diff --git a/tests/widgets/test_labeled_slider_class.py b/tests/widgets/test_labeled_slider_class.py index 8ea6dc3b..28a9566b 100644 --- a/tests/widgets/test_labeled_slider_class.py +++ b/tests/widgets/test_labeled_slider_class.py @@ -1,18 +1,10 @@ -from unittest.mock import MagicMock, patch - import pydantic import pytest import ratapi +from PyQt6 import QtCore, QtWidgets -from PyQt6 import QtWidgets, QtCore - -from rascal2.widgets.sliders_view import ( - SliderChangeHolder, - LabeledSlider -) -from rascal2.widgets.project.tables import ( - ParametersModel -) +from rascal2.widgets.project.tables import ParametersModel +from rascal2.widgets.sliders_view import LabeledSlider, SliderChangeHolder class ParametersModelMock(ParametersModel): @@ -27,7 +19,9 @@ 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: + 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 diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index c76b7672..d3e89217 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -2,16 +2,13 @@ import pytest import ratapi +from PyQt6 import QtWidgets from rascal2.ui.view import MainWindowView - -from PyQt6 import QtWidgets, QtCore,QtGui - from rascal2.widgets.project.project import create_draft_project +from rascal2.widgets.project.tables import ParameterFieldWidget + -from rascal2.widgets.project.tables import ( - ParameterFieldWidget -) class MockFigureCanvas(QtWidgets.QWidget): """A mock figure canvas.""" @@ -53,7 +50,7 @@ def view_with_proj(): def test_extract_properties_for_sliders(view_with_proj): update_sliders = view_with_proj.sliders_view_widget._init_properties_for_sliders() - assert update_sliders == False # its false as at first call sliders should be regenerated + 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) == 2 assert list(view_with_proj.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] assert list(view_with_proj.sliders_view_widget._values_to_revert.values()) == [2.1, 209.] @@ -108,7 +105,7 @@ def test_cancel_button_called(view_with_proj): assert fake_update.num_calls == 2 # project update should be true for last property change assert fake_update.project_updated == [False, True] - assert view_with_proj.show_sliders == False + 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 @@ -146,11 +143,11 @@ 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 fake_show_or_hide_sliders.call_param == False + 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 fake_show_or_hide_sliders.call_param == False \ No newline at end of file + assert not fake_show_or_hide_sliders.call_param \ No newline at end of file From 4d03d05f20db5b0698768dd7709d09ca5991a27d Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:17:58 +0000 Subject: [PATCH 38/69] Re #148 more ruff changes --- rascal2/widgets/project/project.py | 3 ++- rascal2/widgets/project/tables.py | 3 ++- tests/widgets/project/test_models.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index b9adf768..77427311 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -361,7 +361,8 @@ def show_project_view(self) -> None: def show_edit_view(self) -> None: """Show edit view""" - self.parent.show_or_hide_sliders(do_show_sliders=False) # when you show it again it contents will be updated according to edit changes + self.parent.show_or_hide_sliders(do_show_sliders=False) # when you show it again it contents + # will be updated according to edit changes self.update_project_view(0) self.setWindowTitle("Edit Project") self.parent.controls_widget.run_button.setEnabled(False) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 63079666..0273581b 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -75,7 +75,8 @@ 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 : float, role=QtCore.Qt.ItemDataRole.EditRole,recalculate_proj = True) -> bool: + def setData(self, index : QtCore.QModelIndex, value : float, + role=QtCore.Qt.ItemDataRole.EditRole,recalculate_proj = True) -> bool: """ Implement abstract setData method of QAbstractTableModel with additional variable recalculate_proj -- Set it to False when modifying a bunch of properties in a loop, setting it to True for the last value to recalculate project and update all table dependent widgets. diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index fc903049..d4b833ce 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -316,7 +316,8 @@ def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): index = widget_with_delegates.model.index(1,1) fed = MockEditor() n_calls = 0 - for n_calls,(delegate,field_name) in enumerate(zip(delegates_list,selected_fields)): + for n_calls,(delegate,field_name) in enumerate( + zip(delegates_list,selected_fields,strict=True)): delegate.setModelData(fed,widget_with_delegates.model, index) assert sr.call_count == n_calls assert sr.cache_state == (index,field_name) From a3bab04f48ff58b5d09d765ab7711dee779b2296 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:20:23 +0000 Subject: [PATCH 39/69] Re #148 Formal change -- something wrong with ruff --- rascal2/widgets/sliders_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index c81f764f..7783ec3e 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -260,7 +260,7 @@ def _update_sliders_widgets(self) -> None: def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders - and hide sliders view. + and hide sliders view. """ last_call = len(self._values_to_revert) - 1 From 21387f7c00d69a6bafe973723130e06c7bdf33c4 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:28:05 +0000 Subject: [PATCH 40/69] Re #148 Ruff format applied --- rascal2/settings.py | 3 +- rascal2/widgets/__init__.py | 2 +- rascal2/widgets/delegates.py | 1 + rascal2/widgets/project/tables.py | 31 ++++----- rascal2/widgets/sliders_view.py | 11 ++- tests/test_ui.py | 3 +- tests/ui/test_view.py | 38 +++++++---- tests/widgets/project/test_models.py | 43 ++++++------ tests/widgets/project/test_project.py | 8 ++- tests/widgets/test_labeled_slider_class.py | 53 ++++++++------- tests/widgets/test_sliders_widget.py | 79 ++++++++++++---------- 11 files changed, 148 insertions(+), 124 deletions(-) diff --git a/rascal2/settings.py b/rascal2/settings.py index 893e6074..3720c79a 100644 --- a/rascal2/settings.py +++ b/rascal2/settings.py @@ -96,11 +96,12 @@ def _missing_(cls, value): class MDIGeometries(BaseModel): """Model for storing window positions and sizes.""" + Plots: WindowGeometry = Field(max_length=5, min_length=5) Project: WindowGeometry = Field(max_length=5, min_length=5) Terminal: WindowGeometry = Field(max_length=5, min_length=5) FittingControls: WindowGeometry = Field(max_length=5, min_length=5) - SlidersView: WindowGeometry = Field(max_length=5, min_length=5) + SlidersView: WindowGeometry = Field(max_length=5, min_length=5) class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True): diff --git a/rascal2/widgets/__init__.py b/rascal2/widgets/__init__.py index 4d61b8b2..bd884688 100644 --- a/rascal2/widgets/__init__.py +++ b/rascal2/widgets/__init__.py @@ -12,5 +12,5 @@ "MultiSelectList", "PlotWidget", "TerminalWidget", - "SlidersViewWidget" + "SlidersViewWidget", ] diff --git a/rascal2/widgets/delegates.py b/rascal2/widgets/delegates.py index 73d07fee..3dc09fbb 100644 --- a/rascal2/widgets/delegates.py +++ b/rascal2/widgets/delegates.py @@ -101,6 +101,7 @@ class ValueSpinBoxDelegate(QtWidgets.QStyledItemDelegate): The field of the parameter """ + # create custom signal to send to labelled sliders when contents of a cell in # a table cell attached to sliders have been changed edit_finished_inform_sliders = QtCore.pyqtSignal(QtCore.QModelIndex, object) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 0273581b..eba5692f 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -75,11 +75,12 @@ 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 : float, - role=QtCore.Qt.ItemDataRole.EditRole,recalculate_proj = True) -> bool: - """ Implement abstract setData method of QAbstractTableModel with additional variable - recalculate_proj -- Set it to False when modifying a bunch of properties in a loop, setting - it to True for the last value to recalculate project and update all table dependent widgets. + def setData( + self, index: QtCore.QModelIndex, value: float, role=QtCore.Qt.ItemDataRole.EditRole, recalculate_proj=True + ) -> bool: + """Implement abstract setData method of QAbstractTableModel with additional variable + recalculate_proj -- Set it to False when modifying a bunch of properties in a loop, setting + it to True for the last value to recalculate project and update all table dependent widgets. """ if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole: row = index.row() @@ -98,7 +99,7 @@ def setData(self, index : QtCore.QModelIndex, value : float, 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" and recalculate_proj self.parent.update_project(recalculate) self.dataChanged.emit(index, index) return True @@ -254,22 +255,17 @@ 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 - ) + 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 + 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) - ) + dlgts.append(self.table.itemDelegateForColumn(i + self.model.col_offset)) return dlgts def append_item(self): @@ -371,7 +367,6 @@ class ParameterFieldWidget(ProjectFieldWidget): classlist_model = ParametersModel def set_item_delegates(self): - for i, header in enumerate(self.model.headers): if header in ["min", "value", "max"]: delegate = delegates.ValueSpinBoxDelegate(header, self.table) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 7783ec3e..100ae89e 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -134,7 +134,7 @@ def _init_properties_for_sliders(self) -> bool: if not isinstance(data_model, ParametersModel): continue # data may be empty - for row,model_param in enumerate(data_model.classlist): + for row, model_param in enumerate(data_model.classlist): if hasattr(model_param, "fit") and model_param.fit: # Parameters model should always # have fit attribute, but let's be on the safe side. # Store information about necessary property and the model, which contains the property. @@ -158,7 +158,6 @@ def _init_properties_for_sliders(self) -> bool: 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. @@ -260,14 +259,14 @@ def _update_sliders_widgets(self) -> None: def _cancel_changes_from_sliders(self): """Cancel changes to properties obtained from sliders - and hide sliders view. + and hide sliders view. """ last_call = len(self._values_to_revert) - 1 - for call_cnt,(key, val) in enumerate(self._values_to_revert.items()): + for call_cnt, (key, val) in enumerate(self._values_to_revert.items()): self._prop_to_change[key].update_value_representation( val, - recalculate_project=(call_cnt == last_call) # it is important to update project at last call only + recalculate_project=(call_cnt == last_call), # it is important to update project at last call only ) self._parent.show_or_hide_sliders(do_show_sliders=False) @@ -311,7 +310,7 @@ def value(self) -> float: @value.setter def value(self, value: float) -> None: - self.param.value = value + self.param.value = value def update_value_representation(self, val: float, recalculate_project=True) -> None: """given new value, update project table and property representations diff --git a/tests/test_ui.py b/tests/test_ui.py index bfd9a491..849d22cb 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -39,9 +39,8 @@ def test_integration(qt_application, make_main_window): window.presenter.create_project("project", ".") names = [win.windowTitle() for win in window.mdi.subWindowList()] # QMDIArea is first in last out hence the reversed list - assert names == ["Fitting Controls", "Terminal", "Project", "Plots","Sliders View"] + assert names == ["Fitting Controls", "Terminal", "Project", "Plots", "Sliders View"] # Work through the different sections of the UI window.close() - diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index af28ecbd..1cb65d53 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -39,10 +39,20 @@ def test_view(): @pytest.mark.parametrize( "geometry", [ - ((1, 2, 196, 24, True), (1, 2, 196, 24, True), - (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True)), - ((1, 2, 196, 24, True), (3, 78, 196, 24, True), - (1, 2, 204, 66, False), (12, 342, 196, 24, True), (5, 6, 200, 28, True)), + ( + (1, 2, 196, 24, True), + (1, 2, 196, 24, True), + (1, 2, 196, 24, True), + (1, 2, 196, 24, True), + (1, 2, 196, 24, True), + ), + ( + (1, 2, 196, 24, True), + (3, 78, 196, 24, True), + (1, 2, 204, 66, False), + (12, 342, 196, 24, True), + (5, 6, 200, 28, True), + ), ], ) @patch("rascal2.ui.view.ProjectWidget.show_project_view") @@ -58,7 +68,7 @@ def test_reset_mdi(self, mock1, mock2, mock3, test_view, geometry): Project=geometry[1], Terminal=geometry[2], FittingControls=geometry[3], - SlidersView=geometry[4] + SlidersView=geometry[4], ) test_view.reset_mdi_layout() for window in test_view.mdi.subWindowList(): @@ -140,7 +150,6 @@ def change_dir(*args, **kwargs): mock_overwrite.assert_called_once() - @pytest.mark.parametrize("submenu_name", ["&File", "&Edit", "&Windows", "&Tools", "&Help"]) def test_menu_element_present(test_view, submenu_name): """Test requested menu items are present""" @@ -174,7 +183,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"]), ], ) @@ -191,8 +200,8 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay @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 + """An instance of MainWindowView with mdi property defined to some rubbish + for mimicking operations performed in MainWindowView.reset_mdi_layout """ with patch("rascal2.widgets.plot.FigureCanvas", return_value=MockFigureCanvas()): mw = MainWindowView() @@ -205,7 +214,7 @@ def test_view_with_mdi(): @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): +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 """ @@ -224,11 +233,12 @@ def test_click_on_select_sliders_works_as_expected(mock_hide,mock_show,test_view 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") -def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view_with_mdi): +def test_click_on_select_tabs_works_as_expected(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 + and initiates correct callback """ # check initial state -- defined now but needs to be refactored when @@ -242,10 +252,10 @@ def test_click_on_select_tabs_works_as_expected(mock_hide,mock_show,test_view_wi # 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 + assert mock_show.call_count == 1 # this would show sliders widget # check if next click returns to initial state 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_hide.call_count == 1 # this would hide sliders widget diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index d4b833ce..57c521f4 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -256,71 +256,76 @@ def test_parameter_flags(param_model, prior_type, protected): @pytest.fixture def widget_with_delegates(): - widget = ParameterFieldWidget("Test", parent) widget.parent = MagicMock() - param = [ ratapi.models.Parameter() for i in [0, 1, 2] ] + 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): if header in ["min", "value", "max"]: - assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), - delegates.ValueSpinBoxDelegate) + assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), delegates.ValueSpinBoxDelegate) else: - assert isinstance(widget_with_delegates.table.itemDelegateForColumn(column), - delegates.ValidatedInputDelegate) + assert isinstance( + widget_with_delegates.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 """ + """Test that parameter models provides the item delegates related to slides""" - delegates_list = widget_with_delegates.get_item_delegates(["min","max","value"]) + 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) + assert isinstance(delegate, delegates.ValueSpinBoxDelegate) class MockEditor: """A class with have only one method providing value. Used in test below""" + def value(self): return 42 + class MockReceiver: - """Test object which receives signals sent to slider """ + """Test object which receives signals sent to slider""" + def __init__(self): self.cache_state = [] self.call_count = 0 - def receive_signal(self,index,value): + def receive_signal(self, index, value): """To bind to delegate signal""" self.call_count += 1 - self.cache_state = (index,value) + self.cache_state = (index, value) + def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): """Test if edit_finished signals emitted to subscribed clients""" sr = MockReceiver() - selected_fields = ["min","value","max"] + selected_fields = ["min", "value", "max"] # Expected order of delegates in the property should be is as in the list. delegates_list = widget_with_delegates.get_item_delegates(selected_fields) for delegate in delegates_list: - delegate.edit_finished_inform_sliders.connect(lambda idx,tab_name : sr.receive_signal(idx,tab_name)) + delegate.edit_finished_inform_sliders.connect(lambda idx, tab_name: sr.receive_signal(idx, tab_name)) - index = widget_with_delegates.model.index(1,1) + index = widget_with_delegates.model.index(1, 1) fed = MockEditor() n_calls = 0 - for n_calls,(delegate,field_name) in enumerate( - zip(delegates_list,selected_fields,strict=True)): - delegate.setModelData(fed,widget_with_delegates.model, index) + for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True)): + delegate.setModelData(fed, widget_with_delegates.model, index) assert sr.call_count == n_calls - assert sr.cache_state == (index,field_name) + assert sr.cache_state == (index, field_name) + def test_hidden_bayesian_columns(widget_with_delegates): """Test that Bayes columns are hidden when procedure is not Bayesian.""" diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index bb0466d4..2347cb50 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -39,7 +39,7 @@ def __init__(self): self.controls_widget = MagicMock() self.sliders_view_widget = SlidersViewWidget(self) - def show_or_hide_sliders(self,do_show_sliders = True): + def show_or_hide_sliders(self, do_show_sliders=True): if do_show_sliders: self.sliders_view_widget.show() else: @@ -159,8 +159,9 @@ def test_project_widget_initial_state(setup_project_widget): assert project_widget.project_tab.currentIndex() == 0 assert project_widget.edit_project_tab.currentIndex() == 0 + @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_edit_cancel_button_toggle(mock_hide,setup_project_widget): +def test_edit_cancel_button_toggle(mock_hide, setup_project_widget): """ Tests clicking the edit button causes the stacked widget to change state. """ @@ -182,8 +183,9 @@ def test_edit_cancel_button_toggle(mock_hide,setup_project_widget): assert project_widget.model_type.text() == LayerModels.StandardLayers assert project_widget.calculation_type.text() == Calculations.Normal + @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_save_changes_to_model_project(mock_hide,setup_project_widget): +def test_save_changes_to_model_project(mock_hide, setup_project_widget): """ Tests that making changes to the project settings """ diff --git a/tests/widgets/test_labeled_slider_class.py b/tests/widgets/test_labeled_slider_class.py index 28a9566b..11fd1135 100644 --- a/tests/widgets/test_labeled_slider_class.py +++ b/tests/widgets/test_labeled_slider_class.py @@ -8,7 +8,6 @@ class ParametersModelMock(ParametersModel): - _value: float _index: QtCore.QModelIndex _role: QtCore.Qt.ItemDataRole @@ -17,18 +16,19 @@ class ParametersModelMock(ParametersModel): def __init__(self, class_list: ratapi.ClassList, parent: QtWidgets.QWidget): super().__init__(class_list, parent) - self.call_count = 0 + self.call_count = 0 - def setData(self,index : QtCore.QModelIndex, val : float, - qt_role = QtCore.Qt.ItemDataRole.EditRole, - recalculate_project = True) -> bool: + 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 + self.call_count += 1 return True + class DataModel(pydantic.BaseModel, validate_assignment=True): """A test Pydantic model.""" @@ -42,18 +42,18 @@ class DataModel(pydantic.BaseModel, validate_assignment=True): @pytest.fixture def slider(): - param = ratapi.models.Parameter(name = "Test Slider", min=1, max=10, value = 2.1, fit=True) + 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), + 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) + inputs = SliderChangeHolder(row_number=2, model=model, param=param) return LabeledSlider(inputs) @@ -61,21 +61,24 @@ 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_range == 10 - 1 assert slider._value == 2.1 - assert slider._value_step == 9/100 + 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) @@ -83,20 +86,23 @@ def test_set_slider_value_changes_label(slider): 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""" + """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""" + """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 """ + """update value bound correctly and does correct calls""" assert slider._prop._vis_model.call_count == 0 slider._slider.setValue(50) @@ -105,22 +111,23 @@ def test_set_value_do_correct_calls(slider): 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 + 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 - ] + (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""" + """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._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 index d3e89217..0f971a3e 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -15,23 +15,26 @@ class MockFigureCanvas(QtWidgets.QWidget): 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 + 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 3", min=100, max=1000, value=209, fit=True), + ] ) draft["background_parameters"] = ratapi.ClassList( - [ratapi.models.Parameter(name="Background Param 1", min=0, max=1, value=0.2, fit=False),] + [ + 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"]: @@ -42,26 +45,24 @@ def view_with_proj(): mw.project_widget.view_tabs["Parameters"].update_model(draft) mw.presenter.model.project = project - #project = self._parent.presenter.model.project - #prop_dictionary = create_draft_project(project) + # project = self._parent.presenter.model.project + # prop_dictionary = create_draft_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) == 2 - assert list(view_with_proj.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] - assert list(view_with_proj.sliders_view_widget._values_to_revert.values()) == [2.1, 209.] - assert view_with_proj.sliders_view_widget._init_properties_for_sliders() # now its true as sliders should be - # available for update on second call + 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) == 2 + assert list(view_with_proj.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] + assert list(view_with_proj.sliders_view_widget._values_to_revert.values()) == [2.1, 209.0] + 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): - +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 @@ -71,7 +72,6 @@ def test_create_update_called(add_sliders,update_sliders,view_with_proj): 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) == 2 assert "Param 1" in view_with_proj.sliders_view_widget._sliders @@ -82,65 +82,70 @@ def test_init_slider_widget_builds_sliders(view_with_proj): assert slider2._prop._vis_model == view_with_proj.project_widget.view_tabs["Parameters"].tables["parameters"].model -def fake_update(self,recalculate_project): +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 = [] -@patch.object(ParameterFieldWidget,"update_project",fake_update) +@patch.object(ParameterFieldWidget, "update_project", fake_update) def test_cancel_button_called(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") + 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 + assert fake_update.num_calls == 2 # project update should be true for last property change - assert fake_update.project_updated == [False, True] + 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 -@patch("rascal2.ui.view.SlidersViewWidget._apply_changes_from_sliders") -def test_cancel_accept_button_connections(mock_accept,view_with_proj): +@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 = 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): +@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 = 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): + +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): +@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 @@ -150,4 +155,4 @@ def test_apply_cancel_changes_called_hide_sliders(view_with_proj): 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 \ No newline at end of file + assert not fake_show_or_hide_sliders.call_param From ba721970699cf9da4741b4a33634f247afad3c73 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:35:45 +0000 Subject: [PATCH 41/69] Re #148 fixed unit tests for project/test_project.py --- tests/widgets/project/test_project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 2347cb50..405e38e3 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -37,6 +37,7 @@ def __init__(self): super().__init__() 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): From c6325c20cfc76a8aec1f4727ea7cf7824db59329 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:44:18 +0000 Subject: [PATCH 42/69] Re #148 Bug due to ruff reformatting --- tests/widgets/project/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 57c521f4..83801346 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -320,8 +320,8 @@ def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): index = widget_with_delegates.model.index(1, 1) fed = MockEditor() - n_calls = 0 - for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True)): + + for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True),start=1): delegate.setModelData(fed, widget_with_delegates.model, index) assert sr.call_count == n_calls assert sr.cache_state == (index, field_name) From 5e993e84c37323ca8b7862dcd3ab3825f9c99238 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 17:56:17 +0000 Subject: [PATCH 43/69] Re #148 fixed (figure canvas) --- tests/ui/test_view.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 1cb65d53..58c5bf15 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -203,13 +203,13 @@ 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 """ - with patch("rascal2.widgets.plot.FigureCanvas", return_value=MockFigureCanvas()): - 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() - yield mw + + 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") From 7436406d845e4c83340ba4739f34ef042341ad35 Mon Sep 17 00:00:00 2001 From: abuts Date: Thu, 13 Nov 2025 18:02:37 +0000 Subject: [PATCH 44/69] Re #148 ruff errors --- tests/widgets/project/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 83801346..34b4e758 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -321,7 +321,7 @@ def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): index = widget_with_delegates.model.index(1, 1) fed = MockEditor() - for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True),start=1): + for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True), start=1): delegate.setModelData(fed, widget_with_delegates.model, index) assert sr.call_count == n_calls assert sr.cache_state == (index, field_name) From 55b38128522ceb4e8f8e099f9ddaf4c78a46d213 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 10:59:22 +0000 Subject: [PATCH 45/69] Re #148 Tests for empty sliders --- rascal2/widgets/sliders_view.py | 4 ++- tests/widgets/test_sliders_widget.py | 53 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 100ae89e..7fad527e 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -162,7 +162,9 @@ def _init_properties_for_sliders(self) -> bool: # 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 + 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 diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 0f971a3e..c415da0d 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -7,6 +7,7 @@ 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): @@ -156,3 +157,55 @@ def test_apply_cancel_changes_called_hide_sliders(view_with_proj): 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) From e95392b09138a22e58f1354257aed19f4e675c73 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 14:55:45 +0000 Subject: [PATCH 46/69] Re #149 unit tests for show sliders widget --- rascal2/ui/view.py | 37 +++++++++++++++++-- rascal2/widgets/project/project.py | 14 +++++++- tests/ui/test_view.py | 48 +++++++++++++++++++++++++ tests/widgets/test_sliders_widget.py | 54 ++++++++++++++++++++++++++-- 4 files changed, 147 insertions(+), 6 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 6e99d355..203b46e4 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -24,7 +24,8 @@ def __init__(self): super().__init__() # Public interface self.disabled_elements = [] - self.show_sliders = False # no one displays sliders initially + self.show_sliders = False # no one displays sliders initially except got from configuration + # (not implemented yet) self.setWindowTitle(MAIN_WINDOW_TITLE) @@ -186,14 +187,16 @@ def create_actions(self): open_help_action.triggered.connect(self.open_docs) self.open_help_action = open_help_action - # done this way expecting the value "display_sliders" being stored - # in configuration in a future. + # 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) + self.__prev_call_vis_sliders_state = False # Always initially false, used by Project when editing + # through sliders_view_enabled method below to remember sliders state 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 @@ -311,6 +314,34 @@ def show_or_hide_sliders(self, do_show_sliders=None): self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) self.sliders_view_widget.hide() + def sliders_view_enabled(self,is_enabled : bool,prev_call_vis_sliders_state : bool = None): + """Makes sliders view button in menu enabled or disabled depending + on the state of the input parameters. + + Used by: project widget to control menu when project editing is enabled. + + Inputs: + ------ + is_enabled -- if True, slider state should be enabled, if False - disabled. + prev_call_vis_sliders_state -- + logical stating what sliders view widget + view state was when this method was called + """ + self._show_or_hide_slider_action.setEnabled(is_enabled) + + # Store previous slider view state in the internal variable to + # restore the state if requested + if prev_call_vis_sliders_state is None: + prev_call_vis_sliders_state = self.__prev_call_vis_sliders_state + else: + self.__prev_call_vis_sliders_state = prev_call_vis_sliders_state + + # 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) + def open_about_info(self): """Opens about menu containing information about RASCAL gui""" self.about_dialog.update_rascal_info(self) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index 77427311..69524abb 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -84,12 +84,17 @@ 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)) + 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) button_layout = QtWidgets.QHBoxLayout() button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + button_layout.addWidget(show_sliders_button) button_layout.addWidget(self.edit_project_button) + main_layout.addLayout(button_layout) settings_layout = QtWidgets.QHBoxLayout() @@ -357,11 +362,16 @@ def show_project_view(self) -> None: self.setWindowTitle("Project") self.parent.controls_widget.run_button.setEnabled(True) self.stacked_widget.setCurrentIndex(0) + self.parent.sliders_view_enabled(is_enabled=True) + def show_edit_view(self) -> None: """Show edit view""" - self.parent.show_or_hide_sliders(do_show_sliders=False) # when you show it again it contents + self.parent.sliders_view_enabled( + is_enabled=False, + prev_call_vis_sliders_state=self.parent.sliders_view_widget.isVisible() + ) # will be updated according to edit changes self.update_project_view(0) self.setWindowTitle("Edit Project") @@ -385,6 +395,8 @@ def save_changes(self) -> None: self.parent.terminal_widget.write_error(f"Could not save draft project:\n {custom_errors}") else: self.show_project_view() + self.parent.sliders_view_enabled(is_enabled=True) + def validate_draft_project(self) -> Generator[str, None, None]: """Get all errors with the draft project.""" diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 58c5bf15..78f748cd 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -259,3 +259,51 @@ def test_click_on_select_tabs_works_as_expected(mock_hide, mock_show, test_view_ 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 + +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_enable_disable_sliders_menu_without_params(mock_hide, test_view_with_mdi): + """Test if click on menu in the state "Show Sliders" changes text appropriately + and initiates correct callback + """ + assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state + assert not test_view_with_mdi.sliders_view_widget.isVisible() + + main_menu = test_view_with_mdi.menuBar() + submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") + all_actions = submenu.actions() + assert all_actions[0].isEnabled() # enabled in patch + + test_view_with_mdi.sliders_view_enabled(False) + assert not all_actions[0].isEnabled() # disabled now + assert mock_hide.call_count == 1 # this would hide sliders widget if it was visible + + test_view_with_mdi.sliders_view_enabled(True) + assert all_actions[0].isEnabled() # enabled now + assert mock_hide.call_count == 2 # still call hide as this was the previous state + assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # nothing have changed + # here as it remembers the call state + +@patch("rascal2.ui.view.SlidersViewWidget.show") +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_enable_disable_sliders_menu_with_params(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 + """ + assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state + assert not test_view_with_mdi.sliders_view_widget.isVisible() + + main_menu = test_view_with_mdi.menuBar() + submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") + all_actions = submenu.actions() + assert all_actions[0].isEnabled() # enabled in patch + + test_view_with_mdi.sliders_view_enabled(False,True) + assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remembered + assert not all_actions[0].isEnabled() + assert mock_hide.call_count == 1 # this would hide sliders widget if it was visible + + test_view_with_mdi.sliders_view_enabled(True) + assert all_actions[0].isEnabled() # enabled now + assert mock_show.call_count == 1 # call show as this was the previous prev_call_vis_sliders_state state + assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remains persistent + diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index c415da0d..58cb9f39 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -5,9 +5,9 @@ from PyQt6 import QtWidgets from rascal2.ui.view import MainWindowView -from rascal2.widgets.project.project import create_draft_project +from rascal2.widgets.project.project import create_draft_project,ProjectWidget from rascal2.widgets.project.tables import ParameterFieldWidget -from rascal2.widgets.sliders_view import EmptySlider, LabeledSlider +from rascal2.widgets.sliders_view import EmptySlider, LabeledSlider, SlidersViewWidget class MockFigureCanvas(QtWidgets.QWidget): @@ -159,6 +159,7 @@ def test_apply_cancel_changes_called_hide_sliders(view_with_proj): 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""" @@ -209,3 +210,52 @@ def test_empty_slider_removed(view_with_proj): assert len(view_with_proj.sliders_view_widget._sliders) == 1 slider1 = view_with_proj.sliders_view_widget._sliders["Param 2"] assert isinstance(slider1, LabeledSlider) + +#====================================================================================================================== +@patch.object(SlidersViewWidget,"isVisible",lambda self: True) +@patch.object(ProjectWidget,"validate_draft_project",lambda self: "Errors present") +@patch("rascal2.ui.view.ProjectWidget.update_project_view") +@patch("rascal2.ui.view.SlidersViewWidget.show") +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_hide_sliders_when_edited_restore_when_accepted(mock_hide,mock_show,mock_update,view_with_proj): + + view_with_proj.sliders_view_widget.mdi_holder = QtWidgets.QWidget() # needs for project to be defined + + edit_button = view_with_proj.project_widget.edit_project_button + edit_button.click() + assert mock_hide.call_count == 1 + assert mock_update.call_count == 1 + # show state stored + assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + + save_button = view_with_proj.project_widget.save_project_button + save_button.click() + + assert mock_show.call_count == 1 + assert mock_update.call_count == 1 # we patched save with error so should not update + # call state persistent and return does not recover anything + assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + + +@patch.object(SlidersViewWidget,"isVisible",lambda self: True) +@patch("rascal2.ui.view.ProjectWidget.update_project_view") +@patch("rascal2.ui.view.SlidersViewWidget.show") +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_hide_sliders_when_edited_restore_when_accepted(mock_hide,mock_show,mock_update,view_with_proj): + + view_with_proj.sliders_view_widget.mdi_holder = QtWidgets.QWidget() # needs for project to be defined + + edit_button = view_with_proj.project_widget.edit_project_button + edit_button.click() + assert mock_hide.call_count == 1 + assert mock_update.call_count == 1 + # show state stored + assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + + cancel_button = view_with_proj.project_widget.cancel_button + cancel_button.click() + + assert mock_show.call_count == 1 + assert mock_update.call_count == 2 + # call state persistent and return does not recover anything + assert view_with_proj._MainWindowView__prev_call_vis_sliders_state From d08054dfe3423560408bc6b62171d8df25cf1a98 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 15:06:53 +0000 Subject: [PATCH 47/69] Re #149 Ruff errors --- tests/widgets/test_sliders_widget.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 58cb9f39..f9961a93 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -5,7 +5,7 @@ from PyQt6 import QtWidgets from rascal2.ui.view import MainWindowView -from rascal2.widgets.project.project import create_draft_project,ProjectWidget +from rascal2.widgets.project.project import ProjectWidget, create_draft_project from rascal2.widgets.project.tables import ParameterFieldWidget from rascal2.widgets.sliders_view import EmptySlider, LabeledSlider, SlidersViewWidget @@ -159,7 +159,7 @@ def test_apply_cancel_changes_called_hide_sliders(view_with_proj): 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""" @@ -211,15 +211,15 @@ def test_empty_slider_removed(view_with_proj): slider1 = view_with_proj.sliders_view_widget._sliders["Param 2"] assert isinstance(slider1, LabeledSlider) -#====================================================================================================================== -@patch.object(SlidersViewWidget,"isVisible",lambda self: True) -@patch.object(ProjectWidget,"validate_draft_project",lambda self: "Errors present") + +# ====================================================================================================================== +@patch.object(SlidersViewWidget, "isVisible", lambda self: True) +@patch.object(ProjectWidget, "validate_draft_project", lambda self: "Errors present") @patch("rascal2.ui.view.ProjectWidget.update_project_view") @patch("rascal2.ui.view.SlidersViewWidget.show") @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_hide_sliders_when_edited_restore_when_accepted(mock_hide,mock_show,mock_update,view_with_proj): - - view_with_proj.sliders_view_widget.mdi_holder = QtWidgets.QWidget() # needs for project to be defined +def test_hide_sliders_when_edited_restore_when_accepted(mock_hide, mock_show, mock_update, view_with_proj): + view_with_proj.sliders_view_widget.mdi_holder = QtWidgets.QWidget() # needs for project to be defined edit_button = view_with_proj.project_widget.edit_project_button edit_button.click() @@ -232,18 +232,17 @@ def test_hide_sliders_when_edited_restore_when_accepted(mock_hide,mock_show,mock save_button.click() assert mock_show.call_count == 1 - assert mock_update.call_count == 1 # we patched save with error so should not update + assert mock_update.call_count == 1 # we patched save with error so should not update # call state persistent and return does not recover anything assert view_with_proj._MainWindowView__prev_call_vis_sliders_state -@patch.object(SlidersViewWidget,"isVisible",lambda self: True) +@patch.object(SlidersViewWidget, "isVisible", lambda self: True) @patch("rascal2.ui.view.ProjectWidget.update_project_view") @patch("rascal2.ui.view.SlidersViewWidget.show") @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_hide_sliders_when_edited_restore_when_accepted(mock_hide,mock_show,mock_update,view_with_proj): - - view_with_proj.sliders_view_widget.mdi_holder = QtWidgets.QWidget() # needs for project to be defined +def test_hide_sliders_when_edited_restore_when_canceled(mock_hide, mock_show, mock_update, view_with_proj): + view_with_proj.sliders_view_widget.mdi_holder = QtWidgets.QWidget() # needs for project to be defined edit_button = view_with_proj.project_widget.edit_project_button edit_button.click() From 1ff90b7d6b4d4515a5a4ec7d7ea3e8f83b80611f Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 15:11:29 +0000 Subject: [PATCH 48/69] Re #149 ruff formatting --- rascal2/ui/view.py | 4 ++-- rascal2/widgets/project/project.py | 10 +++------- tests/ui/test_view.py | 23 ++++++++++++----------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 203b46e4..d18d79d8 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -195,7 +195,7 @@ def create_actions(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) - self.__prev_call_vis_sliders_state = False # Always initially false, used by Project when editing + self.__prev_call_vis_sliders_state = False # Always initially false, used by Project when editing # through sliders_view_enabled method below to remember sliders state show_or_hide_slider_action.setStatusTip("Show or Hide Sliders") show_or_hide_slider_action.triggered.connect(lambda: self.show_or_hide_sliders(None)) @@ -314,7 +314,7 @@ def show_or_hide_sliders(self, do_show_sliders=None): self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) self.sliders_view_widget.hide() - def sliders_view_enabled(self,is_enabled : bool,prev_call_vis_sliders_state : bool = None): + def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bool = None): """Makes sliders view button in menu enabled or disabled depending on the state of the input parameters. diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index 69524abb..4659175c 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -84,8 +84,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, objectName="ShowSliders") + show_sliders_button.clicked.connect(lambda: self.parent.show_or_hide_sliders(True)) 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) @@ -94,7 +94,6 @@ def create_project_view(self) -> QtWidgets.QWidget: button_layout.addWidget(show_sliders_button) button_layout.addWidget(self.edit_project_button) - main_layout.addLayout(button_layout) settings_layout = QtWidgets.QHBoxLayout() @@ -364,13 +363,11 @@ def show_project_view(self) -> None: self.stacked_widget.setCurrentIndex(0) self.parent.sliders_view_enabled(is_enabled=True) - def show_edit_view(self) -> None: """Show edit view""" self.parent.sliders_view_enabled( - is_enabled=False, - prev_call_vis_sliders_state=self.parent.sliders_view_widget.isVisible() + is_enabled=False, prev_call_vis_sliders_state=self.parent.sliders_view_widget.isVisible() ) # will be updated according to edit changes self.update_project_view(0) @@ -397,7 +394,6 @@ def save_changes(self) -> None: self.show_project_view() self.parent.sliders_view_enabled(is_enabled=True) - def validate_draft_project(self) -> Generator[str, None, None]: """Get all errors with the draft project.""" yield from self.validate_layers() diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 78f748cd..b215f455 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -260,6 +260,7 @@ def test_click_on_select_tabs_works_as_expected(mock_hide, mock_show, test_view_ assert not test_view_with_mdi.show_sliders assert mock_hide.call_count == 1 # this would hide sliders widget + @patch("rascal2.ui.view.SlidersViewWidget.hide") def test_enable_disable_sliders_menu_without_params(mock_hide, test_view_with_mdi): """Test if click on menu in the state "Show Sliders" changes text appropriately @@ -271,21 +272,22 @@ def test_enable_disable_sliders_menu_without_params(mock_hide, test_view_with_md main_menu = test_view_with_mdi.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") all_actions = submenu.actions() - assert all_actions[0].isEnabled() # enabled in patch + assert all_actions[0].isEnabled() # enabled in patch test_view_with_mdi.sliders_view_enabled(False) - assert not all_actions[0].isEnabled() # disabled now + assert not all_actions[0].isEnabled() # disabled now assert mock_hide.call_count == 1 # this would hide sliders widget if it was visible test_view_with_mdi.sliders_view_enabled(True) - assert all_actions[0].isEnabled() # enabled now + assert all_actions[0].isEnabled() # enabled now assert mock_hide.call_count == 2 # still call hide as this was the previous state - assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # nothing have changed + assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # nothing have changed # here as it remembers the call state + @patch("rascal2.ui.view.SlidersViewWidget.show") @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_enable_disable_sliders_menu_with_params(mock_hide,mock_show, test_view_with_mdi): +def test_enable_disable_sliders_menu_with_params(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 """ @@ -295,15 +297,14 @@ def test_enable_disable_sliders_menu_with_params(mock_hide,mock_show, test_view_ main_menu = test_view_with_mdi.menuBar() submenu = main_menu.findChild(QtWidgets.QMenu, "&Tools") all_actions = submenu.actions() - assert all_actions[0].isEnabled() # enabled in patch + assert all_actions[0].isEnabled() # enabled in patch - test_view_with_mdi.sliders_view_enabled(False,True) - assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remembered + test_view_with_mdi.sliders_view_enabled(False, True) + assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remembered assert not all_actions[0].isEnabled() assert mock_hide.call_count == 1 # this would hide sliders widget if it was visible test_view_with_mdi.sliders_view_enabled(True) - assert all_actions[0].isEnabled() # enabled now + assert all_actions[0].isEnabled() # enabled now assert mock_show.call_count == 1 # call show as this was the previous prev_call_vis_sliders_state state - assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remains persistent - + assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remains persistent From 7cd7465bed566d48418e45d472b1349f65678f69 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 16:50:33 +0000 Subject: [PATCH 49/69] Re #149 simplify logic behind storing project view when project is edited --- rascal2/ui/view.py | 12 ++---------- rascal2/widgets/project/project.py | 19 +++++++++++++++---- tests/ui/test_view.py | 8 ++------ tests/widgets/test_sliders_widget.py | 8 ++++---- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index d18d79d8..9b9a8e50 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -195,8 +195,6 @@ def create_actions(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) - self.__prev_call_vis_sliders_state = False # Always initially false, used by Project when editing - # through sliders_view_enabled method below to remember sliders state 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 @@ -314,7 +312,7 @@ def show_or_hide_sliders(self, do_show_sliders=None): self._show_or_hide_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) self.sliders_view_widget.hide() - def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bool = None): + def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bool = False): """Makes sliders view button in menu enabled or disabled depending on the state of the input parameters. @@ -326,16 +324,10 @@ def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bo prev_call_vis_sliders_state -- logical stating what sliders view widget view state was when this method was called + when slider state was disabled """ self._show_or_hide_slider_action.setEnabled(is_enabled) - # Store previous slider view state in the internal variable to - # restore the state if requested - if prev_call_vis_sliders_state is None: - prev_call_vis_sliders_state = self.__prev_call_vis_sliders_state - else: - self.__prev_call_vis_sliders_state = prev_call_vis_sliders_state - # hide sliders when disabled or else if is_enabled: self.show_or_hide_sliders(do_show_sliders=prev_call_vis_sliders_state) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index 4659175c..fba8a1fa 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -77,6 +77,9 @@ def __init__(self, parent): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.stacked_widget) self.setLayout(layout) + # function holder to store/restore state of slider_view when edit project + # button is pressed + self.__slider_view_state_holder_function = None def create_project_view(self) -> QtWidgets.QWidget: """Creates the project (non-edit) view""" @@ -361,13 +364,18 @@ def show_project_view(self) -> None: self.setWindowTitle("Project") self.parent.controls_widget.run_button.setEnabled(True) self.stacked_widget.setCurrentIndex(0) - self.parent.sliders_view_enabled(is_enabled=True) + if self.__slider_view_state_holder_function is not None: + # restore previous sliders view + self.__slider_view_state_holder_function() + self.__slider_view_state_holder_function = None def show_edit_view(self) -> None: """Show edit view""" - self.parent.sliders_view_enabled( - is_enabled=False, prev_call_vis_sliders_state=self.parent.sliders_view_widget.isVisible() + sliders_visible = self.parent.sliders_view_widget.isVisible() + self.parent.sliders_view_enabled(False, sliders_visible) + self.__slider_view_state_holder_function = ( + lambda enbl=True, vis=sliders_visible: self.parent.sliders_view_enabled(enbl, vis) ) # will be updated according to edit changes self.update_project_view(0) @@ -392,7 +400,10 @@ def save_changes(self) -> None: self.parent.terminal_widget.write_error(f"Could not save draft project:\n {custom_errors}") else: self.show_project_view() - self.parent.sliders_view_enabled(is_enabled=True) + if self.__slider_view_state_holder_function is not None: + # restore previous sliders view state + self.__slider_view_state_holder_function() + self.__slider_view_state_holder_function = None def validate_draft_project(self) -> Generator[str, None, None]: """Get all errors with the draft project.""" diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index b215f455..dd3b28ad 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -266,7 +266,6 @@ def test_enable_disable_sliders_menu_without_params(mock_hide, test_view_with_md """Test if click on menu in the state "Show Sliders" changes text appropriately and initiates correct callback """ - assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state assert not test_view_with_mdi.sliders_view_widget.isVisible() main_menu = test_view_with_mdi.menuBar() @@ -281,7 +280,7 @@ def test_enable_disable_sliders_menu_without_params(mock_hide, test_view_with_md test_view_with_mdi.sliders_view_enabled(True) assert all_actions[0].isEnabled() # enabled now assert mock_hide.call_count == 2 # still call hide as this was the previous state - assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # nothing have changed + # here as it remembers the call state @@ -291,7 +290,6 @@ def test_enable_disable_sliders_menu_with_params(mock_hide, mock_show, test_view """Test if click on menu in the state "Show Sliders" changes text appropriately and initiates correct callback """ - assert not test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state assert not test_view_with_mdi.sliders_view_widget.isVisible() main_menu = test_view_with_mdi.menuBar() @@ -300,11 +298,9 @@ def test_enable_disable_sliders_menu_with_params(mock_hide, mock_show, test_view assert all_actions[0].isEnabled() # enabled in patch test_view_with_mdi.sliders_view_enabled(False, True) - assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remembered assert not all_actions[0].isEnabled() assert mock_hide.call_count == 1 # this would hide sliders widget if it was visible - test_view_with_mdi.sliders_view_enabled(True) + test_view_with_mdi.sliders_view_enabled(True, True) assert all_actions[0].isEnabled() # enabled now assert mock_show.call_count == 1 # call show as this was the previous prev_call_vis_sliders_state state - assert test_view_with_mdi._MainWindowView__prev_call_vis_sliders_state # state remains persistent diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index f9961a93..9181e196 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -226,7 +226,7 @@ def test_hide_sliders_when_edited_restore_when_accepted(mock_hide, mock_show, mo assert mock_hide.call_count == 1 assert mock_update.call_count == 1 # show state stored - assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + assert callable(view_with_proj.project_widget._ProjectWidget__slider_view_state_holder_function) save_button = view_with_proj.project_widget.save_project_button save_button.click() @@ -234,7 +234,7 @@ def test_hide_sliders_when_edited_restore_when_accepted(mock_hide, mock_show, mo assert mock_show.call_count == 1 assert mock_update.call_count == 1 # we patched save with error so should not update # call state persistent and return does not recover anything - assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + assert view_with_proj.project_widget._ProjectWidget__slider_view_state_holder_function is None @patch.object(SlidersViewWidget, "isVisible", lambda self: True) @@ -249,7 +249,7 @@ def test_hide_sliders_when_edited_restore_when_canceled(mock_hide, mock_show, mo assert mock_hide.call_count == 1 assert mock_update.call_count == 1 # show state stored - assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + assert callable(view_with_proj.project_widget._ProjectWidget__slider_view_state_holder_function) cancel_button = view_with_proj.project_widget.cancel_button cancel_button.click() @@ -257,4 +257,4 @@ def test_hide_sliders_when_edited_restore_when_canceled(mock_hide, mock_show, mo assert mock_show.call_count == 1 assert mock_update.call_count == 2 # call state persistent and return does not recover anything - assert view_with_proj._MainWindowView__prev_call_vis_sliders_state + assert view_with_proj.project_widget._ProjectWidget__slider_view_state_holder_function is None From e30d5943be7cb7b9f33ef1a553e5dd99116ec515 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 17:45:25 +0000 Subject: [PATCH 50/69] Re #149 fixed test_project --- tests/widgets/project/test_project.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 405e38e3..54b732f0 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -46,6 +46,17 @@ def show_or_hide_sliders(self, do_show_sliders=True): 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) + + + class DataModel(pydantic.BaseModel, validate_assignment=True): """A test Pydantic model.""" From e004f233c6253a409c9643994081a6901b881cc4 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 17:48:01 +0000 Subject: [PATCH 51/69] Re #149 ruff errors --- tests/widgets/project/test_project.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 54b732f0..c7ec72cc 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -47,7 +47,6 @@ def show_or_hide_sliders(self, do_show_sliders=True): 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: @@ -56,8 +55,6 @@ def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bo self.show_or_hide_sliders(do_show_sliders=False) - - class DataModel(pydantic.BaseModel, validate_assignment=True): """A test Pydantic model.""" From dbb03d00f29b912254317e477c746cfe3fc9c5e4 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 14 Nov 2025 18:17:59 +0000 Subject: [PATCH 52/69] Re #149 minor code comments --- rascal2/widgets/project/project.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index fba8a1fa..a8375d30 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -372,6 +372,9 @@ def show_project_view(self) -> None: def show_edit_view(self) -> None: """Show edit view""" + # disable possibility to enable sliders view until project is edited + # and store current state of sliders view to restore it when editing + # finishes. sliders_visible = self.parent.sliders_view_widget.isVisible() self.parent.sliders_view_enabled(False, sliders_visible) self.__slider_view_state_holder_function = ( From 38135445fadba2c616f61c50a2affae162013fe6 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 18 Nov 2025 18:58:32 +0000 Subject: [PATCH 53/69] Re #149 Changes from review --- rascal2/ui/view.py | 25 +-- rascal2/widgets/project/tables.py | 22 ++- rascal2/widgets/sliders_view.py | 253 +++++++++++++++++++-------- requirements-dev.txt | 1 - tests/widgets/project/test_models.py | 13 +- 5 files changed, 215 insertions(+), 99 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 9b9a8e50..c5e531a9 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -292,9 +292,12 @@ 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. - do_show_sliders -- if provided, sets self.show_sliders logical variable - into the requested state (True/False), forcing sliders - widget to appear/disappear + 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 @@ -314,17 +317,17 @@ def show_or_hide_sliders(self, do_show_sliders=None): def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bool = False): """Makes sliders view button in menu enabled or disabled depending - on the state of the input parameters. + on the state of the input parameters. Used by: project widget to control menu when project editing is enabled. - Inputs: - ------ - is_enabled -- if True, slider state should be enabled, if False - disabled. - prev_call_vis_sliders_state -- - logical stating what sliders view widget - view state was when this method was called - when slider state was disabled + Parameters: + ----------- + is_enabled: bool, + if True, slider state should be enabled, if False - disabled. + prev_call_vis_sliders_state : bool,default False + logical stating what sliders view widget view state was when this method was called + when slider state was disabled """ self._show_or_hide_slider_action.setEnabled(is_enabled) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index eba5692f..128439b7 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -76,11 +76,25 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): return QtCore.Qt.CheckState.Checked if data else QtCore.Qt.CheckState.Unchecked def setData( - self, index: QtCore.QModelIndex, value: float, role=QtCore.Qt.ItemDataRole.EditRole, recalculate_proj=True + self, index: QtCore.QModelIndex, value, role=QtCore.Qt.ItemDataRole.EditRole, recalculate_proj=True ) -> bool: - """Implement abstract setData method of QAbstractTableModel with additional variable - recalculate_proj -- Set it to False when modifying a bunch of properties in a loop, setting - it to True for the last value to recalculate project and update all table dependent widgets. + """Implement abstract setData method of QAbstractTableModel. + + 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. + 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. """ if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole: row = index.row() diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 7fad527e..2c9cabe4 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -31,13 +31,13 @@ def __init__(self, parent): 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 SliderUpdateHoler classes containing properties + 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 fitable values - # create initial slider view layout and everything else which depends on it + self._sliders = {} # dictionary of the sliders used to display fittable values + # create initial slider view layout and everything else which depends on it self.init() def show(self): @@ -60,6 +60,7 @@ def show(self): for window in self._parent.mdi.subWindowList(): if window.windowTitle() == "Project": self._view_geometry = window.geometry() + break self.mdi_holder.setGeometry(self._view_geometry) self.mdi_holder.show() @@ -77,11 +78,10 @@ def hide(self): self.mdi_holder.hide() def init(self) -> None: - """The main Widget window is ready so this method initializes - general contents (buttons) of the sliders widget. - If project is defined it extracts properties, used to build - sliders and generates list of sliders widgets to - control the properties. + """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.findChild(QtWidgets.QWidget, "AcceptButton") is None: self._create_slider_view_layout() @@ -96,27 +96,31 @@ def init(self) -> None: self._add_sliders_widgets() def _init_properties_for_sliders(self) -> bool: - """Loop through project's widget view tabs and morels associated with them. - Select all ParametersModel and copy all their properties which have attribute - "Fit" == True - into dictionary used to build sliders for them. Also set back-up + """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. Input: ------ - Picks up project: self._parent.project_widget -- project to get properties to change + + SlidersViewWidget with initialized Project. Returns: -------- - update_properties -- true if all properties in the project have already - had sliders, generated for them so we may update existing widgets instead of generating - new ones. + + 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. + 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 @@ -135,8 +139,7 @@ def _init_properties_for_sliders(self) -> bool: continue # data may be empty for row, model_param in enumerate(data_model.classlist): - if hasattr(model_param, "fit") and model_param.fit: # Parameters model should always - # have fit attribute, but let's be on the safe side. + 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. @@ -176,19 +179,25 @@ def _init_properties_for_sliders(self) -> bool: def _table_edit_finished_change_slider(self, index, field_name: str, slider_name: str) -> None: """Method to bind with tables delegates and change slider appearance accordingly to changes in tables. - # - # Signal about slider parameters changed is sent to sliders widget last after all other signals on - # table parameters have been processed. - # At this stage, rascal properties have already been modified, so we just modify appropriate slider appearance - # index -- QtCore.QtTable index of appropriate rascal property in correspondent GUI table. - # Duplicates slider name here so is not currently used. - # field_name - # -- string indicating changed min/max/value fields of property. May be used later to optimize changes - # but benefit of that is minuscules. - # slider_name - # -- name of the property, slider describes and key, which defines slider position in the dictionary - # of sliders + + Signal about slider parameters changed is sent to sliders widget last after all other signals on + table parameters have been processed. + At this stage, rascal properties have already been modified, so we just modify appropriate slider appearance + + Parameters: + ------------ + + index: QtCore.QtTableIndex + Index of appropriate rascal property in correspondent GUI table. + Duplicates slider name is already in dictionary here so is not currently used. + field_name: str + string indicating changed min/max/value fields of property. May be used later to optimize changes + but benefit of that is minuscules. + slider_name: str + name of the property, slider describes used as a key, which defines slider position in the dictionary + of sliders. """ + if self.isVisible(): # Do not bother otherwise, as slider appearance will be modified when made visible and # slider itself may have even been deleted self._sliders[slider_name].update_slider_display_from_property(in_constructor=False) @@ -235,7 +244,7 @@ def _add_sliders_widgets(self) -> None: content = scroll.findChild(QtWidgets.QWidget, "Scroll_content") content_layout = content.layout() - # We are adding new sliders, so delete all previous ones. Update is done in another branch. + # 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 = {} @@ -248,20 +257,21 @@ def _add_sliders_widgets(self) -> None: 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) + 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 + """ + 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): - """Cancel changes to properties obtained from sliders - and hide sliders view. + """ + Cancel changes to properties obtained from sliders and hide sliders view. """ last_call = len(self._values_to_revert) - 1 @@ -274,29 +284,32 @@ def _cancel_changes_from_sliders(self): self._parent.show_or_hide_sliders(do_show_sliders=False) def _apply_changes_from_sliders(self) -> None: - """Apply changes obtained from sliders to the project - and make them permanent + """ + 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(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 + """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: - Inputs: - ------ - row_number: int - the number of the row in the project table, which should be changed - model: rascal2.widgets.project.tables.ParametersModel - parameters model 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 + + 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 @@ -315,11 +328,18 @@ def value(self, value: float) -> None: self.param.value = value def update_value_representation(self, val: float, recalculate_project=True) -> None: - """given new value, update project table and property representations - No check are necessary as value comes from slider or back-up cache - - recalculate_project -- if True, run ratapi calculations and updates - results representation. + """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. @@ -328,9 +348,9 @@ def update_value_representation(self, val: float, recalculate_project=True) -> N class LabeledSlider(QtWidgets.QFrame): - """Class describes slider widget which - allows modifying rascal property value and its representation + """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. """ @@ -355,12 +375,15 @@ class LabeledSlider(QtWidgets.QFrame): def __init__(self, param: SliderChangeHolder): """Construct LabeledSlider for a particular property - Inputs: + + Parameters: ------- - param -- 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. + 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__() self._prop = param # hold the property controlled by slider if param is None: @@ -410,17 +433,24 @@ def __init__(self, param: SliderChangeHolder): layout.addWidget(self._slider) layout.addLayout(scale_layout) - # signal to update label dynamically + # 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)) @@ -431,7 +461,17 @@ def set_slider_gui_position(self, value: float) -> None: 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""" + """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: @@ -445,6 +485,14 @@ def update_slider_display_from_property(self, in_constructor: bool) -> None: 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. @@ -465,8 +513,13 @@ def update_slider_display_from_property(self, in_constructor: bool) -> None: 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. - Update them if they are and return True. False if they have not been changed. + """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: @@ -481,11 +534,39 @@ def _updated_from_rascal_property(self) -> bool: return updated def _value_to_slider_pos(self, value: float) -> int: - """Convert double value into slider position""" + """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 value""" + """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 @@ -493,7 +574,20 @@ def _slider_pos_to_value(self, index: int) -> float: 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) @@ -507,7 +601,18 @@ def _build_slider(self, initial_value: float) -> QtWidgets.QSlider: return slider def _update_value(self, idx: int) -> None: - """Bound in constructor to GUI slider position changed event""" + """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) @@ -523,16 +628,18 @@ class EmptySlider(LabeledSlider): def __init__(self): """Construct empty slider which have interface of LabeledSlider but no properties associated with it - Inputs: - ------ - ignored + + Parameters: + ---------- + All input parameters are ignored """ super().__init__(None) # Build all sliders widget and arrange them as expected self._slider = self._build_slider(0) name_label = QtWidgets.QLabel( - "No property to fit within the project. No sliders constructed", + "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) diff --git a/requirements-dev.txt b/requirements-dev.txt index e69135e9..674ed904 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ pytest pytest-cov -pytest-qt ruff Sphinx diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 34b4e758..c120d4f3 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -288,13 +288,6 @@ def test_param_item_delegates_exposed_to_sliders(widget_with_delegates): assert isinstance(delegate, delegates.ValueSpinBoxDelegate) -class MockEditor: - """A class with have only one method providing value. Used in test below""" - - def value(self): - return 42 - - class MockReceiver: """Test object which receives signals sent to slider""" @@ -318,11 +311,11 @@ def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): for delegate in delegates_list: delegate.edit_finished_inform_sliders.connect(lambda idx, tab_name: sr.receive_signal(idx, tab_name)) - index = widget_with_delegates.model.index(1, 1) - fed = MockEditor() + index = widget_with_delegates.model.index(1,1) + mc_editor = MagicMock() for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True), start=1): - delegate.setModelData(fed, widget_with_delegates.model, index) + delegate.setModelData(mc_editor, widget_with_delegates.model, index) assert sr.call_count == n_calls assert sr.cache_state == (index, field_name) From 4ad1d5dcd466f305a2943329b7d507077ae437d5 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 18 Nov 2025 19:28:09 +0000 Subject: [PATCH 54/69] Re #149 identified logical bug in reverting properties values --- rascal2/widgets/sliders_view.py | 29 ++++++++++++++++++++++++---- tests/widgets/test_sliders_widget.py | 15 ++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 2c9cabe4..3b08a455 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -273,16 +273,37 @@ def _cancel_changes_from_sliders(self): """ Cancel changes to properties obtained from sliders and hide sliders view. """ - last_call = len(self._values_to_revert) - 1 - for call_cnt, (key, val) in enumerate(self._values_to_revert.items()): - self._prop_to_change[key].update_value_representation( + last_changed_name = self._identify_last_changed_property() + if last_changed_name is None: # all properties value remain the same so no point + return # in reverting to them + + for name, val in self._values_to_revert.items(): + self._prop_to_change[name].update_value_representation( val, - recalculate_project=(call_cnt == last_call), # it is important to update project at last call only + recalculate_project=(name == last_changed_name), # it is important to update project for + # last changed property only not to recalculate project multiple times ) self._parent.show_or_hide_sliders(do_show_sliders=False) + def _identify_last_changed_property(self) -> str: + """Identify last changed property in the list of properties to revert. + + To update project once, loop through the list of properties to revert + and indentify the name of last changed property to ensure the project + will be updated. + + Relies on the assumption that the same loop will maintain the same + order in this procedure and _cancel_changes_from_sliders procedure + """ + + last_changed = None + for prop_name, value in self._values_to_revert.items(): + if value != self._prop_to_change[prop_name].value: + last_changed = prop_name + return last_changed + def _apply_changes_from_sliders(self) -> None: """ Apply changes obtained from sliders to the project and make them permanent diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 9181e196..2c49fbf7 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -91,6 +91,21 @@ def fake_update(self, recalculate_project): fake_update.num_calls = 0 fake_update.project_updated = [] +def test_identify_last_changed_property_none_for_unchanged(view_with_proj): + + view_with_proj.sliders_view_widget.init() + + assert view_with_proj.sliders_view_widget._identify_last_changed_property() is None + + +def test_identify_last_changed_property_picks_up_last_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 view_with_proj.sliders_view_widget._identify_last_changed_property() == "Param 3" + @patch.object(ParameterFieldWidget, "update_project", fake_update) def test_cancel_button_called(view_with_proj): From 2dc3ce54199aeca51a45f5531668a350374b87a6 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 18 Nov 2025 20:27:46 +0000 Subject: [PATCH 55/69] Re #149 added unit tests to catch up issue, identified earlier --- rascal2/widgets/sliders_view.py | 17 ++++++++--------- tests/widgets/test_sliders_widget.py | 16 ++++++++++------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 3b08a455..e45c8619 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -275,15 +275,14 @@ def _cancel_changes_from_sliders(self): """ last_changed_name = self._identify_last_changed_property() - if last_changed_name is None: # all properties value remain the same so no point - return # in reverting to them - - for name, val in self._values_to_revert.items(): - self._prop_to_change[name].update_value_representation( - val, - recalculate_project=(name == last_changed_name), # it is important to update project for - # last changed property only not to recalculate project multiple times - ) + if last_changed_name is not None: + for name, val in self._values_to_revert.items(): + self._prop_to_change[name].update_value_representation( + val, + recalculate_project=(name == last_changed_name), # 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) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 2c49fbf7..7fb9810c 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -30,6 +30,7 @@ def view_with_proj(): 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( @@ -46,17 +47,15 @@ def view_with_proj(): mw.project_widget.view_tabs["Parameters"].update_model(draft) mw.presenter.model.project = project - # project = self._parent.presenter.model.project - # prop_dictionary = create_draft_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) == 2 - assert list(view_with_proj.sliders_view_widget._prop_to_change.keys()) == ["Param 1", "Param 3"] - assert list(view_with_proj.sliders_view_widget._values_to_revert.values()) == [2.1, 209.0] + 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 @@ -74,13 +73,16 @@ def test_create_update_called(add_sliders, update_sliders, view_with_proj): 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) == 2 + 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): @@ -114,6 +116,7 @@ def test_cancel_button_called(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 cancel_button = view_with_proj.sliders_view_widget.findChild(QtWidgets.QPushButton, "CancelButton") @@ -127,6 +130,7 @@ def test_cancel_button_called(view_with_proj): 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 @patch("rascal2.ui.view.SlidersViewWidget._apply_changes_from_sliders") From 96a9fbd2fb5687384040170c2e72dee8b2abca01 Mon Sep 17 00:00:00 2001 From: abuts Date: Tue, 18 Nov 2025 20:40:01 +0000 Subject: [PATCH 56/69] Re #149 Ruff format and formal changes for git to recognise them --- rascal2/widgets/sliders_view.py | 2 +- tests/widgets/project/test_models.py | 6 +++--- tests/widgets/test_sliders_widget.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index e45c8619..b0f2848b 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -35,7 +35,7 @@ def __init__(self, parent): # 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._sliders = {} # dictionary of the sliders used to display fittable values. # create initial slider view layout and everything else which depends on it self.init() diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index c120d4f3..57b23300 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -289,20 +289,20 @@ def test_param_item_delegates_exposed_to_sliders(widget_with_delegates): class MockReceiver: - """Test object which receives signals sent to slider""" + """Test object which receives signals sent to slider.""" def __init__(self): self.cache_state = [] self.call_count = 0 def receive_signal(self, index, value): - """To bind to delegate signal""" + """To bind to delegate signal.""" self.call_count += 1 self.cache_state = (index, value) def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): - """Test if edit_finished signals emitted to subscribed clients""" + """Test if edit_finished signals emitted to subscribed clients.""" sr = MockReceiver() selected_fields = ["min", "value", "max"] diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 7fb9810c..909d93e0 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -275,5 +275,5 @@ def test_hide_sliders_when_edited_restore_when_canceled(mock_hide, mock_show, mo assert mock_show.call_count == 1 assert mock_update.call_count == 2 - # call state persistent and return does not recover anything + # call state persistent and return does not recover anything. assert view_with_proj.project_widget._ProjectWidget__slider_view_state_holder_function is None From ffbd6d6a6484827b3db55bbcf7c0c80310825afc Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 01:53:33 +0000 Subject: [PATCH 57/69] Re #149 fixing ruff format --- rascal2/widgets/sliders_view.py | 4 ++-- tests/widgets/project/test_models.py | 2 +- tests/widgets/test_sliders_widget.py | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index b0f2848b..475591d8 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -260,7 +260,7 @@ def _add_sliders_widgets(self) -> None: slider.setMaximumHeight(100) self._sliders[name] = slider - content_layout.addWidget(slider,alignment=QtCore.Qt.AlignmentFlag.AlignTop) + content_layout.addWidget(slider, alignment=QtCore.Qt.AlignmentFlag.AlignTop) def _update_sliders_widgets(self) -> None: """ @@ -658,7 +658,7 @@ def __init__(self): self._slider = self._build_slider(0) name_label = QtWidgets.QLabel( - "There are no fitted parameters.\n" + "There are no fitted parameters.\n" " Select parameters to fit in the project view to populate the sliders view.", alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) diff --git a/tests/widgets/project/test_models.py b/tests/widgets/project/test_models.py index 57b23300..5ef09a81 100644 --- a/tests/widgets/project/test_models.py +++ b/tests/widgets/project/test_models.py @@ -311,7 +311,7 @@ def test_param_item_delegates_emit_to_slider_subscribers(widget_with_delegates): for delegate in delegates_list: delegate.edit_finished_inform_sliders.connect(lambda idx, tab_name: sr.receive_signal(idx, tab_name)) - index = widget_with_delegates.model.index(1,1) + index = widget_with_delegates.model.index(1, 1) mc_editor = MagicMock() for n_calls, (delegate, field_name) in enumerate(zip(delegates_list, selected_fields, strict=True), start=1): diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 909d93e0..a04f75db 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -93,15 +93,14 @@ def fake_update(self, recalculate_project): fake_update.num_calls = 0 fake_update.project_updated = [] -def test_identify_last_changed_property_none_for_unchanged(view_with_proj): +def test_identify_last_changed_property_none_for_unchanged(view_with_proj): view_with_proj.sliders_view_widget.init() assert view_with_proj.sliders_view_widget._identify_last_changed_property() is None def test_identify_last_changed_property_picks_up_last_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 From 799d24dc5b75fe8b80031571c381bde47bc8ea47 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 11:06:10 +0000 Subject: [PATCH 58/69] Re #149 Changes from review and more reliable way of checking changed properties Changed properties do not depend on assumption of loop order remains persistent --- rascal2/ui/view.py | 10 ++++--- rascal2/widgets/sliders_view.py | 46 ++++++++++++++++----------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index c5e531a9..1a2c6998 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -375,10 +375,10 @@ def setup_mdi(self): # if windows are already created, don't set them up again, # just refresh the widget data if len(self.mdi.subWindowList()) == 5: - self.init_mdi_widgets() + self.setup_mdi_widgets() return - self.init_mdi_widgets() + self.setup_mdi_widgets() for title, widget in reversed(self._main_window_widgets.items()): widget.setWindowTitle(title) @@ -398,8 +398,10 @@ def setup_mdi(self): self.startup_dlg = self.takeCentralWidget() self.setCentralWidget(self.mdi) - def init_mdi_widgets(self): - """Performs initialization of MDI widgets that relies on the Project existing.""" + def setup_mdi_widgets(self): + """ + Performs initialization of MDI widgets that rely on the Project being defined. + """ self.controls_widget.setup_controls() self.project_widget.show_project_view() self.plot_widget.clear() diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 475591d8..5f35cb20 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -40,6 +40,8 @@ def __init__(self, parent): # create initial slider view layout and everything else which depends on it self.init() + self.__sliders_widgets_layout = None # Placeholder for the area, containing sliders widgets + def show(self): """Overload parent show method to deal with mdi container showing sliders widget window. Also sets up or updates sliders @@ -228,21 +230,21 @@ 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 """ - scroll = self.findChild(QtWidgets.QScrollArea, "Scroll") - if scroll is None: + + if self.__sliders_widgets_layout is None: main_layout = self.layout() scroll = QtWidgets.QScrollArea() scroll.setWidgetResizable(True) # important: resize content to fit area scroll.setObjectName("Scroll") main_layout.addWidget(scroll) content = QtWidgets.QWidget() - content.setObjectName("Scroll_content") scroll.setWidget(content) # --- Add content layout content_layout = QtWidgets.QVBoxLayout(content) + self.__sliders_widgets_layout = content_layout else: - content = scroll.findChild(QtWidgets.QWidget, "Scroll_content") - content_layout = content.layout() + 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(): @@ -274,34 +276,33 @@ def _cancel_changes_from_sliders(self): Cancel changes to properties obtained from sliders and hide sliders view. """ - last_changed_name = self._identify_last_changed_property() - if last_changed_name is not None: - for name, val in self._values_to_revert.items(): + 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=(name == last_changed_name), # it is important to update project for - # last changed property only not to recalculate project multiple times + 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_last_changed_property(self) -> str: - """Identify last changed property in the list of properties to revert. + def _identify_changed_properties(self) -> dict: + """Identify properties changed by sliders from initial sliders state. - To update project once, loop through the list of properties to revert - and indentify the name of last changed property to ensure the project - will be updated. - - Relies on the assumption that the same loop will maintain the same - order in this procedure and _cancel_changes_from_sliders procedure + Returns + ------ + :dict + dictionary of the initial values of properties changed by sliders. """ - last_changed = None + changed_properties = {} for prop_name, value in self._values_to_revert.items(): if value != self._prop_to_change[prop_name].value: - last_changed = prop_name - return last_changed + changed_properties[prop_name] = value + return changed_properties def _apply_changes_from_sliders(self) -> None: """ @@ -314,7 +315,7 @@ def _apply_changes_from_sliders(self) -> None: class SliderChangeHolder: """Helper class containing information necessary for update ratapi parameter and its representation - in project table view when slider position is changed + in project table view when slider position is changed. """ def __init__(self, row_number: int, model: ParametersModel, param: ratapi.models.Parameter) -> None: @@ -322,7 +323,6 @@ def __init__(self, row_number: int, model: ParametersModel, param: ratapi.models Parameters: ---------- - row_number: int the number of the row in the project table, which should be changed model: rascal2.widgets.project.tables.ParametersModel From 31578d980af309d471a5e1e50e316c71684b0c7d Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 12:16:36 +0000 Subject: [PATCH 59/69] Re #149 Changes from review. Mainly docstring format --- rascal2/widgets/project/tables.py | 6 +++--- rascal2/widgets/sliders_view.py | 28 +++++++++++----------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 128439b7..2017d624 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -80,9 +80,8 @@ def setData( ) -> bool: """Implement abstract setData method of QAbstractTableModel. - Parameters: + Parameters ---------- - index: QtCore.QModelIndex QModelIndex representing the row and column indices of edited cell wrt. the edited table value: @@ -90,11 +89,12 @@ def setData( 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. """ if role == QtCore.Qt.ItemDataRole.EditRole or role == QtCore.Qt.ItemDataRole.CheckStateRole: row = index.row() diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 5f35cb20..42ce6541 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -106,14 +106,10 @@ def _init_properties_for_sliders(self) -> bool: dictionary to reset properties values back to their initial values if "Cancel" button is pressed. - Input: - ------ - - SlidersViewWidget with initialized Project. + Requests: SlidersViewWidget with initialized Project. - Returns: + 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. @@ -188,7 +184,6 @@ def _table_edit_finished_change_slider(self, index, field_name: str, slider_name Parameters: ------------ - index: QtCore.QtTableIndex Index of appropriate rascal property in correspondent GUI table. Duplicates slider name is already in dictionary here so is not currently used. @@ -375,16 +370,8 @@ class LabeledSlider(QtWidgets.QFrame): obtained from property. """ - # Instance attributes generator - # Defaults for property min/max. Will be overwritten from actual input property - _value_min: float | None = 0 # minimal value property may have - _value_max: float | None = 100 # maximal value property may have - _value: float | None = 50 # cache for property value - _value_range: float | None = 100 # difference between maximal and minimal values of the property - _value_step: float | None = 1 # the change in property value per single step slider move - # Class attributes of slider widget which usually remain the same for all classes. - # Do not override unless in __init__ method + # 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 @@ -408,6 +395,13 @@ def __init__(self, param: SliderChangeHolder): self._prop = param # hold the property controlled by slider if param is None: return + # Defaults for property min/max. Will be overwritten from actual input property + _value_min: float | None = 0 # minimal value property may have + _value_max: float | None = 100 # maximal value property may have + _value: float | None = 50 # cache for property value + _value_range: float | None = 100 # difference between maximal and minimal values of the property + _value_step: float | None = 1 # the change in property value per single step slider move + self._labels = [] # list of slider labels describing sliders axis self.__block_slider_value_changed_signal = False @@ -462,7 +456,7 @@ def __init__(self, param: SliderChangeHolder): self.setMaximumHeight(self._slider.height()) def set_slider_gui_position(self, value: float) -> None: - """Set specified slider GUI position programmatically + """Set specified slider GUI position programmatically. As value assumed to be already correct, block signal for change, associated with slider position change in GUI From d27a228d75b84b7d964a3364958681b4f3f90d59 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 12:25:40 +0000 Subject: [PATCH 60/69] Re #149 Ruff errors and changes from review. Mainly docstring format. --- rascal2/widgets/sliders_view.py | 56 ++++++++++++++------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 42ce6541..2f686a21 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -40,7 +40,7 @@ def __init__(self, parent): # create initial slider view layout and everything else which depends on it self.init() - self.__sliders_widgets_layout = None # Placeholder for the area, containing sliders widgets + self.__sliders_widgets_layout = None # Placeholder for the area, containing sliders widgets def show(self): """Overload parent show method to deal with mdi container @@ -182,8 +182,8 @@ def _table_edit_finished_change_slider(self, index, field_name: str, slider_name table parameters have been processed. At this stage, rascal properties have already been modified, so we just modify appropriate slider appearance - Parameters: - ------------ + Parameters + ---------- index: QtCore.QtTableIndex Index of appropriate rascal property in correspondent GUI table. Duplicates slider name is already in dictionary here so is not currently used. @@ -240,7 +240,6 @@ def _add_sliders_widgets(self) -> None: 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() @@ -273,7 +272,7 @@ def _cancel_changes_from_sliders(self): changed_properties = self._identify_changed_properties() if len(changed_properties) > 0: - last_changed_prop_num = len(changed_properties)-1 + 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, @@ -288,7 +287,7 @@ def _identify_changed_properties(self) -> dict: """Identify properties changed by sliders from initial sliders state. Returns - ------ + ------- :dict dictionary of the initial values of properties changed by sliders. """ @@ -316,7 +315,7 @@ class SliderChangeHolder: def __init__(self, row_number: int, model: ParametersModel, param: ratapi.models.Parameter) -> None: """Class Initialization function: - Parameters: + Parameters ---------- row_number: int the number of the row in the project table, which should be changed @@ -347,8 +346,8 @@ def update_value_representation(self, val: float, recalculate_project=True) -> N No checks are necessary as value comes from slider or undo cache - Parameters: - ----------- + Parameters + ---------- val: float new value to set up slider position according to the slider's numerical scale (recalculated into actual integer position) @@ -383,8 +382,8 @@ class LabeledSlider(QtWidgets.QFrame): def __init__(self, param: SliderChangeHolder): """Construct LabeledSlider for a particular property - Parameters: - ------- + 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 @@ -461,7 +460,7 @@ def set_slider_gui_position(self, value: float) -> None: As value assumed to be already correct, block signal for change, associated with slider position change in GUI - Parameters: + Parameters ---------- value: float new float value of the slider @@ -477,9 +476,8 @@ def set_slider_gui_position(self, value: float) -> None: def update_slider_parameters(self, param: SliderChangeHolder, in_constructor=False): """Modifies slider values which may change for this slider from his parent property - Parameters: - ----------- - + Parameters + ---------- param: SliderChangeHolder instance of the SliderChangeHolder class, containing updated values for the slider in_constructor: bool,default False @@ -502,7 +500,6 @@ def update_slider_display_from_property(self, in_constructor: bool) -> None: 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 @@ -532,7 +529,6 @@ def _updated_from_rascal_property(self) -> bool: Returns: ------- - True if change detected and False otherwise """ updated = False @@ -552,14 +548,12 @@ def _value_to_slider_pos(self, value: float) -> int: 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 """ @@ -568,15 +562,13 @@ def _value_to_slider_pos(self, value: float) -> int: def _slider_pos_to_value(self, index: int) -> float: """Convert slider GUI position (index) into double property value - Parameters: - ----------- - + Parameters + ---------- index : int integer position within 0-self._slider_max_idx range to process - Returns: - -------- - + Returns + ------- value : float double value within slider's min-max range corresponding to input index """ @@ -591,15 +583,14 @@ def _build_slider(self, initial_value: float) -> QtWidgets.QSlider: Part of slider constructor - Parameters: - ----------- + Parameters + ---------- value : float double value within slider's min-max range to identify integer position corresponding to this value. - Returns: - -------- - + Returns + ------- QtWidgets.QSlider instance with settings, corresponding to input parameters. """ @@ -620,9 +611,8 @@ def _update_value(self, idx: int) -> None: Bound in constructor to GUI slider position changed event - Parameters: + Parameters ---------- - idx : int integer position of slider deal in GUI @@ -643,7 +633,7 @@ def __init__(self): """Construct empty slider which have interface of LabeledSlider but no properties associated with it - Parameters: + Parameters ---------- All input parameters are ignored """ From 9583d78bb4b4cd8cda40d98f3959842163035ecc Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 12:45:20 +0000 Subject: [PATCH 61/69] Re #149 fixing bugs caused by recent changes --- rascal2/widgets/sliders_view.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index 2f686a21..a759e8ee 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -37,10 +37,11 @@ def __init__(self, parent): self._sliders = {} # dictionary of the sliders used to display fittable values. + self.__sliders_widgets_layout = None # Placeholder for the area, containing sliders widgets + # create initial slider view layout and everything else which depends on it self.init() - self.__sliders_widgets_layout = None # Placeholder for the area, containing sliders widgets def show(self): """Overload parent show method to deal with mdi container @@ -391,15 +392,16 @@ def __init__(self, param: SliderChangeHolder): """ 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 - # Defaults for property min/max. Will be overwritten from actual input property - _value_min: float | None = 0 # minimal value property may have - _value_max: float | None = 100 # maximal value property may have - _value: float | None = 50 # cache for property value - _value_range: float | None = 100 # difference between maximal and minimal values of the property - _value_step: float | None = 1 # the change in property value per single step slider move self._labels = [] # list of slider labels describing sliders axis self.__block_slider_value_changed_signal = False @@ -638,8 +640,6 @@ def __init__(self): All input parameters are ignored """ super().__init__(None) - # Build all sliders widget and arrange them as expected - self._slider = self._build_slider(0) name_label = QtWidgets.QLabel( "There are no fitted parameters.\n" From 2ee9fa23f85db8e066c88173bde5991ad286521d Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 12:47:00 +0000 Subject: [PATCH 62/69] Re #149 ruff errors --- rascal2/widgets/sliders_view.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index a759e8ee..e8c5eb39 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -42,7 +42,6 @@ def __init__(self, parent): # create initial slider view layout and everything else which depends on it self.init() - def show(self): """Overload parent show method to deal with mdi container showing sliders widget window. Also sets up or updates sliders @@ -393,11 +392,11 @@ def __init__(self, param: SliderChangeHolder): 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_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._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: From e9a231c23c827231593a9157cf052f8472dadf12 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 13:51:23 +0000 Subject: [PATCH 63/69] Re #149 fixed unit tests related to changed properties --- tests/widgets/test_sliders_widget.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index a04f75db..5f839073 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -94,18 +94,19 @@ def fake_update(self, recalculate_project): fake_update.project_updated = [] -def test_identify_last_changed_property_none_for_unchanged(view_with_proj): +def test_identify_changed_properties_empty_for_unchanged(view_with_proj): view_with_proj.sliders_view_widget.init() - assert view_with_proj.sliders_view_widget._identify_last_changed_property() is None + assert len(view_with_proj.sliders_view_widget._identify_changed_properties()) == 0 -def test_identify_last_changed_property_picks_up_last_changed(view_with_proj): +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 view_with_proj.sliders_view_widget._identify_last_changed_property() == "Param 3" + 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) From eea2eba19c88cda81f121974c1bf5994da60c420 Mon Sep 17 00:00:00 2001 From: abuts Date: Wed, 19 Nov 2025 16:22:10 +0000 Subject: [PATCH 64/69] Re #149 minor comments --- rascal2/widgets/sliders_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index e8c5eb39..05d6faa9 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -266,8 +266,8 @@ def _update_sliders_widgets(self) -> None: self._sliders[name].update_slider_parameters(prop) def _cancel_changes_from_sliders(self): - """ - Cancel changes to properties obtained from sliders and hide sliders view. + """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() @@ -289,7 +289,7 @@ def _identify_changed_properties(self) -> dict: Returns ------- :dict - dictionary of the initial values of properties changed by sliders. + dictionary of the original values for properties changed by sliders. """ changed_properties = {} From ff95014204a9e69e45ffd5ef4348cf698713ea3e Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 28 Nov 2025 17:44:04 +0000 Subject: [PATCH 65/69] Re XXX fixing merge errors --- rascal2/ui/view.py | 6 +- rascal2/widgets/project/project.py | 22 -- rascal2/widgets/project/slider_view.py | 264 ---------------------- tests/widgets/project/test_project.py | 4 +- tests/widgets/project/test_slider_view.py | 125 ---------- 5 files changed, 5 insertions(+), 416 deletions(-) delete mode 100644 rascal2/widgets/project/slider_view.py delete mode 100644 tests/widgets/project/test_slider_view.py diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index e2b51282..9ee930a6 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -319,13 +319,13 @@ def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bo logical stating what sliders view widget view state was when this method was called when slider state was disabled """ - self._show_or_hide_slider_action.setEnabled(is_enabled) + self._toggle_slider_action.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) + self.toggle_sliders(do_show_sliders=prev_call_vis_sliders_state) else: - self.show_or_hide_sliders(do_show_sliders=False) + self.toggle_sliders(do_show_sliders=False) def open_about_info(self): """Opens about menu containing information about RASCAL gui""" diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index cf933da9..4300d75d 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -11,7 +11,6 @@ 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, @@ -42,7 +41,6 @@ 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 = { @@ -250,26 +248,6 @@ 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 deleted file mode 100644 index 80fc0cbe..00000000 --- a/rascal2/widgets/project/slider_view.py +++ /dev/null @@ -1,264 +0,0 @@ -"""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/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 56a0961f..7b14ef71 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -50,9 +50,9 @@ def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bo 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) + self.toggle_sliders(do_show_sliders=prev_call_vis_sliders_state) else: - self.show_or_hide_sliders(do_show_sliders=False) + self.toggle_sliders(do_show_sliders=False) 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 deleted file mode 100644 index c57aa0d5..00000000 --- a/tests/widgets/project/test_slider_view.py +++ /dev/null @@ -1,125 +0,0 @@ -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() From 13d6b4c2f8f2c027673df751b37ae755dfc002e3 Mon Sep 17 00:00:00 2001 From: abuts Date: Fri, 28 Nov 2025 17:55:36 +0000 Subject: [PATCH 66/69] Re XXX fixed test failing due to toggle_sliders rename --- tests/widgets/test_sliders_widget.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/widgets/test_sliders_widget.py b/tests/widgets/test_sliders_widget.py index 5f839073..4ea4bf83 100644 --- a/tests/widgets/test_sliders_widget.py +++ b/tests/widgets/test_sliders_widget.py @@ -155,27 +155,27 @@ def test_cancel_cancel_button_connections(mock_cancel, view_with_proj): 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 +def fake_toggle_sliders(self, do_show_sliders): + fake_toggle_sliders.num_calls = +1 + fake_toggle_sliders.call_param = do_show_sliders -fake_show_or_hide_sliders.num_calls = 0 -fake_show_or_hide_sliders.call_param = [] +fake_toggle_sliders.num_calls = 0 +fake_toggle_sliders.call_param = [] -@patch.object(MainWindowView, "show_or_hide_sliders", fake_show_or_hide_sliders) +@patch.object(MainWindowView, "toggle_sliders", fake_toggle_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 + assert fake_toggle_sliders.num_calls == 1 + assert not fake_toggle_sliders.call_param - fake_show_or_hide_sliders.num_calls = 0 - fake_show_or_hide_sliders.call_param = [] + fake_toggle_sliders.num_calls = 0 + fake_toggle_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 + assert fake_toggle_sliders.num_calls == 1 + assert not fake_toggle_sliders.call_param # ====================================================================================================================== From ab3b5fe9448d1b8d36736696fdcc38f3b76d4c98 Mon Sep 17 00:00:00 2001 From: abuts Date: Sun, 30 Nov 2025 15:23:59 +0000 Subject: [PATCH 67/69] Re XXX fix tests failing due to merge with master --- rascal2/ui/view.py | 34 ++++++++++------------- rascal2/widgets/project/tables.py | 2 +- tests/ui/test_view.py | 45 +++++++++++++++++-------------- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 9ee930a6..be701640 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -47,12 +47,6 @@ def __init__(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.create_actions() self.add_submenus() @@ -184,19 +178,17 @@ def create_actions(self): 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(lambda: self.toggle_sliders(None)) # 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 - toggle_slider_action = QtGui.QAction(self._sliders_menu_control_text["HideSliders"], self) - else: - # if display_sliders state is False, action will be show - toggle_slider_action = QtGui.QAction(self._sliders_menu_control_text["ShowSliders"], self) - toggle_slider_action.setStatusTip("Show or Hide Sliders") - toggle_slider_action.triggered.connect(lambda: self.toggle_sliders(None)) - self._toggle_slider_action = toggle_slider_action - self._toggle_slider_action.setEnabled(False) - self.disabled_elements.append(self._toggle_slider_action) + self.toggle_sliders(self.show_sliders) + if not self.show_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") @@ -269,6 +261,7 @@ def add_submenus(self): self.disabled_elements.append(windows_menu) tools_menu = main_menu.addMenu("&Tools") + tools_menu.setObjectName("&Tools") tools_menu.addAction(self._toggle_slider_action) tools_menu.addSeparator() tools_menu.addAction(self.clear_terminal_action) @@ -279,7 +272,7 @@ def add_submenus(self): help_menu.addAction(self.open_about_action) help_menu.addAction(self.open_help_action) - def toggle_sliders(self,do_show_sliders=None): + def toggle_sliders(self, do_show_sliders=None): """Depending on current state, show or hide sliders for table properties within Project class view. @@ -299,11 +292,12 @@ def toggle_sliders(self,do_show_sliders=None): return if self.show_sliders: - self._toggle_slider_action.setText(self._sliders_menu_control_text["HideSliders"]) + sliders_text = self._toggle_slider_action.property("hide_text") self.sliders_view_widget.show() else: - self._toggle_slider_action.setText(self._sliders_menu_control_text["ShowSliders"]) + sliders_text = self._toggle_slider_action.property("show_text") self.sliders_view_widget.hide() + self._toggle_slider_action.setText(sliders_text) def sliders_view_enabled(self, is_enabled: bool, prev_call_vis_sliders_state: bool = False): """Makes sliders view button in menu enabled or disabled depending diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 50efbf7e..74eedea8 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -78,7 +78,7 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole): def setData( self, index: QtCore.QModelIndex, value, role=QtCore.Qt.ItemDataRole.EditRole, recalculate_proj=True ) -> bool: - """Implement abstract setData method of QAbstractTableModel + """Implement abstract setData method of QAbstractTableModel and sets the data of a given index in the table model Parameters diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 7edcc52a..156f4fdc 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -191,28 +191,15 @@ 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 -def test_toggle_slider(): - mw = MainWindowView() - 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() @pytest.fixture def test_view_with_mdi(): @@ -227,6 +214,24 @@ def test_view_with_mdi(): mw.enable_elements() return mw +@patch("rascal2.ui.view.SlidersViewWidget.show") +@patch("rascal2.ui.view.SlidersViewWidget.hide") +def test_toggle_slider(mock_hide, mock_show,test_view_with_mdi): + mw = test_view_with_mdi + + + 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 + mw.toggle_sliders() + + assert mw._toggle_slider_action.text() == hide_text + mock_show.assert_called_once() + + mw.toggle_sliders() + assert mw._toggle_slider_action.text() == show_text + mock_hide.assert_called_once() + @patch("rascal2.ui.view.SlidersViewWidget.show") @patch("rascal2.ui.view.SlidersViewWidget.hide") @@ -245,7 +250,7 @@ def test_click_on_select_sliders_works_as_expected(mock_hide, mock_show, test_vi # Trigger the action all_actions[0].trigger() - assert all_actions[0].text() == "&Hide Sliders" + assert all_actions[0].text() == "Hide &Sliders" assert test_view_with_mdi.show_sliders assert mock_show.call_count == 1 @@ -272,7 +277,7 @@ def test_click_on_select_tabs_works_as_expected(mock_hide, mock_show, test_view_ # check if next click returns to initial state all_actions[0].trigger() - assert all_actions[0].text() == "&Show Sliders" + 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 From 7db4742f6bff3dce1c9d0614346b6585012c983c Mon Sep 17 00:00:00 2001 From: abuts Date: Sun, 30 Nov 2025 15:26:52 +0000 Subject: [PATCH 68/69] Re XXX modify sliders to use standard events blocking method instead of custom one --- rascal2/widgets/sliders_view.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rascal2/widgets/sliders_view.py b/rascal2/widgets/sliders_view.py index d525a1e8..af3193e8 100644 --- a/rascal2/widgets/sliders_view.py +++ b/rascal2/widgets/sliders_view.py @@ -403,7 +403,6 @@ def __init__(self, param: SliderChangeHolder): 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 @@ -470,9 +469,10 @@ def set_slider_gui_position(self, value: float) -> None: 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.blockSignals(True) self._slider.setValue(idx) - self.__block_slider_value_changed_signal = False + self._slider.blockSignals(False) def update_slider_parameters(self, param: SliderChangeHolder, in_constructor=False): """Modifies slider values which may change for this slider from his parent property @@ -618,8 +618,6 @@ def _update_value(self, idx: int) -> None: 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)) From fd33b9b6441d8fb84e490a7334598ce480019c63 Mon Sep 17 00:00:00 2001 From: abuts Date: Sun, 30 Nov 2025 15:29:21 +0000 Subject: [PATCH 69/69] Re XXX Ruff formatting --- tests/ui/test_view.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ui/test_view.py b/tests/ui/test_view.py index 156f4fdc..4ab7d776 100644 --- a/tests/ui/test_view.py +++ b/tests/ui/test_view.py @@ -201,6 +201,7 @@ def test_help_menu_actions_present(test_view, submenu_name, action_names_and_lay 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 @@ -214,12 +215,12 @@ def test_view_with_mdi(): mw.enable_elements() return mw + @patch("rascal2.ui.view.SlidersViewWidget.show") @patch("rascal2.ui.view.SlidersViewWidget.hide") -def test_toggle_slider(mock_hide, mock_show,test_view_with_mdi): +def test_toggle_slider(mock_hide, mock_show, test_view_with_mdi): mw = test_view_with_mdi - 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