diff --git a/Framework/Properties/Mantid.properties.template b/Framework/Properties/Mantid.properties.template index 0dcf657e4b0a..773aa9fc36a5 100644 --- a/Framework/Properties/Mantid.properties.template +++ b/Framework/Properties/Mantid.properties.template @@ -258,7 +258,7 @@ plots.y_min = plots.y_max = # The default width of the lines for the axes -plots.axesLineWidth = 1 +plots.axesLineWidth = 1.0 # Whether to enable the grid by default plots.enableGrid = Off @@ -300,7 +300,7 @@ plots.line.Width = 1.5 plots.marker.Style = None # Default maker size on 1d plots -plots.marker.Size = 6 +plots.marker.Size = 6.0 # Default cap size on error bars in 1d plots plots.errorbar.Capsize = 0.0 @@ -324,7 +324,7 @@ plots.errorbar.MarkerSize = 4 plots.legend.Location = best # Default legend size -plots.legend.FontSize = 8 +plots.legend.FontSize = 8.0 # Default colormap for image plots plots.images.Colormap = viridis diff --git a/docs/source/release/v6.13.0/Workbench/New_features/38799.rst b/docs/source/release/v6.13.0/Workbench/New_features/38799.rst new file mode 100644 index 000000000000..5fb6945e29de --- /dev/null +++ b/docs/source/release/v6.13.0/Workbench/New_features/38799.rst @@ -0,0 +1 @@ +- The settings widget 'Apply' button is now disabled when there are no pending changes. diff --git a/qt/applications/workbench/CMakeLists.txt b/qt/applications/workbench/CMakeLists.txt index 147adf008867..46840660f85e 100644 --- a/qt/applications/workbench/CMakeLists.txt +++ b/qt/applications/workbench/CMakeLists.txt @@ -154,10 +154,16 @@ set(TEST_FILES workbench/widgets/settings/test/test_settings_model.py workbench/widgets/settings/test/test_settings_presenter.py workbench/widgets/settings/test/test_settings_view.py + workbench/widgets/settings/base_classes/test/test_config_settings_changes_model.py + workbench/widgets/settings/base_classes/test/test_config_settings_presenter.py workbench/widgets/settings/general/test/test_general_settings.py + workbench/widgets/settings/general/test/test_general_settings_model.py workbench/widgets/settings/plots/test/test_plot_settings.py + workbench/widgets/settings/plots/test/test_plot_settings_model.py workbench/widgets/settings/categories/test/test_categories_settings.py + workbench/widgets/settings/categories/test/test_categories_settings_model.py workbench/widgets/settings/fitting/test/test_fitting_settings.py + workbench/widgets/settings/fitting/test/test_fitting_settings_model.py workbench/projectrecovery/test/test_projectrecovery.py workbench/projectrecovery/test/test_projectrecoveryloader.py workbench/projectrecovery/test/test_projectrecoverysaver.py diff --git a/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_changes_model.py b/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_changes_model.py index 8d987ad3ee6b..f1e1e460f32b 100644 --- a/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_changes_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_changes_model.py @@ -4,12 +4,13 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +from abc import ABC from typing import Dict, List from mantid.kernel import ConfigService -class ConfigSettingsChangesModel: +class ConfigSettingsChangesModel(ABC): def __init__(self): self._changes: Dict[str, str] = {} @@ -29,7 +30,7 @@ def apply_changes(self) -> None: def add_change(self, property_string: str, value: str) -> None: saved_value = self.get_saved_value(property_string) - if saved_value != value: + if saved_value.lower().rstrip() != value.lower().rstrip(): self._changes[property_string] = value elif property_string in self._changes.keys(): self._changes.pop(property_string) diff --git a/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_presenter.py b/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_presenter.py new file mode 100644 index 000000000000..6b2825e4252f --- /dev/null +++ b/qt/applications/workbench/workbench/widgets/settings/base_classes/config_settings_presenter.py @@ -0,0 +1,22 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2025 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source, +# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +# SPDX - License - Identifier: GPL - 3.0 + +from abc import ABC + +from workbench.widgets.settings.base_classes.config_settings_changes_model import ConfigSettingsChangesModel + + +class SettingsPresenterBase(ABC): + def __init__(self, model: ConfigSettingsChangesModel): + self._model: ConfigSettingsChangesModel = model + self._parent_presenter = None + + def notify_changes(self): + if self._parent_presenter is not None: + self._parent_presenter.changes_updated(self._model.has_unsaved_changes()) + + def subscribe_parent_presenter(self, parent_presenter): + self._parent_presenter = parent_presenter diff --git a/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_changes_model.py b/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_changes_model.py index 0e0e22f34d7a..679ff62f98ea 100644 --- a/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_changes_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_changes_model.py @@ -4,7 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + -from unittest import TestCase +import unittest from unittest.mock import patch, call from workbench.widgets.settings.base_classes.config_settings_changes_model import ConfigSettingsChangesModel @@ -12,7 +12,7 @@ @patch(BASE_CLASS_CONFIG_SERVICE_PATCH_PATH, new_callable=MockConfigService) -class ConfigSettingsChangesModelTest(TestCase): +class ConfigSettingsChangesModelTest(unittest.TestCase): def setUp(self) -> None: self.model = ConfigSettingsChangesModel() @@ -88,3 +88,7 @@ def test_apply_changes(self, mock_config_service: MockConfigService): mock_config_service.setString.assert_has_calls([call("property.1", "blue"), call("property.2", "apple")]) self.assertEqual(self.model.get_changes(), {}) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_presenter.py b/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_presenter.py new file mode 100644 index 000000000000..50301040f1db --- /dev/null +++ b/qt/applications/workbench/workbench/widgets/settings/base_classes/test/test_config_settings_presenter.py @@ -0,0 +1,33 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2025 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source, +# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +# SPDX - License - Identifier: GPL - 3.0 + +import unittest +from unittest.mock import MagicMock + +from workbench.widgets.settings.base_classes.config_settings_presenter import SettingsPresenterBase + + +class ConfigSettingsPresenterTest(unittest.TestCase): + def setUp(self) -> None: + self.mock_model = MagicMock() + self.mock_model.has_unsaved_changes = MagicMock() + self.presenter = SettingsPresenterBase(self.mock_model) + self._mock_parent_presenter = MagicMock() + self.presenter.subscribe_parent_presenter(self._mock_parent_presenter) + + def test_notify_changes_with_changes(self): + self.mock_model.has_unsaved_changes.return_value = True + self.presenter.notify_changes() + self._mock_parent_presenter.changes_updated.assert_called_once_with(True) + + def test_notify_changes_without_changes(self): + self.mock_model.has_unsaved_changes.return_value = False + self.presenter.notify_changes() + self._mock_parent_presenter.changes_updated.assert_called_once_with(False) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/categories/presenter.py b/qt/applications/workbench/workbench/widgets/settings/categories/presenter.py index 03697dfc3056..135b32de67ee 100644 --- a/qt/applications/workbench/workbench/widgets/settings/categories/presenter.py +++ b/qt/applications/workbench/workbench/widgets/settings/categories/presenter.py @@ -4,12 +4,14 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +from workbench.widgets.settings.base_classes.config_settings_presenter import SettingsPresenterBase +from workbench.widgets.settings.categories.categories_settings_model import CategoriesSettingsModel from workbench.widgets.settings.categories.view import CategoriesSettingsView from qtpy.QtCore import Qt -class CategoriesSettings(object): +class CategoriesSettings(SettingsPresenterBase): """ Presenter of the visible categories settings section. It handles all changes to options within the section, and updates the ConfigService and workbench CONF accordingly. @@ -18,10 +20,10 @@ class CategoriesSettings(object): be handled here. """ - def __init__(self, parent, model, view=None): - self._view = view if view else CategoriesSettingsView(parent, self) + def __init__(self, parent, model: CategoriesSettingsModel, view=None): + super().__init__(model) self.parent = parent - self._model = model + self._view = view if view else CategoriesSettingsView(parent, self) self._view.algorithm_tree_widget.setHeaderLabel("Show/Hide Algorithm Categories") self._view.interface_tree_widget.setHeaderLabel("Show/Hide Interface Categories") self.set_algorithm_tree_categories() @@ -36,10 +38,12 @@ def get_view(self): def set_hidden_algorithms_string(self, _): categories_string = ";".join(self._create_hidden_categories_string(self._view.algorithm_tree_widget)) self._model.set_hidden_algorithms(categories_string) + self.notify_changes() def set_hidden_interfaces_string(self, _): categories_string = ";".join(self._create_hidden_categories_string(self._view.interface_tree_widget)) self._model.set_hidden_interfaces(categories_string) + self.notify_changes() def nested_box_clicked(self, item_clicked, column): new_state = item_clicked.checkState(column) diff --git a/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings.py b/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings.py index 7158951487a4..e02945274c75 100644 --- a/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings.py +++ b/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings.py @@ -6,7 +6,7 @@ # SPDX - License - Identifier: GPL - 3.0 + import unittest -from unittest.mock import call, Mock, ANY, MagicMock +from unittest.mock import call, Mock, ANY, MagicMock, patch from mantidqt.utils.qt.testing import start_qapplication from mantidqt.utils.testing.mocks.mock_qt import MockQWidget from mantidqt.utils.testing.strict_mock import StrictMock @@ -117,7 +117,8 @@ def test_algorithm_categories_partial_states_change_correctly_when_bottom_level_ child_item1.setCheckState.assert_called_once_with(0, Qt.Checked) parent_item.setCheckState.assert_called_once_with(0, Qt.PartiallyChecked) - def test_set_hidden_algorithms_string(self): + @patch("workbench.widgets.settings.categories.presenter.CategoriesSettings.notify_changes") + def test_set_hidden_algorithms_string(self, notify_changes_mock: MagicMock): presenter = CategoriesSettings(None, view=self.mock_view, model=self.mock_model) hidden_algorithim_string = [ i for i in sorted(self.mock_model.algorithm_and_states.keys()) if self.mock_model.algorithm_and_states[i] is True @@ -126,6 +127,7 @@ def test_set_hidden_algorithms_string(self): presenter.set_hidden_algorithms_string(None) self.mock_model.set_hidden_algorithms.assert_called_once_with(";".join(hidden_algorithim_string)) + notify_changes_mock.assert_called_once() def test_interface_state_correct_when_created(self): mock_main_window = MockMainWindow() @@ -142,7 +144,8 @@ def test_interface_state_correct_when_created(self): self.mock_view.add_checked_widget_item.assert_has_calls(expected_calls) - def test_set_hidden_interface_string(self): + @patch("workbench.widgets.settings.categories.presenter.CategoriesSettings.notify_changes") + def test_set_hidden_interface_string(self, notify_changes_mock: MagicMock): self.mock_model.get_hidden_interfaces.return_value = "" presenter = CategoriesSettings(None, view=self.mock_view, model=self.mock_model) hidden_interface_string = "Indirect; Muon; Reflectometry" @@ -150,3 +153,8 @@ def test_set_hidden_interface_string(self): presenter._create_hidden_categories_string = Mock(return_value=hidden_interface_string) presenter.set_hidden_interfaces_string(None) self.mock_model.set_hidden_interfaces.assert_called_once_with(";".join(hidden_interface_string)) + notify_changes_mock.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings_model.py b/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings_model.py index c200a7703beb..6ea60626b6fa 100644 --- a/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/categories/test/test_categories_settings_model.py @@ -4,6 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +import unittest from unittest.mock import patch, MagicMock, call from workbench.widgets.settings.categories.categories_settings_model import CategoriesSettingsModel, CategoryProperties @@ -54,3 +55,7 @@ def test_get_algorithm_factory_category_map(self): [{"Basic": "Load"}, {"Arithmetic": "Mean"}], call(), ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/fitting/presenter.py b/qt/applications/workbench/workbench/widgets/settings/fitting/presenter.py index b80abcf8f5e3..a2d171338200 100644 --- a/qt/applications/workbench/workbench/widgets/settings/fitting/presenter.py +++ b/qt/applications/workbench/workbench/widgets/settings/fitting/presenter.py @@ -4,16 +4,19 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +from workbench.widgets.settings.base_classes.config_settings_presenter import SettingsPresenterBase +from workbench.widgets.settings.fitting.fitting_settings_model import FittingSettingsModel from workbench.widgets.settings.fitting.view import FittingSettingsView from workbench.widgets.settings.view_utilities.settings_view_utilities import filter_out_mousewheel_events_from_combo_or_spin_box from qtpy.QtCore import Qt -class FittingSettings(object): - def __init__(self, parent, model, view=None): - self._view = view if view else FittingSettingsView(parent, self) +class FittingSettings(SettingsPresenterBase): + def __init__(self, parent, model: FittingSettingsModel, view=None): + super().__init__(model) self.parent = parent + self._view = view if view else FittingSettingsView(parent, self) self._model = model self.add_filters() self.add_items_to_combo_boxes() @@ -77,9 +80,10 @@ def setup_signals(self): def action_auto_background_changed(self, item_name): if item_name == "None": self._model.set_auto_background("") - return - background_string = item_name + " " + self._view.background_args.text() - self._model.set_auto_background(background_string) + else: + background_string = item_name + " " + self._view.background_args.text() + self._model.set_auto_background(background_string) + self.notify_changes() def action_background_args_changed(self): if self._view.auto_bkg.currentText() == "None": @@ -87,15 +91,19 @@ def action_background_args_changed(self): else: background_string = self._view.auto_bkg.currentText() + " " + self._view.background_args.text() self._model.set_auto_background(background_string) + self.notify_changes() def action_default_peak_changed(self, item_name): self._model.set_default_peak(item_name) + self.notify_changes() def action_find_peaks_fwhm_changed(self, value): self._model.set_fwhm(str(value)) + self.notify_changes() def action_find_peaks_tolerance_changed(self, value): self._model.set_tolerance(str(value)) + self.notify_changes() def update_properties(self): self.load_current_setting_values() diff --git a/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings.py b/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings.py index f734a2295040..5b58e69bd85a 100644 --- a/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings.py +++ b/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings.py @@ -38,6 +38,7 @@ def __init__(self): MOUSEWHEEL_EVENT_FILTER_PATH = "workbench.widgets.settings.fitting.presenter.filter_out_mousewheel_events_from_combo_or_spin_box" +NOTIFY_CHANGES_PATH = "workbench.widgets.settings.fitting.presenter.FittingSettings.notify_changes" @start_qapplication @@ -72,7 +73,8 @@ def test_setup_signals(self, _): self.mock_view.findpeaks_fwhm.valueChanged.connect.assert_called_once_with(presenter.action_find_peaks_fwhm_changed) self.mock_view.findpeaks_tol.valueChanged.connect.assert_called_once_with(presenter.action_find_peaks_tolerance_changed) - def test_action_auto_background_changed(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_action_auto_background_changed(self, mock_notify_changes: MagicMock, _): self.mock_view.background_args.text = Mock(return_value="") presenter = FittingSettings(None, view=self.mock_view, model=self.mock_model) @@ -80,13 +82,17 @@ def test_action_auto_background_changed(self, _): presenter.action_auto_background_changed("None") self.mock_model.set_auto_background.assert_called_once_with("") + mock_notify_changes.assert_called_once() self.mock_model.set_auto_background.reset_mock() + mock_notify_changes.reset_mock() presenter.action_auto_background_changed("Polynomial") self.mock_model.set_auto_background.assert_called_once_with("Polynomial ") + mock_notify_changes.assert_called_once() - def test_action_background_args_changed(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_action_background_args_changed(self, mock_notify_changes: MagicMock, _): self.mock_view.auto_bkg.currentText = Mock(return_value="Polynomial") presenter = FittingSettings(None, view=self.mock_view, model=self.mock_model) @@ -95,14 +101,18 @@ def test_action_background_args_changed(self, _): self.mock_view.background_args.text = Mock(return_value="n=3") presenter.action_background_args_changed() self.mock_model.set_auto_background.assert_called_once_with("Polynomial n=3") + mock_notify_changes.assert_called_once() self.mock_model.set_auto_background.reset_mock() + mock_notify_changes.reset_mock() self.mock_view.background_args.text = Mock(return_value="n=5") presenter.action_background_args_changed() self.mock_model.set_auto_background.assert_called_once_with("Polynomial n=5") + mock_notify_changes.assert_called_once() - def test_action_background_args_changed_with_auto_background_none(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_action_background_args_changed_with_auto_background_none(self, mock_notify_changes: MagicMock, _): self.mock_view.auto_bkg.currentText = Mock(return_value="None") presenter = FittingSettings(None, view=self.mock_view, model=self.mock_model) @@ -111,36 +121,53 @@ def test_action_background_args_changed_with_auto_background_none(self, _): self.mock_view.background_args.text = Mock(return_value="n=3") presenter.action_background_args_changed() self.mock_model.set_auto_background.assert_called_once_with("") + mock_notify_changes.assert_called_once() - def test_action_default_peak_changed(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_action_default_peak_changed(self, mock_notify_changes: MagicMock, _): presenter = FittingSettings(None, view=self.mock_view, model=self.mock_model) presenter.action_default_peak_changed("None") self.mock_model.set_default_peak.assert_called_once_with("None") + mock_notify_changes.assert_called_once() self.mock_model.set_default_peak.reset_mock() + mock_notify_changes.reset_mock() presenter.action_default_peak_changed("Gaussian") self.mock_model.set_default_peak.assert_called_once_with("Gaussian") + mock_notify_changes.assert_called_once() - def test_action_find_peaks_fwhm_changed(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_action_find_peaks_fwhm_changed(self, mock_notify_changes: MagicMock, _): presenter = FittingSettings(None, view=self.mock_view, model=self.mock_model) presenter.action_find_peaks_fwhm_changed(5) self.mock_model.set_fwhm.assert_called_once_with("5") + mock_notify_changes.assert_called_once() self.mock_model.set_fwhm.reset_mock() + mock_notify_changes.reset_mock() presenter.action_find_peaks_fwhm_changed(9) self.mock_model.set_fwhm.assert_called_once_with("9") + mock_notify_changes.assert_called_once() - def test_action_find_peaks_tolerance_changed(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_action_find_peaks_tolerance_changed(self, mock_notify_changes: MagicMock, _): presenter = FittingSettings(None, view=self.mock_view, model=self.mock_model) presenter.action_find_peaks_tolerance_changed(3) self.mock_model.set_tolerance.assert_called_once_with("3") + mock_notify_changes.assert_called_once() self.mock_model.set_tolerance.reset_mock() + mock_notify_changes.reset_mock() presenter.action_find_peaks_tolerance_changed(8) self.mock_model.set_tolerance.assert_called_once_with("8") + mock_notify_changes.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings_model.py b/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings_model.py index a06a2f8ce80f..b0939a9ad23b 100644 --- a/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/fitting/test/test_fitting_settings_model.py @@ -4,6 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +import unittest from unittest.mock import MagicMock, patch, call from workbench.widgets.settings.fitting.fitting_settings_model import FittingSettingsModel, FittingProperties @@ -91,3 +92,7 @@ def test_set_tolerance(self, add_change_mock: MagicMock): self._assert_setter_with_different_values( add_change_mock, self.model.set_tolerance, ["0.5", "1"], FittingProperties.TOLERANCE.value ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/general/presenter.py b/qt/applications/workbench/workbench/widgets/settings/general/presenter.py index 9fa9d9bc0807..5c14dc5848aa 100644 --- a/qt/applications/workbench/workbench/widgets/settings/general/presenter.py +++ b/qt/applications/workbench/workbench/widgets/settings/general/presenter.py @@ -6,14 +6,16 @@ # SPDX - License - Identifier: GPL - 3.0 + from mantidqt.widgets import instrumentselector from workbench.config import SAVE_STATE_VERSION +from workbench.widgets.settings.base_classes.config_settings_presenter import SettingsPresenterBase from workbench.widgets.settings.general.view import GeneralSettingsView +from workbench.widgets.settings.general.general_settings_model import GeneralSettingsModel from workbench.widgets.settings.view_utilities.settings_view_utilities import filter_out_mousewheel_events_from_combo_or_spin_box from qtpy.QtCore import Qt from qtpy.QtGui import QFontDatabase -class GeneralSettings(object): +class GeneralSettings(SettingsPresenterBase): """ Presenter of the General settings section. It handles all changes to options within the section, and updates the ConfigService and workbench CONF accordingly. @@ -24,11 +26,11 @@ class GeneralSettings(object): WINDOW_BEHAVIOUR = ["On top", "Floating"] - def __init__(self, parent, model, view=None, settings_presenter=None): - self._view = view if view else GeneralSettingsView(parent, self) + def __init__(self, parent, model: GeneralSettingsModel, view=None, settings_presenter=None): + super().__init__(model) self.parent = parent + self._view = view if view else GeneralSettingsView(parent, self) self.settings_presenter = settings_presenter - self._model = model self.load_current_setting_values() self.setup_facilities_group() @@ -103,12 +105,15 @@ def action_font_selected(self, font): font_string = "" if font_string != font.toString(): self._model.set_font(font.toString()) + self.notify_changes() def action_window_behaviour_changed(self, text): self._model.set_window_behaviour(text) + self.notify_changes() def action_completion_enabled_modified(self, state): self._model.set_completion_enabled(bool(state)) + self.notify_changes() def setup_checkbox_signals(self): self._view.show_invisible_workspaces.stateChanged.connect(self.action_show_invisible_workspaces) @@ -129,6 +134,7 @@ def action_facility_changed(self, new_facility): # refresh the instrument selection to contain instruments about the selected facility only self._view.instrument.setFacility(new_facility) self.action_instrument_changed(self._view.instrument.currentText()) + self.notify_changes() def setup_confirmations(self): self._view.prompt_save_on_close.stateChanged.connect(self.action_prompt_save_on_close) @@ -138,15 +144,19 @@ def setup_confirmations(self): def action_prompt_save_on_close(self, state): self._model.set_prompt_save_on_close(bool(state)) + self.notify_changes() def action_prompt_save_editor_modified(self, state): self._model.set_prompt_on_save_editor_modified(bool(state)) + self.notify_changes() def action_prompt_deleting_workspace(self, state): self._model.set_prompt_on_deleting_workspace(bool(state)) + self.notify_changes() def action_use_notifications_modified(self, state): self._model.set_use_notifications("On" if bool(state) else "Off") + self.notify_changes() def load_current_setting_values(self): self._view.prompt_save_on_close.setChecked(self._model.get_prompt_save_on_close()) @@ -175,24 +185,31 @@ def load_current_setting_values(self): def action_project_recovery_enabled(self, state): self._model.set_project_recovery_enabled(str(bool(state))) + self.notify_changes() def action_time_between_recovery(self, value): self._model.set_project_recovery_time_between_recoveries(str(value)) + self.notify_changes() def action_total_number_checkpoints(self, value): self._model.set_project_recovery_number_of_checkpoints(str(value)) + self.notify_changes() def action_crystallography_convention(self, state): self._model.set_crystallography_convention("Crystallography" if state == Qt.Checked else "Inelastic") + self.notify_changes() def action_instrument_changed(self, new_instrument): self._model.set_instrument(new_instrument) + self.notify_changes() def action_show_invisible_workspaces(self, state): - self._model.set_show_invisible_workspaces(str(bool(state))) + self._model.set_show_invisible_workspaces("1" if state == Qt.Checked else "0") + self.notify_changes() def action_use_open_gl(self, state): self._model.set_use_opengl("On" if bool(state) else "Off") + self.notify_changes() def setup_layout_options(self): self.fill_layout_display() @@ -220,6 +237,7 @@ def save_layout(self): layout_dict = self.get_layout_dict() layout_dict[filename] = self.parent.saveState(SAVE_STATE_VERSION) self._model.set_user_layout(layout_dict) + self.notify_changes() self._view.new_layout_name.clear() self.fill_layout_display() self.parent.populate_layout_menu() @@ -238,6 +256,7 @@ def delete_layout(self): layout_dict = self.get_layout_dict() layout_dict.pop(layout, None) self._model.set_user_layout(layout_dict) + self.notify_changes() self.fill_layout_display() self.parent.populate_layout_menu() diff --git a/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings.py b/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings.py index 080915b8971c..799cb101bde0 100644 --- a/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings.py +++ b/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings.py @@ -68,6 +68,7 @@ def __init__(self): @start_qapplication class GeneralSettingsTest(unittest.TestCase): MOUSEWHEEL_EVENT_FILTER_PATH = "workbench.widgets.settings.general.presenter.filter_out_mousewheel_events_from_combo_or_spin_box" + NOTIFY_CHANGES_PATH = "workbench.widgets.settings.general.presenter.GeneralSettings.notify_changes" def setUp(self) -> None: self.mock_model = MockGeneralSettingsModel() @@ -75,8 +76,9 @@ def setUp(self) -> None: def assert_connected_once(self, owner, signal): self.assertEqual(1, owner.receivers(signal)) + @patch(NOTIFY_CHANGES_PATH) @patch(MOUSEWHEEL_EVENT_FILTER_PATH) - def test_filters_added_to_combo_and_spin_boxes(self, mock_mousewheel_filter): + def test_filters_added_to_combo_and_spin_boxes(self, mock_mousewheel_filter, _): presenter = GeneralSettings(None, model=GeneralSettingsModel()) calls = [ @@ -89,7 +91,8 @@ def test_filters_added_to_combo_and_spin_boxes(self, mock_mousewheel_filter): mock_mousewheel_filter.assert_has_calls(calls, any_order=True) - def test_setup_facilities_with_valid_combination(self): + @patch(NOTIFY_CHANGES_PATH) + def test_setup_facilities_with_valid_combination(self, _): mock_facility = MockFacility("facility1") self.mock_model.get_facility.return_value = mock_facility.name() mock_instrument = mock_facility.all_instruments[0] @@ -141,12 +144,14 @@ def test_setup_general_group_signals(self): self.assert_connected_once(presenter.get_view().main_font, presenter.get_view().main_font.clicked) self.assert_connected_once(presenter.get_view().window_behaviour, presenter.get_view().window_behaviour.currentTextChanged) - def test_action_facility_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_facility_changed(self, mock_notify_changes: MagicMock): self.mock_model.get_facility_names.return_value = ["facility1", "facility2"] self.mock_model.get_facility.return_value = "facility1" presenter = GeneralSettings(None, model=self.mock_model) self.mock_model.set_facility.reset_mock() self.mock_model.set_instrument.reset_mock() + mock_notify_changes.reset_mock() new_facility = "TEST_LIVE" default_test_live_instrument = "ADARA_FakeEvent" @@ -154,6 +159,7 @@ def test_action_facility_changed(self): self.mock_model.set_facility.assert_called_once_with(new_facility) self.mock_model.set_instrument.assert_called_once_with(default_test_live_instrument) + mock_notify_changes.assert_has_calls([call(), call()]) self.assertEqual(presenter.get_view().instrument.getFacility(), "TEST_LIVE") @@ -163,54 +169,74 @@ def test_setup_confirmations(self): # check that the signals are connected to something self.assert_connected_once(presenter.get_view().prompt_save_on_close, presenter.get_view().prompt_save_on_close.stateChanged) - def test_action_prompt_save_on_close(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_prompt_save_on_close(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_prompt_save_on_close(True) self.mock_model.set_prompt_save_on_close.assert_called_once_with(True) + mock_notify_changes.assert_called_once() + self.mock_model.set_prompt_save_on_close.reset_mock() + mock_notify_changes.reset_mock() presenter.action_prompt_save_on_close(False) self.mock_model.set_prompt_save_on_close.assert_called_once_with(False) + mock_notify_changes.assert_called_once() - def test_action_window_behaviour_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_window_behaviour_changed(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) values = presenter.WINDOW_BEHAVIOUR presenter.action_window_behaviour_changed(values[0]) self.mock_model.set_window_behaviour.assert_called_once_with(values[0]) + mock_notify_changes.assert_called_once() + self.mock_model.set_window_behaviour.reset_mock() + mock_notify_changes.reset_mock() presenter.action_window_behaviour_changed(values[1]) self.mock_model.set_window_behaviour.assert_called_once_with(values[1]) + mock_notify_changes.assert_called_once() - def test_action_prompt_save_editor_modified(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_prompt_save_editor_modified(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_prompt_save_editor_modified(True) self.mock_model.set_prompt_on_save_editor_modified.assert_called_once_with(True) + mock_notify_changes.assert_called_once() + self.mock_model.set_prompt_on_save_editor_modified.reset_mock() + mock_notify_changes.reset_mock() presenter.action_prompt_save_editor_modified(False) self.mock_model.set_prompt_on_save_editor_modified.assert_called_once_with(False) + mock_notify_changes.assert_called_once() - def test_action_prompt_deleting_workspace(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_prompt_deleting_workspace(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.settings_presenter = MagicMock() presenter.action_prompt_deleting_workspace(True) self.mock_model.set_prompt_on_deleting_workspace.assert_called_once_with(True) + mock_notify_changes.assert_called_once() + self.mock_model.set_prompt_on_deleting_workspace.reset_mock() + mock_notify_changes.reset_mock() presenter.action_prompt_deleting_workspace(False) self.mock_model.set_prompt_on_deleting_workspace.assert_called_once_with(False) + mock_notify_changes.assert_called_once() def test_load_current_setting_values(self): # load current setting is called automatically in the constructor @@ -228,99 +254,133 @@ def test_load_current_setting_values(self): self.mock_model.get_show_invisible_workspaces.assert_called_once() self.mock_model.get_completion_enabled.assert_called_once() - def test_action_project_recovery_enabled(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_project_recovery_enabled(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_project_recovery_enabled(True) self.mock_model.set_project_recovery_enabled.assert_called_once_with("True") + mock_notify_changes.assert_called_once() self.mock_model.set_project_recovery_enabled.reset_mock() + mock_notify_changes.reset_mock() presenter.action_project_recovery_enabled(False) self.mock_model.set_project_recovery_enabled.assert_called_once_with("False") + mock_notify_changes.assert_called_once() - def test_action_time_between_recovery(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_time_between_recovery(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) time = "6000" presenter.action_time_between_recovery(time) self.mock_model.set_project_recovery_time_between_recoveries.assert_called_once_with(time) + mock_notify_changes.assert_called_once() - def test_action_total_number_checkpoints(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_total_number_checkpoints(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) num_checkpoints = "532532" presenter.action_total_number_checkpoints(num_checkpoints) self.mock_model.set_project_recovery_number_of_checkpoints.assert_called_once_with(num_checkpoints) + mock_notify_changes.assert_called_once() - def test_action_instrument_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_instrument_changed(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) new_instr = "apples" presenter.action_instrument_changed(new_instr) - self.mock_model.set_instrument(new_instr) + self.mock_model.set_instrument.assert_called_once_with(new_instr) + mock_notify_changes.assert_called_once() - def test_action_crystallography_convention(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_crystallography_convention(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_crystallography_convention(Qt.Checked) self.mock_model.set_crystallography_convention.assert_called_once_with("Crystallography") + mock_notify_changes.assert_called_once() self.mock_model.set_crystallography_convention.reset_mock() + mock_notify_changes.reset_mock() presenter.action_crystallography_convention(Qt.Unchecked) self.mock_model.set_crystallography_convention.assert_called_once_with("Inelastic") + mock_notify_changes.assert_called_once() - def test_action_use_open_gl(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_use_open_gl(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_use_open_gl(Qt.Checked) self.mock_model.set_use_opengl.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_use_opengl.reset_mock() + mock_notify_changes.reset_mock() presenter.action_use_open_gl(Qt.Unchecked) self.mock_model.set_use_opengl.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_use_notifications_modified(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_use_notifications_modified(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_use_notifications_modified(Qt.Checked) self.mock_model.set_use_notifications.assert_called_once_with("On") + mock_notify_changes.assert_called_once() + self.mock_model.set_use_notifications.reset_mock() + mock_notify_changes.reset_mock() presenter.action_use_notifications_modified(Qt.Unchecked) self.mock_model.set_use_notifications.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_font_selected(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_font_selected(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) mock_font = Mock() mock_font.toString.return_value = "Serif" presenter.action_font_selected(mock_font) self.mock_model.set_font.assert_called_once_with("Serif") + mock_notify_changes.assert_called_once() - def test_action_show_invisible_workspaces(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_invisible_workspaces(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) - presenter.action_show_invisible_workspaces(True) - self.mock_model.set_show_invisible_workspaces.assert_called_once_with("True") + presenter.action_show_invisible_workspaces(Qt.Checked) + self.mock_model.set_show_invisible_workspaces.assert_called_once_with("1") + mock_notify_changes.assert_called_once() self.mock_model.set_show_invisible_workspaces.reset_mock() + mock_notify_changes.reset_mock() - presenter.action_show_invisible_workspaces(False) - self.mock_model.set_show_invisible_workspaces.assert_called_once_with("False") + presenter.action_show_invisible_workspaces(Qt.Unchecked) + self.mock_model.set_show_invisible_workspaces.assert_called_once_with("0") + mock_notify_changes.assert_called_once() - def test_action_completion_enabled_modified(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_completion_enabled_modified(self, mock_notify_changes: MagicMock): presenter = GeneralSettings(None, model=self.mock_model) presenter.action_completion_enabled_modified(True) self.mock_model.set_completion_enabled.assert_called_once_with(True) + mock_notify_changes.assert_called_once() + self.mock_model.set_completion_enabled.reset_mock() + mock_notify_changes.reset_mock() presenter.action_completion_enabled_modified(False) self.mock_model.set_completion_enabled.assert_called_once_with(False) + mock_notify_changes.assert_called_once() @patch(MOUSEWHEEL_EVENT_FILTER_PATH) def test_fill_layout_display(self, _): @@ -353,7 +413,8 @@ def test_get_layout_dict_key_error(self): self.assertEqual({}, presenter.get_layout_dict()) @patch(MOUSEWHEEL_EVENT_FILTER_PATH) - def test_save_layout(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_save_layout(self, mock_notify_changes: MagicMock, _): mock_view = Mock() presenter = GeneralSettings(None, view=mock_view, model=self.mock_model) # setup parent @@ -372,6 +433,7 @@ def test_save_layout(self, _): calls = [call(get_potential_update=True), call(get_potential_update=True)] self.mock_model.get_user_layout.assert_has_calls(calls) self.mock_model.set_user_layout.assert_called_once_with({"a": 1, "key": "value"}) + mock_notify_changes.assert_called_once() mock_parent.saveState.assert_called_once_with(SAVE_STATE_VERSION) mock_parent.populate_layout_menu.assert_called_once_with() @@ -397,7 +459,8 @@ def test_load_layout(self, _): mock_parent.restoreState.assert_called_once_with(test_dict["a"], SAVE_STATE_VERSION) @patch(MOUSEWHEEL_EVENT_FILTER_PATH) - def test_delete_layout(self, _): + @patch(NOTIFY_CHANGES_PATH) + def test_delete_layout(self, mock_notify_changes: MagicMock, _): mock_view = Mock() presenter = GeneralSettings(None, view=mock_view, model=self.mock_model) # setup parent @@ -416,4 +479,9 @@ def test_delete_layout(self, _): calls = [call(get_potential_update=True), call(get_potential_update=True)] self.mock_model.get_user_layout.assert_has_calls(calls) self.mock_model.set_user_layout.assert_called_once_with({}) + mock_notify_changes.assert_called_once() mock_parent.populate_layout_menu.assert_called_once_with() + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings_model.py b/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings_model.py index cd5aaca02fe8..613dbd913aa4 100644 --- a/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/general/test/test_general_settings_model.py @@ -4,6 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +import unittest from unittest.mock import MagicMock, patch, call from workbench.widgets.settings.general.general_settings_model import GeneralSettingsModel, GeneralProperties, GeneralUserConfigProperties @@ -405,3 +406,7 @@ def test_set_user_layour(self, add_change_mock: MagicMock): [{"a test": "dictionary"}, {"another": "one"}], GeneralUserConfigProperties.USER_LAYOUT.value, ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/main.ui b/qt/applications/workbench/workbench/widgets/settings/main.ui index bb084852a2c7..27e1b8f9007c 100644 --- a/qt/applications/workbench/workbench/widgets/settings/main.ui +++ b/qt/applications/workbench/workbench/widgets/settings/main.ui @@ -143,7 +143,7 @@ QListView::item { - Okay + OK diff --git a/qt/applications/workbench/workbench/widgets/settings/plots/presenter.py b/qt/applications/workbench/workbench/widgets/settings/plots/presenter.py index 6c68d698daf9..5a24b5fd2c5c 100644 --- a/qt/applications/workbench/workbench/widgets/settings/plots/presenter.py +++ b/qt/applications/workbench/workbench/widgets/settings/plots/presenter.py @@ -6,7 +6,9 @@ # SPDX - License - Identifier: GPL - 3.0 + from mantid.plots.utility import get_colormap_names from mantidqt.widgets.plotconfigdialog.curvestabwidget.markertabwidget.view import MARKER_STYLES +from workbench.widgets.settings.base_classes.config_settings_presenter import SettingsPresenterBase from workbench.widgets.settings.plots.view import PlotsSettingsView +from workbench.widgets.settings.plots.model import PlotsSettingsModel from workbench.widgets.settings.view_utilities.settings_view_utilities import filter_out_mousewheel_events_from_combo_or_spin_box from workbench.plotting.style import VALID_LINE_STYLE, VALID_DRAW_STYLE @@ -15,7 +17,7 @@ import sys -class PlotSettings(object): +class PlotSettings(SettingsPresenterBase): AXES_SCALE = ["Linear", "Log"] AXES_Y_POSITION = ["Left", "Right"] AXES_X_POSITION = ["Bottom", "Top"] @@ -32,10 +34,10 @@ class PlotSettings(object): "upper center", ] - def __init__(self, parent, model, view=None): - self._view = view if view else PlotsSettingsView(parent, self) - self._model = model + def __init__(self, parent, model: "PlotsSettingsModel", view=None): + super().__init__(model) self.parent = parent + self._view = view if view else PlotsSettingsView(parent, self) self.add_filters() self.add_list_items() self.load_general_setting_values() @@ -298,15 +300,19 @@ def setup_signals(self): def action_normalization_changed(self, state): self._model.set_normalize_by_bin_width("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_title_changed(self, state): self._model.set_show_title("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_enable_grid_changed(self, state): self._model.set_enable_grid("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_minor_ticks_changed(self, state): self._model.set_show_minor_ticks("On" if state == Qt.Checked else "Off") + self.notify_changes() self._view.show_minor_gridlines.setEnabled(state == Qt.Checked) if not self._view.show_minor_gridlines.isEnabled(): @@ -314,84 +320,111 @@ def action_show_minor_ticks_changed(self, state): def action_show_minor_gridlines_changed(self, state): self._model.set_show_minor_gridlines("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_font_combo_changed(self, font_name): self._model.set_plot_font(font_name) + self.notify_changes() def action_show_legend_changed(self, state): self._model.set_show_legend("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_default_x_axes_changed(self, axes_scale): self._model.set_x_axes_scale(axes_scale) + self.notify_changes() def action_default_y_axes_changed(self, axes_scale): self._model.set_y_axes_scale(axes_scale) + self.notify_changes() def action_axes_line_width_changed(self, width): self._model.set_axes_line_width(str(width)) + self.notify_changes() def action_show_ticks_left_changed(self, state): self._model.set_show_ticks_left("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_ticks_bottom_changed(self, state): self._model.set_show_ticks_bottom("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_ticks_right_changed(self, state): self._model.set_show_ticks_right("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_ticks_top_changed(self, state): self._model.set_show_ticks_top("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_labels_left_changed(self, state): self._model.set_show_labels_left("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_labels_bottom_changed(self, state): self._model.set_show_labels_bottom("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_labels_right_changed(self, state): self._model.set_show_labels_right("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_show_labels_top_changed(self, state): self._model.set_show_labels_top("On" if state == Qt.Checked else "Off") + self.notify_changes() def action_major_ticks_length_changed(self, value): self._model.set_major_ticks_length(str(value)) + self.notify_changes() def action_major_ticks_width_changed(self, value): self._model.set_major_ticks_width(str(value)) + self.notify_changes() def action_major_ticks_direction_changed(self, direction): self._model.set_major_ticks_direction(direction) + self.notify_changes() def action_minor_ticks_length_changed(self, value): self._model.set_minor_ticks_length(str(value)) + self.notify_changes() def action_minor_ticks_width_changed(self, value): self._model.set_minor_ticks_width(str(value)) + self.notify_changes() def action_minor_ticks_direction_changed(self, direction): self._model.set_minor_ticks_direction(direction) + self.notify_changes() def action_line_style_changed(self, style): self._model.set_line_style(style) + self.notify_changes() def action_draw_style_changed(self, style): self._model.set_draw_style(style) + self.notify_changes() def action_line_width_changed(self, value): self._model.set_line_width(str(value)) + self.notify_changes() def action_x_min_changed(self, value): self._model.set_x_min(str(value)) + self.notify_changes() def action_x_max_changed(self, value): self._model.set_x_max(str(value)) + self.notify_changes() def action_y_min_changed(self, value): self._model.set_y_min(str(value)) + self.notify_changes() def action_y_max_changed(self, value): self._model.set_y_max(str(value)) + self.notify_changes() def action_x_min_box_changed(self, state): self._view.x_min.setEnabled(state) @@ -423,30 +456,39 @@ def action_y_max_box_changed(self, state): def action_marker_style_changed(self, style): self._model.set_marker_style(style) + self.notify_changes() def action_marker_size_changed(self, value): self._model.set_marker_size(str(value)) + self.notify_changes() def action_error_width_changed(self, value): self._model.set_error_width(str(value)) + self.notify_changes() def action_capsize_changed(self, value): self._model.set_capsize(str(value)) + self.notify_changes() def action_cap_thickness_changed(self, value): self._model.set_cap_thickness(str(value)) + self.notify_changes() def action_error_every_changed(self, value): self._model.set_error_every(str(value)) + self.notify_changes() def action_legend_location_changed(self, location): self._model.set_legend_location(str(location)) + self.notify_changes() def action_legend_size_changed(self, value): self._model.set_legend_font_size(str(value)) + self.notify_changes() def action_colorbar_scale_changed(self, value): self._model.set_colorbar_scale(value) + self.notify_changes() def action_default_colormap_changed(self): colormap = self._view.default_colormap_combo_box.currentText() @@ -454,6 +496,7 @@ def action_default_colormap_changed(self): colormap += "_r" self._model.set_color_map(colormap) + self.notify_changes() def populate_font_combo_box(self): fonts = self._model.get_font_names() diff --git a/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings.py b/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings.py index 16ba989b7be0..88260d93150d 100644 --- a/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings.py +++ b/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings.py @@ -98,6 +98,7 @@ def __init__(self): @start_qapplication class PlotsSettingsTest(unittest.TestCase): MOUSEWHEEL_EVENT_FILTER_PATH = "workbench.widgets.settings.plots.presenter.filter_out_mousewheel_events_from_combo_or_spin_box" + NOTIFY_CHANGES_PATH = "workbench.widgets.settings.plots.presenter.PlotSettings.notify_changes" def setUp(self) -> None: self.mock_model = MockPlotsSettingsModel() @@ -152,104 +153,140 @@ def test_load_current_setting_values(self): self.mock_model.get_legend_font_size.assert_called_once() self.mock_model.get_color_map.assert_called_once() - def test_action_normalization_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_normalization_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_normalization_changed(Qt.Checked) self.mock_model.set_normalize_by_bin_width.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_normalize_by_bin_width.reset_mock() + mock_notify_changes.reset_mock() presenter.action_normalization_changed(Qt.Unchecked) self.mock_model.set_normalize_by_bin_width.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_title_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_title_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_title_changed(Qt.Checked) self.mock_model.set_show_title.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_title.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_title_changed(Qt.Unchecked) self.mock_model.set_show_title.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_default_x_axes_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_default_x_axes_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_default_x_axes_changed("Linear") self.mock_model.set_x_axes_scale.assert_called_once_with("Linear") + mock_notify_changes.assert_called_once() self.mock_model.set_x_axes_scale.reset_mock() + mock_notify_changes.reset_mock() presenter.action_default_x_axes_changed("Log") self.mock_model.set_x_axes_scale.assert_called_once_with("Log") + mock_notify_changes.assert_called_once() - def test_action_default_y_axes_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_default_y_axes_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_default_y_axes_changed("Linear") self.mock_model.set_y_axes_scale.assert_called_once_with("Linear") + mock_notify_changes.assert_called_once() self.mock_model.set_y_axes_scale.reset_mock() + mock_notify_changes.reset_mock() presenter.action_default_y_axes_changed("Log") self.mock_model.set_y_axes_scale.assert_called_once_with("Log") + mock_notify_changes.assert_called_once() - def test_action_axes_line_width_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_axes_line_width_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_axes_line_width_changed(2) self.mock_model.set_axes_line_width.assert_called_once_with("2") + mock_notify_changes.assert_called_once() self.mock_model.set_axes_line_width.reset_mock() + mock_notify_changes.reset_mock() presenter.action_axes_line_width_changed(3.5) self.mock_model.set_axes_line_width.assert_called_once_with("3.5") + mock_notify_changes.assert_called_once() - def test_action_x_min_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_x_min_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_x_min_changed(3.2) self.mock_model.set_x_min.assert_called_once_with("3.2") + mock_notify_changes.assert_called_once() self.mock_model.set_x_min.reset_mock() + mock_notify_changes.reset_mock() presenter.action_x_min_changed(1.5) self.mock_model.set_x_min.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() - def test_action_x_max_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_x_max_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_x_max_changed(3.2) self.mock_model.set_x_max.assert_called_once_with("3.2") + mock_notify_changes.assert_called_once() self.mock_model.set_x_max.reset_mock() + mock_notify_changes.reset_mock() presenter.action_x_max_changed(1.5) self.mock_model.set_x_max.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() - def test_action_y_min_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_y_min_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_y_min_changed(3.2) self.mock_model.set_y_min.assert_called_once_with("3.2") + mock_notify_changes.assert_called_once() self.mock_model.set_y_min.reset_mock() + mock_notify_changes.reset_mock() presenter.action_y_min_changed(1.5) self.mock_model.set_y_min.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() - def test_action_y_max_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_y_max_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_y_max_changed(3.2) self.mock_model.set_y_max.assert_called_once_with("3.2") + mock_notify_changes.assert_called_once() self.mock_model.set_y_max.reset_mock() + mock_notify_changes.reset_mock() presenter.action_y_max_changed(1.5) self.mock_model.set_y_max.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() def test_x_min_box_and_check_box_disabled_if_no_value(self): self.mock_model.get_x_min.return_value = "" @@ -315,309 +352,414 @@ def test_y_max_box_and_check_box_enabled_if_value(self): self.assertTrue(presenter.get_view().y_max.isEnabled()) self.assertTrue(presenter.get_view().y_max_box.isChecked()) - def test_x_min_checkbox_clears_property_when_disabled(self): + @patch(NOTIFY_CHANGES_PATH) + def test_x_min_checkbox_clears_property_when_disabled(self, mock_notify_changes: MagicMock): self.mock_model.get_x_min.return_value = "50" presenter = PlotSettings(None, model=self.mock_model) presenter.action_x_min_box_changed(False) self.mock_model.set_x_min.assert_called_once_with("") + mock_notify_changes.assert_called_once() - def test_x_max_checkbox_clears_property_when_disabled(self): + @patch(NOTIFY_CHANGES_PATH) + def test_x_max_checkbox_clears_property_when_disabled(self, mock_notify_changes: MagicMock): self.mock_model.get_x_max.return_value = "50" presenter = PlotSettings(None, model=self.mock_model) presenter.action_x_max_box_changed(False) self.mock_model.set_x_max.assert_called_once_with("") + mock_notify_changes.assert_called_once() - def test_y_min_checkbox_clears_property_when_disabled(self): + @patch(NOTIFY_CHANGES_PATH) + def test_y_min_checkbox_clears_property_when_disabled(self, mock_notify_changes: MagicMock): self.mock_model.get_y_min.return_value = "50" presenter = PlotSettings(None, model=self.mock_model) presenter.action_y_min_box_changed(False) self.mock_model.set_y_min.assert_called_once_with("") + mock_notify_changes.assert_called_once() - def test_y_max_checkbox_clears_property_when_disabled(self): + @patch(NOTIFY_CHANGES_PATH) + def test_y_max_checkbox_clears_property_when_disabled(self, mock_notify_changes: MagicMock): self.mock_model.get_y_max.return_value = "50" presenter = PlotSettings(None, model=self.mock_model) presenter.action_y_max_box_changed(False) self.mock_model.set_y_max.assert_called_once_with("") + mock_notify_changes.assert_called_once() - def test_action_enable_grid(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_enable_grid(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_enable_grid_changed(Qt.Checked) self.mock_model.set_enable_grid.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_enable_grid.reset_mock() + mock_notify_changes.reset_mock() presenter.action_enable_grid_changed(Qt.Unchecked) self.mock_model.set_enable_grid.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_ticks_left(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_ticks_left(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_ticks_left_changed(Qt.Checked) self.mock_model.set_show_ticks_left.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_ticks_left.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_ticks_left_changed(Qt.Unchecked) self.mock_model.set_show_ticks_left.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_ticks_bottom(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_ticks_bottom(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_ticks_bottom_changed(Qt.Checked) self.mock_model.set_show_ticks_bottom.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_ticks_bottom.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_ticks_bottom_changed(Qt.Unchecked) self.mock_model.set_show_ticks_bottom.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_ticks_right(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_ticks_right(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_ticks_right_changed(Qt.Checked) self.mock_model.set_show_ticks_right.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_ticks_right.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_ticks_right_changed(Qt.Unchecked) self.mock_model.set_show_ticks_right.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_ticks_top(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_ticks_top(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_ticks_top_changed(Qt.Checked) self.mock_model.set_show_ticks_top.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_ticks_top.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_ticks_top_changed(Qt.Unchecked) self.mock_model.set_show_ticks_top.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_labels_left(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_labels_left(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_labels_left_changed(Qt.Checked) self.mock_model.set_show_labels_left.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_labels_left.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_labels_left_changed(Qt.Unchecked) self.mock_model.set_show_labels_left.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_labels_bottom(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_labels_bottom(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_labels_bottom_changed(Qt.Checked) self.mock_model.set_show_labels_bottom.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_labels_bottom.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_labels_bottom_changed(Qt.Unchecked) self.mock_model.set_show_labels_bottom.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_labels_right(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_labels_right(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_labels_right_changed(Qt.Checked) self.mock_model.set_show_labels_right.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_labels_right.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_labels_right_changed(Qt.Unchecked) self.mock_model.set_show_labels_right.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_labels_top(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_labels_top(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_labels_top_changed(Qt.Checked) self.mock_model.set_show_labels_top.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_labels_top.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_labels_top_changed(Qt.Unchecked) self.mock_model.set_show_labels_top.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_line_style_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_line_style_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_line_style_changed("dashed") self.mock_model.set_line_style.assert_called_once_with("dashed") + mock_notify_changes.assert_called_once() self.mock_model.set_line_style.reset_mock() + mock_notify_changes.reset_mock() presenter.action_line_style_changed("dotted") self.mock_model.set_line_style.assert_called_once_with("dotted") + mock_notify_changes.assert_called_once() - def test_action_line_width_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_line_width_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_line_width_changed(2) self.mock_model.set_line_width.assert_called_once_with("2") + mock_notify_changes.assert_called_once() self.mock_model.set_line_width.reset_mock() + mock_notify_changes.reset_mock() presenter.action_line_width_changed(3.5) self.mock_model.set_line_width.assert_called_once_with("3.5") + mock_notify_changes.assert_called_once() - def test_action_marker_style_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_marker_style_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_marker_style_changed("circle") self.mock_model.set_marker_style.assert_called_once_with("circle") + mock_notify_changes.assert_called_once() self.mock_model.set_marker_style.reset_mock() + mock_notify_changes.reset_mock() presenter.action_marker_style_changed("octagon") self.mock_model.set_marker_style.assert_called_once_with("octagon") + mock_notify_changes.assert_called_once() - def test_action_marker_size_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_marker_size_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_marker_size_changed("8.0") self.mock_model.set_marker_size.assert_called_once_with("8.0") + mock_notify_changes.assert_called_once() self.mock_model.set_marker_size.reset_mock() + mock_notify_changes.reset_mock() presenter.action_marker_size_changed("5.0") self.mock_model.set_marker_size.assert_called_once_with("5.0") + mock_notify_changes.assert_called_once() - def test_action_error_width_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_error_width_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_error_width_changed(2) self.mock_model.set_error_width.assert_called_once_with("2") + mock_notify_changes.assert_called_once() self.mock_model.set_error_width.reset_mock() + mock_notify_changes.reset_mock() presenter.action_error_width_changed(1.5) self.mock_model.set_error_width.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() - def test_action_capsize_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_capsize_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_capsize_changed(2) self.mock_model.set_capsize.assert_called_once_with("2") + mock_notify_changes.assert_called_once() self.mock_model.set_capsize.reset_mock() + mock_notify_changes.reset_mock() presenter.action_capsize_changed(1.5) self.mock_model.set_capsize.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() - def test_action_cap_thickness_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_cap_thickness_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_cap_thickness_changed(2) self.mock_model.set_cap_thickness.assert_called_once_with("2") + mock_notify_changes.assert_called_once() self.mock_model.set_cap_thickness.reset_mock() + mock_notify_changes.reset_mock() presenter.action_cap_thickness_changed(1.5) self.mock_model.set_cap_thickness.assert_called_once_with("1.5") + mock_notify_changes.assert_called_once() - def test_action_error_every_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_error_every_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_error_every_changed(2) self.mock_model.set_error_every.assert_called_once_with("2") + mock_notify_changes.assert_called_once() self.mock_model.set_error_every.reset_mock() + mock_notify_changes.reset_mock() presenter.action_error_every_changed(5) self.mock_model.set_error_every.assert_called_once_with("5") + mock_notify_changes.assert_called_once() - def test_action_show_minor_ticks_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_minor_ticks_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_minor_ticks_changed(Qt.Checked) self.mock_model.set_show_minor_ticks.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_minor_ticks.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_minor_ticks_changed(Qt.Unchecked) self.mock_model.set_show_minor_ticks.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_minor_gridlines_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_minor_gridlines_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_minor_gridlines_changed(Qt.Checked) self.mock_model.set_show_minor_gridlines.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_minor_gridlines.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_minor_gridlines_changed(Qt.Unchecked) self.mock_model.set_show_minor_gridlines.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_show_legend_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_show_legend_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_show_legend_changed(Qt.Checked) self.mock_model.set_show_legend.assert_called_once_with("On") + mock_notify_changes.assert_called_once() self.mock_model.set_show_legend.reset_mock() + mock_notify_changes.reset_mock() presenter.action_show_legend_changed(Qt.Unchecked) self.mock_model.set_show_legend.assert_called_once_with("Off") + mock_notify_changes.assert_called_once() - def test_action_legend_every_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_legend_every_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_legend_location_changed("best") self.mock_model.set_legend_location.assert_called_once_with("best") + mock_notify_changes.assert_called_once() self.mock_model.set_legend_location.reset_mock() + mock_notify_changes.reset_mock() presenter.action_legend_location_changed("upper left") self.mock_model.set_legend_location.assert_called_once_with("upper left") + mock_notify_changes.assert_called_once() - def test_action_legend_size_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_legend_size_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_legend_size_changed(10) self.mock_model.set_legend_font_size.assert_called_once_with("10") + mock_notify_changes.assert_called_once() self.mock_model.set_legend_font_size.reset_mock() + mock_notify_changes.reset_mock() presenter.action_legend_size_changed(8) self.mock_model.set_legend_font_size.assert_called_once_with("8") + mock_notify_changes.assert_called_once() - def test_action_default_colormap_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_default_colormap_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.get_view().default_colormap_combo_box.setCurrentIndex(4) colormap = presenter.get_view().default_colormap_combo_box.currentText() self.mock_model.set_color_map.reset_mock() + mock_notify_changes.reset_mock() presenter.action_default_colormap_changed() self.mock_model.set_color_map.assert_called_once_with(colormap) + mock_notify_changes.assert_called_once() presenter.get_view().default_colormap_combo_box.setCurrentIndex(5) colormap = presenter.get_view().default_colormap_combo_box.currentText() presenter.get_view().reverse_colormap_check_box.setChecked(True) self.mock_model.set_color_map.reset_mock() + mock_notify_changes.reset_mock() presenter.action_default_colormap_changed() self.mock_model.set_color_map.assert_called_once_with(colormap + "_r") + mock_notify_changes.assert_called_once() - def test_action_font_combo_changed(self): + @patch(NOTIFY_CHANGES_PATH) + def test_action_font_combo_changed(self, mock_notify_changes: MagicMock): presenter = PlotSettings(None, model=self.mock_model) presenter.action_font_combo_changed("Helvetica") self.mock_model.set_plot_font.assert_called_once_with("Helvetica") + mock_notify_changes.assert_called_once() self.mock_model.set_plot_font.reset_mock() + mock_notify_changes.reset_mock() presenter.action_font_combo_changed("Something that is not a font") self.mock_model.set_plot_font.assert_called_once_with("Something that is not a font") + mock_notify_changes.assert_called_once() if __name__ == "__main__": diff --git a/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings_model.py b/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings_model.py index 659e076cc0d3..1d1cca1164a4 100644 --- a/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/plots/test/test_plot_settings_model.py @@ -4,6 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + +import unittest from unittest.mock import MagicMock, call, patch from workbench.widgets.settings.plots.model import PlotsSettingsModel, PlotProperties @@ -520,3 +521,7 @@ def test_set_color_map(self, add_change_mock: MagicMock): self._assert_setter_with_different_values( add_change_mock, self.model.set_color_map, ["jet_r", "greyscale"], PlotProperties.COLORMAP.value ) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/presenter.py b/qt/applications/workbench/workbench/widgets/settings/presenter.py index 73616cb19aef..7decfde9819c 100644 --- a/qt/applications/workbench/workbench/widgets/settings/presenter.py +++ b/qt/applications/workbench/workbench/widgets/settings/presenter.py @@ -58,6 +58,11 @@ def __init__( self.view.container.addWidget(self.plot_settings.get_view()) self.view.container.addWidget(self.fitting_settings.get_view()) + self.plot_settings.subscribe_parent_presenter(self) + self.categories_settings.subscribe_parent_presenter(self) + self.general_settings.subscribe_parent_presenter(self) + self.fitting_settings.subscribe_parent_presenter(self) + self.view.okay_button.clicked.connect(self.action_okay_button_pushed) self.view.apply_button.clicked.connect(self.action_apply_button_pushed) @@ -68,6 +73,8 @@ def __init__( self.model.register_property_which_needs_a_restart(str(GeneralUserConfigProperties.FONT.value)) self.model.register_property_which_needs_a_restart(str(GeneralUserConfigProperties.PROMPT_ON_DELETING_WORKSPACE.value)) + self.update_apply_button() + def show(self, modal=True): if modal: self.view.setWindowModality(Qt.WindowModal) @@ -90,6 +97,14 @@ def action_apply_button_pushed(self): self.model.apply_all_settings() self.parent.config_updated() self.view.notify_changes_need_restart(changes_that_need_restart) + self.update_apply_button() + + def changes_updated(self, unsaved_changes: bool): + self.view.apply_button.setEnabled(unsaved_changes) + + def update_apply_button(self): + unsaved_changes = self.model.unsaved_changes() != {} + self.view.apply_button.setEnabled(unsaved_changes) def action_section_changed(self, new_section_pos): """ diff --git a/qt/applications/workbench/workbench/widgets/settings/test/test_settings_model.py b/qt/applications/workbench/workbench/widgets/settings/test/test_settings_model.py index be45aab5ca40..e256879b191b 100644 --- a/qt/applications/workbench/workbench/widgets/settings/test/test_settings_model.py +++ b/qt/applications/workbench/workbench/widgets/settings/test/test_settings_model.py @@ -4,7 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + -from unittest import TestCase +import unittest from unittest.mock import patch, MagicMock, call, mock_open from workbench.widgets.settings.model import SettingsModel @@ -46,7 +46,7 @@ def __init__(self): self.properties_to_be_changed = MagicMock(return_value=[]) -class SettingsModelTest(TestCase): +class SettingsModelTest(unittest.TestCase): def setUp(self) -> None: self.mock_settings_category_model_1 = MockConfigModel() self.mock_settings_category_model_2 = MockConfigModel() @@ -112,3 +112,7 @@ def test_load_settings_from_file(self, mock_conf, mock_configService): open_mock.assert_called_once_with("filepath", "r") mock_configService.setString.assert_has_calls([call(prop, str(value)) for prop, value in test_config_settings.items()]) mock_conf.set.assert_has_calls([call(prop, value) for prop, value in test_CONF_settings.items()]) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/test/test_settings_presenter.py b/qt/applications/workbench/workbench/widgets/settings/test/test_settings_presenter.py index 3205d6e735a8..4578ac6336d1 100644 --- a/qt/applications/workbench/workbench/widgets/settings/test/test_settings_presenter.py +++ b/qt/applications/workbench/workbench/widgets/settings/test/test_settings_presenter.py @@ -4,7 +4,7 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + -from unittest import TestCase +import unittest from unittest.mock import call, MagicMock, Mock, patch from mantidqt.utils.testing.mocks.mock_qt import MockQButton, MockQWidget @@ -17,6 +17,7 @@ def __init__(self): self._view = MockQWidget() self.update_properties = MagicMock() self.get_view = MagicMock(return_value=self._view) + self.subscribe_parent_presenter = MagicMock() class FakeSectionsListWidget: @@ -54,7 +55,7 @@ def __init__(self): self.ask_before_close = MagicMock() -class SettingsPresenterTest(TestCase): +class SettingsPresenterTest(unittest.TestCase): def setUp(self) -> None: self.mock_view = MockSettingsView() self.mock_model = MagicMock() @@ -100,10 +101,33 @@ def test_register_change_needs_restart(self): self.presenter.action_apply_button_pushed() self.presenter.view.notify_changes_need_restart.assert_called_once_with(settings_needing_restart) - def test_action_apply_button_pushed(self): + def test_update_apply_button(self): + for changes in ({}, {"a": "change"}): + self.mock_view.apply_button.setEnabled.reset_mock() + self.mock_model.unsaved_changes.return_value = changes + self.presenter.update_apply_button() + self.mock_view.apply_button.setEnabled.assert_called_once_with(changes != {}) + + def test_changes_updated(self): + self.mock_view.apply_button.setEnabled.reset_mock() + self.presenter.changes_updated(False) + self.mock_view.apply_button.setEnabled.assert_called_once_with(False) + self.mock_view.apply_button.setEnabled.reset_mock() + self.presenter.changes_updated(True) + self.mock_view.apply_button.setEnabled.assert_called_once_with(True) + + def test_presenter_signals_setup(self): + self.mock_view.general_settings.subscribe_parent_presenter.assert_called_once_with(self.presenter) + self.mock_view.plot_settings.subscribe_parent_presenter.assert_called_once_with(self.presenter) + self.mock_view.fitting_settings.subscribe_parent_presenter.assert_called_once_with(self.presenter) + self.mock_view.categories_settings.subscribe_parent_presenter.assert_called_once_with(self.presenter) + + @patch("workbench.widgets.settings.presenter.SettingsPresenter.update_apply_button") + def test_action_apply_button_pushed(self, update_apply_mock: MagicMock): self.presenter.action_apply_button_pushed() self.mock_model.apply_all_settings.assert_called_once() self.mock_parent.config_updated.assert_called_once() + update_apply_mock.assert_called_once() @patch("workbench.widgets.settings.presenter.SettingsPresenter.view_closing") def test_action_okay_button_pushed(self, mock_view_closing: MagicMock): @@ -140,3 +164,7 @@ def test_view_closing_unsaved_changes_choose_no(self): closed = self.presenter.view_closing() self.assertFalse(closed) + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/applications/workbench/workbench/widgets/settings/test/test_settings_view.py b/qt/applications/workbench/workbench/widgets/settings/test/test_settings_view.py index c88750bb9048..ed1a3d08d5c6 100644 --- a/qt/applications/workbench/workbench/widgets/settings/test/test_settings_view.py +++ b/qt/applications/workbench/workbench/widgets/settings/test/test_settings_view.py @@ -56,3 +56,7 @@ def test_deletes_on_close(self): QApplication.sendPostedEvents() self.assert_no_toplevel_widgets() + + +if __name__ == "__main__": + unittest.main() diff --git a/qt/python/mantidqt/mantidqt/utils/testing/mocks/mock_qt.py b/qt/python/mantidqt/mantidqt/utils/testing/mocks/mock_qt.py index f76df7340dc9..bc3da7aa3e30 100644 --- a/qt/python/mantidqt/mantidqt/utils/testing/mocks/mock_qt.py +++ b/qt/python/mantidqt/mantidqt/utils/testing/mocks/mock_qt.py @@ -5,7 +5,7 @@ # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + # This file is part of the mantid workbench. -from unittest.mock import Mock +from unittest.mock import Mock, MagicMock from mantidqt.utils.testing.strict_mock import StrictMock try: @@ -119,6 +119,7 @@ class MockQButton(object): def __init__(self): self.mock_clicked_signal = MockQtSignal() self.clicked = Mock(return_value=self.mock_clicked_signal) + self.setEnabled = MagicMock() class MockQWidget(object):