From 91100ca96f6f4cbceaf54e7361acad948b6bb191 Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 18 Aug 2022 15:33:03 -0400 Subject: [PATCH 1/9] Create base preferences panel --- src/sas/qtgui/Utilities/PreferencesPanel.py | 105 ++++++++++++++++ src/sas/qtgui/Utilities/UI/PreferencesUI.ui | 132 ++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/sas/qtgui/Utilities/PreferencesPanel.py create mode 100644 src/sas/qtgui/Utilities/UI/PreferencesUI.ui diff --git a/src/sas/qtgui/Utilities/PreferencesPanel.py b/src/sas/qtgui/Utilities/PreferencesPanel.py new file mode 100644 index 0000000000..7e336011f3 --- /dev/null +++ b/src/sas/qtgui/Utilities/PreferencesPanel.py @@ -0,0 +1,105 @@ +import logging + +from PyQt5.QtWidgets import QComboBox, QDialog, QPushButton +from typing import Optional, Any, List + +from sas import get_custom_config +from sas.qtgui.Utilities.UI.PreferencesUI import Ui_preferencesUI + +logger = logging.getLogger(__name__) + + +def set_config_value(attr: str, value: Any): + """Helper method to set any config value, regardless if it exists or not + :param attr: The configuration attribute that will be set + :param value: The value the attribute will be set to. This could be a str, int, bool, a class instance, or any other + """ + custom_config = get_custom_config() + setattr(custom_config, attr, value) + + +def get_config_value(attr: str, default: Optional[Any] = None) -> Any: + """Helper method to get any config value, regardless if it exists or not + :param attr: The configuration attribute that will be returned + :param default: The assumed value, if the attribute cannot be found + """ + custom_config = get_custom_config() + return getattr(custom_config, attr, default) if hasattr(custom_config, attr) else default + + +def cb_replace_all_items_with_new(cb: QComboBox, new_items: List[str], default_item: Optional[str] = None): + """Helper method that removes existing ComboBox values, replaces them and sets a default item, if defined + :param cb: A QComboBox object + :param new_items: A list of strings that will be used to populate the QComboBox + :param default_item: The value to set the QComboBox to, if set + """ + cb.clear() + cb.addItems(new_items) + if default_item and default_item in new_items: + cb.setCurrentIndex(cb.findText(default_item)) + + +class PreferencesPanel(QDialog, Ui_preferencesUI): + """A preferences panel to house all SasView related settings. The left side of the window is a listWidget with a + options menus available. The right side of the window is a stackedWidget object that houses the options + associated with each listWidget item. + **Important Note** When adding new preference widgets, the index for the listWidget and stackedWidget *must* match + Release notes: + SasView v5.0.5: Added defaults for loaded data units and plotted units + """ + + def __init__(self, parent=None): + super(PreferencesPanel, self).__init__(parent) + self.setupUi(self) + self.parent = parent + self.setWindowTitle("Preferences") + self.warning = None + # A list of callables used to restore the default values for each item in StackedWidget + self.restoreDefaultMethods = [] + # Set defaults values for the list and stacked widgets + self.stackedWidget.setCurrentIndex(0) + self.listWidget.setCurrentRow(0) + # Add window actions + self.listWidget.currentItemChanged.connect(self.prefMenuChanged) + self.buttonBox.clicked.connect(self.onClick) + + ###################################################### + # Setup each widget separately below here + ###################################################### + + def prefMenuChanged(self): + """When the preferences menu selection changes, change to the appropriate preferences widget """ + row = self.listWidget.currentRow() + self.stackedWidget.setCurrentIndex(row) + + def onClick(self, btn: QPushButton): + """Handle button click events in one area""" + # Reset to the default preferences + if btn.text() == 'Restore Defaults': + self.restoreDefaultPreferences() + elif btn.text() == 'OK': + self.close() + elif btn.text() == 'Help': + self.help() + + def restoreDefaultPreferences(self): + """Reset all preferences to their default preferences""" + for method in self.restoreDefaultMethods: + if callable(method): + method() + else: + logger.warning(f'While restoring defaults, {str(method)} of type {type(method)}' + + ' was given. A callable object was expected.') + + def close(self): + """Save the configuration values when the preferences window is closed""" + if hasattr(self.parent, 'guiManager'): + self.parent.guiManager.writeCustomConfig(get_custom_config()) + super(PreferencesPanel, self).close() + + def help(self): + """Open the help window associated with the preferences window""" + # TODO: Write the help file and then link to it here + pass + +# TODO: Create API to easily add preferences to the panel \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/UI/PreferencesUI.ui b/src/sas/qtgui/Utilities/UI/PreferencesUI.ui new file mode 100644 index 0000000000..40f0531079 --- /dev/null +++ b/src/sas/qtgui/Utilities/UI/PreferencesUI.ui @@ -0,0 +1,132 @@ + + + preferencesUI + + + + 0 + 0 + 731 + 463 + + + + + 0 + 0 + + + + Preferences + + + + :/res/ball.ico:/res/ball.ico + + + + + + QLayout::SetNoConstraint + + + + + true + + + + 0 + 0 + + + + + 256 + 16777215 + + + + 0 + + + + + + + + + true + + + Qt::LeftToRight + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 2 + + + 1 + + + 0 + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Help|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + + buttonBox + accepted() + preferencesUI + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + preferencesUI + reject() + + + 316 + 260 + + + 286 + 274 + + + + + \ No newline at end of file From fbc6d7cc3ec7e0607dbea9a7ae95db0cd6f5c469 Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 18 Aug 2022 15:33:31 -0400 Subject: [PATCH 2/9] Add preferences panel into the main UI --- src/sas/qtgui/MainWindow/GuiManager.py | 6 ++++++ src/sas/qtgui/MainWindow/UI/MainWindowUI.ui | 11 +++++++++++ src/sas/qtgui/UI/main_resources.qrc | 1 + src/sas/qtgui/UI/res/settings.png | Bin 0 -> 497 bytes 4 files changed, 18 insertions(+) create mode 100644 src/sas/qtgui/UI/res/settings.png diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index df49c2e2f1..1c3a386a18 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -33,6 +33,7 @@ from sas.qtgui.Utilities.ResultPanel import ResultPanel from sas.qtgui.Utilities.Reports.ReportDialog import ReportDialog +from sas.qtgui.Utilities.PreferencesPanel import PreferencesPanel from sas.qtgui.MainWindow.Acknowledgements import Acknowledgements from sas.qtgui.MainWindow.AboutBox import AboutBox from sas.qtgui.MainWindow.WelcomePanel import WelcomePanel @@ -162,6 +163,7 @@ def addWidgets(self): self.ackWidget = Acknowledgements() self.aboutWidget = AboutBox() self.categoryManagerWidget = CategoryManager(self._parent, manager=self) + self.preferences = PreferencesPanel(self._parent) self.grid_window = None self.grid_window = BatchOutputPanel(parent=self) @@ -647,6 +649,7 @@ def addTriggers(self): self._workspace.actionOpen_Analysis.triggered.connect(self.actionOpen_Analysis) self._workspace.actionSave.triggered.connect(self.actionSave_Project) self._workspace.actionSave_Analysis.triggered.connect(self.actionSave_Analysis) + self._workspace.actionPreferences.triggered.connect(self.actionOpen_Preferences) self._workspace.actionQuit.triggered.connect(self.actionQuit) # Edit self._workspace.actionUndo.triggered.connect(self.actionUndo) @@ -797,6 +800,9 @@ def actionSave_Analysis(self): else: logger.warning('No analysis was available to be saved.') + def actionOpen_Preferences(self): + self.preferences.show() + def actionQuit(self): """ Close the reactor, exit the application. diff --git a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui index ef68a7b0fa..8c3ede5e69 100755 --- a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui +++ b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui @@ -40,6 +40,8 @@ + + @@ -607,6 +609,15 @@ Model Marketplace + + + + :/res/settings.png:/res/settings.png + + + Preferences... + + diff --git a/src/sas/qtgui/UI/main_resources.qrc b/src/sas/qtgui/UI/main_resources.qrc index e37d4eb44a..36b8468da0 100755 --- a/src/sas/qtgui/UI/main_resources.qrc +++ b/src/sas/qtgui/UI/main_resources.qrc @@ -15,6 +15,7 @@ res/report.png res/reset.png res/save.png + res/settings.png res/left-round.png res/right-round.png diff --git a/src/sas/qtgui/UI/res/settings.png b/src/sas/qtgui/UI/res/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..59746f1d3d9b7f4192c403c9e791e54ade41d968 GIT binary patch literal 497 zcmVO(H5R8xzA~w;+!fz0pSf;lSKZWUy z2%?B>5Yvb#C`b@Y=?Ac|(?-Pv6O^cDu`lFump>az9T@iZ-J98)x4U!jUu*+#Jicxd zIApoNye0N4iv#zVuU?Ql!s3G_3jigc2xNe5;9ChBz^J?->`R=CVoLN;C5vC{cVG*M zxp*-kuXkR4wp;RX;6bE4pbXpr$sQk+6sN2hM>#^B$eS)gP}s1>S&Rf#`7sdHyQU;T z;88hpofcRDY9gGeqGs|J{8BstVORC%!fQafTgfS)E@DDhQS%ceoB~dT6~swsURREu z_9pU}=2gwZE>1+)tE&P>v2A8^(HxBd3$D++3#44ww8l$~yBK*QdIq@gaEIt2QJT)1 z9K~#?caxui8F8#OIfwt4HmeC^Hnbu?awWGurXdEuXmjG+0Q>a0Xwbb?`-Aoa*+P-+wXm6@00000NkvXXu0mjfXH(W* literal 0 HcmV?d00001 From cf092fcc75a52fef64377663be54176f75482d2a Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 18 Aug 2022 15:33:53 -0400 Subject: [PATCH 3/9] Create unit tests for preferences panel and add them to the utilities suite --- src/sas/qtgui/GUITests.py | 2 + .../UnitTesting/PreferencesPanelTest.py | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py diff --git a/src/sas/qtgui/GUITests.py b/src/sas/qtgui/GUITests.py index a62d43502b..16e0122c96 100644 --- a/src/sas/qtgui/GUITests.py +++ b/src/sas/qtgui/GUITests.py @@ -78,6 +78,7 @@ from Utilities.UnitTesting import AddMultEditorTest from Utilities.UnitTesting import ReportDialogTest from Utilities.UnitTesting import FileConverterTest +from Utilities.UnitTesting import PreferencesPanelTest # Unit Testing from UnitTesting import TestUtilsTest @@ -153,6 +154,7 @@ def utilitiesSuite(): unittest.makeSuite(AddMultEditorTest.AddMultEditorTest, 'test'), unittest.makeSuite(ReportDialogTest.ReportDialogTest, 'test'), unittest.makeSuite(FileConverterTest.FileConverterTest, 'test'), + unittest.makeSuite(PreferencesPanelTest.PreferencesPanelTest, 'test'), ) return unittest.TestSuite(suites) diff --git a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py new file mode 100644 index 0000000000..4e23c91b3b --- /dev/null +++ b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py @@ -0,0 +1,75 @@ +import os +import unittest + +from PyQt5.QtWidgets import QWidget + +from sas.qtgui.Plotting.PlotterData import Data1D +from sas.qtgui.Utilities.PreferencesPanel import * + + +class PreferencesPanelTest(unittest.TestCase): + + def setUp(self): + """ + Prepare the unit test window + """ + class DummyClass: + def __init__(self): + self.path = None + + def writeCustomConfig(self, config): + """ + Write custom configuration + """ + path = './custom_config.py' + # Just clobber the file - we already have its content read in + with open(path, 'w') as out_f: + self.path = os.path.abspath(path) + out_f.write("#Application appearance custom configuration\n") + for key, item in config.__dict__.items(): + if key[:2] == "__": + continue + if isinstance(item, str): + item = '"' + item + '"' + out_f.write("%s = %s\n" % (key, str(item))) + + self.dummy_parent = QWidget() + self.dummy_parent.guiManager = DummyClass() + self.pref_panel = PreferencesPanel(self.dummy_parent) + self.data = [Data1D(x=[1.0, 2.0, 3.0], y=[10.0, 11.0, 12.0])] + + def tearDown(self) -> None: + """Restore global defaults, close the panel and reset class variables""" + self.pref_panel.restoreDefaultPreferences() + self.pref_panel.close() + if self.dummy_parent.guiManager.path: + os.remove(self.dummy_parent.guiManager.path) + self.dummy_parent.close() + self.dummy_parent = None + self.pref_panel = None + + def testDefaults(self): + """Test the freshly-opened panel with no changes made""" + self.assertEqual(self.pref_panel.stackedWidget.count(), self.pref_panel.listWidget.count()) + self.assertEqual(-1, self.pref_panel.stackedWidget.currentIndex()) + + def testPreferencesInteractions(self): + """Test the base interactions in window behavior""" + # Check the list widget and stacked widget are tied together + last_row = self.pref_panel.listWidget.count() - 1 + self.pref_panel.listWidget.setCurrentRow(last_row) + self.assertEqual(self.pref_panel.stackedWidget.currentIndex(), self.pref_panel.listWidget.currentRow()) + + def testPreferencesExtensibility(self): + """Test ability to add and remove items from the listWidget and stackedWidget""" + # Create fake QWidget, add to stacked widget, and add item to list widget + new_widget = QWidget() + starting_size = self.pref_panel.stackedWidget.count() + self.pref_panel.stackedWidget.addWidget(new_widget) + self.pref_panel.listWidget.addItem("Fake Widget") + # Ensure stacked widget and list widget have the same number of elements + self.assertEqual(self.pref_panel.stackedWidget.count(), self.pref_panel.listWidget.count()) + self.assertEqual(starting_size + 1, self.pref_panel.stackedWidget.count()) + # Select last item in list widget and check the stacked widget moves too + self.pref_panel.listWidget.setCurrentRow(self.pref_panel.listWidget.count() - 1) + self.assertEqual(self.pref_panel.stackedWidget.currentIndex(), self.pref_panel.listWidget.currentRow()) From 662dfa28e07a12ac03412e0aeb2989dd7553f2d8 Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 19 Aug 2022 12:42:26 -0400 Subject: [PATCH 4/9] Add helper class for adding items to the preferences panel and update unit tests --- src/sas/qtgui/Utilities/PreferencesPanel.py | 80 +++++++++++++++++-- .../UnitTesting/PreferencesPanelTest.py | 9 +-- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/sas/qtgui/Utilities/PreferencesPanel.py b/src/sas/qtgui/Utilities/PreferencesPanel.py index 7e336011f3..e004b7a6f9 100644 --- a/src/sas/qtgui/Utilities/PreferencesPanel.py +++ b/src/sas/qtgui/Utilities/PreferencesPanel.py @@ -1,7 +1,7 @@ import logging -from PyQt5.QtWidgets import QComboBox, QDialog, QPushButton -from typing import Optional, Any, List +from PyQt5.QtWidgets import QComboBox, QDialog, QPushButton, QWidget, QLabel, QHBoxLayout, QVBoxLayout, QLineEdit, QCheckBox +from typing import Optional, Any, List, Union, Callable from sas import get_custom_config from sas.qtgui.Utilities.UI.PreferencesUI import Ui_preferencesUI @@ -63,10 +63,6 @@ def __init__(self, parent=None): self.listWidget.currentItemChanged.connect(self.prefMenuChanged) self.buttonBox.clicked.connect(self.onClick) - ###################################################### - # Setup each widget separately below here - ###################################################### - def prefMenuChanged(self): """When the preferences menu selection changes, change to the appropriate preferences widget """ row = self.listWidget.currentRow() @@ -97,9 +93,79 @@ def close(self): self.parent.guiManager.writeCustomConfig(get_custom_config()) super(PreferencesPanel, self).close() + def addWidget(self, widget: QWidget): + self.stackedWidget.addWidget(widget) + self.listWidget.addItem(widget.name) + if widget.resetDefaults is not None and callable(widget.resetDefaults): + self.restoreDefaultMethods.append(widget.resetDefaults) + def help(self): """Open the help window associated with the preferences window""" # TODO: Write the help file and then link to it here pass -# TODO: Create API to easily add preferences to the panel \ No newline at end of file + +class PreferencesWidget(QWidget): + """A helper class that bundles all values needed to add a new widget to the preferences panel + """ + # Name that will be added to the PreferencesPanel listWidget + name = None # type: str + + def __init__(self, name: str, default_method: Optional[Callable] = None): + super(PreferencesWidget, self).__init__() + self.name = name + self.resetDefaults = default_method + self.horizontalLayout = QHBoxLayout() + self.setLayout(self.horizontalLayout) + self.adjustSize() + + def _createLayoutAndTitle(self, title: str): + """A private class method that creates a vertical layout to hold the title and interactive item. + :param title: The title of the interactive item to be added to the preferences panel. + :return: A QVBoxLayout instance with a title box already added + """ + layout = QVBoxLayout(self.horizontalLayout) + label = QLabel(title + ": ", layout) + layout.addWidget(label) + return layout + + def addComboBox(self, title: str, params: List[Union[str, int, float]], callback: Callable): + """Add a title and combo box within the widget. + :param title: The title of the combo box to be added to the preferences panel. + :param params: A list of options to be added to the combo box. + :param callback: A callback method called when the combobox value is changed. + """ + layout = self._createLayoutAndTitle(title) + box = QComboBox(layout) + for value in params: + box.addItem(str(value)) + box.currentIndexChanged.connect(callback) + layout.addWidget(box) + self.horizontalLayout.addWidget(layout) + + def addTextInput(self, title: str, callback: Callable, default_text: Optional[str] = ""): + """Add a title and text box within the widget. + :param title: The title of the text box to be added to the preferences panel. + :param callback: A callback method called when the combobox value is changed. + :param default_text: An optional value to be put within the text box as a default. Defaults to an empty string. + """ + layout = self._createLayoutAndTitle(title) + text_box = QLineEdit(layout) + if default_text: + text_box.setText(default_text) + text_box.textChanged.connect(callback) + layout.addWidget(text_box) + self.horizontalLayout.addWidget(layout) + + def addCheckBox(self, title: str, callback: Callable, checked: Optional[bool] = False): + """Add a title and check box within the widget. + :param title: The title of the check box to be added to the preferences panel. + :param callback: A callback method called when the combobox value is changed. + :param checked: An optional boolean value to specify if the check box is checked. Defaults to unchecked. + """ + layout = self._createLayoutAndTitle(title) + check_box = QCheckBox(layout) + check_box.setChecked(checked) + check_box.toggled.connect(callback) + layout.addWidget(check_box) + self.horizontalLayout.addWidget(layout) diff --git a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py index 4e23c91b3b..9721545624 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py @@ -1,8 +1,6 @@ import os import unittest -from PyQt5.QtWidgets import QWidget - from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Utilities.PreferencesPanel import * @@ -62,11 +60,10 @@ def testPreferencesInteractions(self): def testPreferencesExtensibility(self): """Test ability to add and remove items from the listWidget and stackedWidget""" - # Create fake QWidget, add to stacked widget, and add item to list widget - new_widget = QWidget() + # Create fake PreferencesWidget, add to stacked widget, and add item to list widget + new_widget = PreferencesWidget("Fake Widget") starting_size = self.pref_panel.stackedWidget.count() - self.pref_panel.stackedWidget.addWidget(new_widget) - self.pref_panel.listWidget.addItem("Fake Widget") + self.pref_panel.addWidget(new_widget) # Ensure stacked widget and list widget have the same number of elements self.assertEqual(self.pref_panel.stackedWidget.count(), self.pref_panel.listWidget.count()) self.assertEqual(starting_size + 1, self.pref_panel.stackedWidget.count()) From 8793244eca6fc72d9da357b627c4e93ebc40af08 Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 19 Aug 2022 12:54:01 -0400 Subject: [PATCH 5/9] Add default option to combo box helper method and use existing method to populate box --- src/sas/qtgui/Utilities/PreferencesPanel.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Utilities/PreferencesPanel.py b/src/sas/qtgui/Utilities/PreferencesPanel.py index e004b7a6f9..971a8159b3 100644 --- a/src/sas/qtgui/Utilities/PreferencesPanel.py +++ b/src/sas/qtgui/Utilities/PreferencesPanel.py @@ -129,7 +129,7 @@ def _createLayoutAndTitle(self, title: str): layout.addWidget(label) return layout - def addComboBox(self, title: str, params: List[Union[str, int, float]], callback: Callable): + def addComboBox(self, title: str, params: List[Union[str, int, float]], callback: Callable, default: Optional[str]): """Add a title and combo box within the widget. :param title: The title of the combo box to be added to the preferences panel. :param params: A list of options to be added to the combo box. @@ -137,8 +137,7 @@ def addComboBox(self, title: str, params: List[Union[str, int, float]], callback """ layout = self._createLayoutAndTitle(title) box = QComboBox(layout) - for value in params: - box.addItem(str(value)) + cb_replace_all_items_with_new(box, params, default) box.currentIndexChanged.connect(callback) layout.addWidget(box) self.horizontalLayout.addWidget(layout) From 99f9f00dcffbc70fb1cf244fbd884f21bac85b84 Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 19 Aug 2022 15:13:01 -0400 Subject: [PATCH 6/9] Add help to the preferences panel and add unit test to be sure no exceptions are thrown --- src/sas/qtgui/MainWindow/media/preferences_help.rst | 11 +++++++++++ src/sas/qtgui/Utilities/PreferencesPanel.py | 4 ++-- .../Utilities/UnitTesting/PreferencesPanelTest.py | 9 +++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/sas/qtgui/MainWindow/media/preferences_help.rst diff --git a/src/sas/qtgui/MainWindow/media/preferences_help.rst b/src/sas/qtgui/MainWindow/media/preferences_help.rst new file mode 100644 index 0000000000..73221280d1 --- /dev/null +++ b/src/sas/qtgui/MainWindow/media/preferences_help.rst @@ -0,0 +1,11 @@ +.. preferences_help.rst + +.. J Krzywon wrote initial draft August 2022 + +.. _Preferences: + +Preferences +============ + +SasView has a number of user-settable options available. Each heading will give more information about a +particular group of settings. Any preferences not held within this window are planned to move here in a future release. \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/PreferencesPanel.py b/src/sas/qtgui/Utilities/PreferencesPanel.py index 971a8159b3..651d5d6747 100644 --- a/src/sas/qtgui/Utilities/PreferencesPanel.py +++ b/src/sas/qtgui/Utilities/PreferencesPanel.py @@ -101,8 +101,8 @@ def addWidget(self, widget: QWidget): def help(self): """Open the help window associated with the preferences window""" - # TODO: Write the help file and then link to it here - pass + tree_location = "/user/qtgui/MainWindow/preferences_help.html" + self.parent.showHelp(tree_location) class PreferencesWidget(QWidget): diff --git a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py index 9721545624..19f3c77f0b 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py @@ -31,6 +31,11 @@ def writeCustomConfig(self, config): item = '"' + item + '"' out_f.write("%s = %s\n" % (key, str(item))) + @staticmethod + def showHelp(location): + """Simulate a help window""" + return location == "/user/qtgui/MainWindow/preferences_help.html" + self.dummy_parent = QWidget() self.dummy_parent.guiManager = DummyClass() self.pref_panel = PreferencesPanel(self.dummy_parent) @@ -70,3 +75,7 @@ def testPreferencesExtensibility(self): # Select last item in list widget and check the stacked widget moves too self.pref_panel.listWidget.setCurrentRow(self.pref_panel.listWidget.count() - 1) self.assertEqual(self.pref_panel.stackedWidget.currentIndex(), self.pref_panel.listWidget.currentRow()) + + def testHelp(self): + help_button = self.pref_panel.buttonBox.buttons()[0] + self.pref_panel.onClick(help_button) From 820291440fd33aaef961ec36f8e9033c7c540e4e Mon Sep 17 00:00:00 2001 From: krzywon Date: Fri, 14 Oct 2022 17:45:47 -0400 Subject: [PATCH 7/9] Update preferences panel to use the new configuration modules --- src/sas/qtgui/Utilities/PreferencesPanel.py | 11 ++++------- .../UnitTesting/PreferencesPanelTest.py | 16 ---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/sas/qtgui/Utilities/PreferencesPanel.py b/src/sas/qtgui/Utilities/PreferencesPanel.py index 651d5d6747..a197f5df4b 100644 --- a/src/sas/qtgui/Utilities/PreferencesPanel.py +++ b/src/sas/qtgui/Utilities/PreferencesPanel.py @@ -3,7 +3,7 @@ from PyQt5.QtWidgets import QComboBox, QDialog, QPushButton, QWidget, QLabel, QHBoxLayout, QVBoxLayout, QLineEdit, QCheckBox from typing import Optional, Any, List, Union, Callable -from sas import get_custom_config +from sas.system.config.config import config from sas.qtgui.Utilities.UI.PreferencesUI import Ui_preferencesUI logger = logging.getLogger(__name__) @@ -14,8 +14,7 @@ def set_config_value(attr: str, value: Any): :param attr: The configuration attribute that will be set :param value: The value the attribute will be set to. This could be a str, int, bool, a class instance, or any other """ - custom_config = get_custom_config() - setattr(custom_config, attr, value) + setattr(config, attr, value) def get_config_value(attr: str, default: Optional[Any] = None) -> Any: @@ -23,8 +22,7 @@ def get_config_value(attr: str, default: Optional[Any] = None) -> Any: :param attr: The configuration attribute that will be returned :param default: The assumed value, if the attribute cannot be found """ - custom_config = get_custom_config() - return getattr(custom_config, attr, default) if hasattr(custom_config, attr) else default + return getattr(config, attr, default) if hasattr(config, attr) else default def cb_replace_all_items_with_new(cb: QComboBox, new_items: List[str], default_item: Optional[str] = None): @@ -89,8 +87,7 @@ def restoreDefaultPreferences(self): def close(self): """Save the configuration values when the preferences window is closed""" - if hasattr(self.parent, 'guiManager'): - self.parent.guiManager.writeCustomConfig(get_custom_config()) + config.save() super(PreferencesPanel, self).close() def addWidget(self, widget: QWidget): diff --git a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py index 19f3c77f0b..78f970c9bb 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py @@ -15,22 +15,6 @@ class DummyClass: def __init__(self): self.path = None - def writeCustomConfig(self, config): - """ - Write custom configuration - """ - path = './custom_config.py' - # Just clobber the file - we already have its content read in - with open(path, 'w') as out_f: - self.path = os.path.abspath(path) - out_f.write("#Application appearance custom configuration\n") - for key, item in config.__dict__.items(): - if key[:2] == "__": - continue - if isinstance(item, str): - item = '"' + item + '"' - out_f.write("%s = %s\n" % (key, str(item))) - @staticmethod def showHelp(location): """Simulate a help window""" From cf58e5cd9a5099bcf2cb723c3e367c82d7c073c6 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 25 Oct 2022 20:26:56 -0400 Subject: [PATCH 8/9] Convert PreferencesPanelTest.py from unittest to pytest --- .../UnitTesting/PreferencesPanelTest.py | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py index 78f970c9bb..df972f33ce 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py @@ -1,16 +1,14 @@ import os -import unittest +import pytest +from PyQt5.QtWidgets import QWidget from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Utilities.PreferencesPanel import * -class PreferencesPanelTest(unittest.TestCase): - - def setUp(self): - """ - Prepare the unit test window - """ +class PreferencesPanelTest: + @pytest.fixture(autouse=True) + def widget(self, qapp): class DummyClass: def __init__(self): self.path = None @@ -20,46 +18,44 @@ def showHelp(location): """Simulate a help window""" return location == "/user/qtgui/MainWindow/preferences_help.html" - self.dummy_parent = QWidget() - self.dummy_parent.guiManager = DummyClass() - self.pref_panel = PreferencesPanel(self.dummy_parent) - self.data = [Data1D(x=[1.0, 2.0, 3.0], y=[10.0, 11.0, 12.0])] + w = QWidget() + w.guiManager = DummyClass() + panel = PreferencesPanel(w) + yield panel + panel.close() + panel.destroy() - def tearDown(self) -> None: - """Restore global defaults, close the panel and reset class variables""" - self.pref_panel.restoreDefaultPreferences() - self.pref_panel.close() - if self.dummy_parent.guiManager.path: - os.remove(self.dummy_parent.guiManager.path) - self.dummy_parent.close() - self.dummy_parent = None - self.pref_panel = None + @pytest.fixture(autouse=True) + def data(self): + data = [Data1D(x=[1.0, 2.0, 3.0], y=[10.0, 11.0, 12.0])] + yield data - def testDefaults(self): + def testDefaults(self, widget): """Test the freshly-opened panel with no changes made""" - self.assertEqual(self.pref_panel.stackedWidget.count(), self.pref_panel.listWidget.count()) - self.assertEqual(-1, self.pref_panel.stackedWidget.currentIndex()) + assert widget.stackedWidget.count() == widget.listWidget.count() + assert -1 == widget.stackedWidget.currentIndex() - def testPreferencesInteractions(self): + def testPreferencesInteractions(self, widget): """Test the base interactions in window behavior""" # Check the list widget and stacked widget are tied together - last_row = self.pref_panel.listWidget.count() - 1 - self.pref_panel.listWidget.setCurrentRow(last_row) - self.assertEqual(self.pref_panel.stackedWidget.currentIndex(), self.pref_panel.listWidget.currentRow()) + last_row = widget.listWidget.count() - 1 + widget.listWidget.setCurrentRow(last_row) + assert widget.stackedWidget.currentIndex() == widget.listWidget.currentRow() - def testPreferencesExtensibility(self): + def testPreferencesExtensibility(self, widget): """Test ability to add and remove items from the listWidget and stackedWidget""" # Create fake PreferencesWidget, add to stacked widget, and add item to list widget new_widget = PreferencesWidget("Fake Widget") - starting_size = self.pref_panel.stackedWidget.count() - self.pref_panel.addWidget(new_widget) + starting_size = widget.stackedWidget.count() + widget.addWidget(new_widget) # Ensure stacked widget and list widget have the same number of elements - self.assertEqual(self.pref_panel.stackedWidget.count(), self.pref_panel.listWidget.count()) - self.assertEqual(starting_size + 1, self.pref_panel.stackedWidget.count()) + assert widget.stackedWidget.count() == widget.listWidget.count() + assert starting_size + 1 == widget.stackedWidget.count() # Select last item in list widget and check the stacked widget moves too - self.pref_panel.listWidget.setCurrentRow(self.pref_panel.listWidget.count() - 1) - self.assertEqual(self.pref_panel.stackedWidget.currentIndex(), self.pref_panel.listWidget.currentRow()) + widget.listWidget.setCurrentRow(widget.listWidget.count() - 1) + assert widget.stackedWidget.currentIndex() == widget.listWidget.currentRow() - def testHelp(self): - help_button = self.pref_panel.buttonBox.buttons()[0] - self.pref_panel.onClick(help_button) + def testHelp(self, widget, mocker): + mocker.patch.object(widget, 'onClick') + widget.buttonBox.buttons()[0].click() + assert widget.onClick.called_once() From a6dff7128fd27f71f3a6b616262b20ca70b2e6e4 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 25 Oct 2022 20:57:09 -0400 Subject: [PATCH 9/9] Add unit tests for preferences widget and update widget objects to fix issues --- src/sas/qtgui/Utilities/PreferencesPanel.py | 24 +++++++------- .../UnitTesting/PreferencesPanelTest.py | 31 ++++++++++++++++++- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/Utilities/PreferencesPanel.py b/src/sas/qtgui/Utilities/PreferencesPanel.py index a197f5df4b..0971cbbae1 100644 --- a/src/sas/qtgui/Utilities/PreferencesPanel.py +++ b/src/sas/qtgui/Utilities/PreferencesPanel.py @@ -33,8 +33,8 @@ def cb_replace_all_items_with_new(cb: QComboBox, new_items: List[str], default_i """ cb.clear() cb.addItems(new_items) - if default_item and default_item in new_items: - cb.setCurrentIndex(cb.findText(default_item)) + index = cb.findText(default_item) if default_item and default_item in new_items else 0 + cb.setCurrentIndex(index) class PreferencesPanel(QDialog, Ui_preferencesUI): @@ -121,23 +121,25 @@ def _createLayoutAndTitle(self, title: str): :param title: The title of the interactive item to be added to the preferences panel. :return: A QVBoxLayout instance with a title box already added """ - layout = QVBoxLayout(self.horizontalLayout) - label = QLabel(title + ": ", layout) + layout = QVBoxLayout(self) + label = QLabel(title + ": ", self) layout.addWidget(label) return layout - def addComboBox(self, title: str, params: List[Union[str, int, float]], callback: Callable, default: Optional[str]): + def addComboBox(self, title: str, params: List[Union[str, int, float]], callback: Callable, + default: Optional[str] = None): """Add a title and combo box within the widget. :param title: The title of the combo box to be added to the preferences panel. :param params: A list of options to be added to the combo box. :param callback: A callback method called when the combobox value is changed. + :param default: The default option to be selected in the combo box. The first item is selected if None. """ layout = self._createLayoutAndTitle(title) - box = QComboBox(layout) + box = QComboBox(self) cb_replace_all_items_with_new(box, params, default) box.currentIndexChanged.connect(callback) layout.addWidget(box) - self.horizontalLayout.addWidget(layout) + self.horizontalLayout.addLayout(layout) def addTextInput(self, title: str, callback: Callable, default_text: Optional[str] = ""): """Add a title and text box within the widget. @@ -146,12 +148,12 @@ def addTextInput(self, title: str, callback: Callable, default_text: Optional[st :param default_text: An optional value to be put within the text box as a default. Defaults to an empty string. """ layout = self._createLayoutAndTitle(title) - text_box = QLineEdit(layout) + text_box = QLineEdit(self) if default_text: text_box.setText(default_text) text_box.textChanged.connect(callback) layout.addWidget(text_box) - self.horizontalLayout.addWidget(layout) + self.horizontalLayout.addLayout(layout) def addCheckBox(self, title: str, callback: Callable, checked: Optional[bool] = False): """Add a title and check box within the widget. @@ -160,8 +162,8 @@ def addCheckBox(self, title: str, callback: Callable, checked: Optional[bool] = :param checked: An optional boolean value to specify if the check box is checked. Defaults to unchecked. """ layout = self._createLayoutAndTitle(title) - check_box = QCheckBox(layout) + check_box = QCheckBox(self) check_box.setChecked(checked) check_box.toggled.connect(callback) layout.addWidget(check_box) - self.horizontalLayout.addWidget(layout) + self.horizontalLayout.addLayout(layout) diff --git a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py index df972f33ce..fc8d371da7 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/PreferencesPanelTest.py @@ -1,6 +1,6 @@ import os import pytest -from PyQt5.QtWidgets import QWidget +from PyQt5.QtWidgets import QWidget, QLineEdit, QComboBox, QCheckBox from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Utilities.PreferencesPanel import * @@ -59,3 +59,32 @@ def testHelp(self, widget, mocker): mocker.patch.object(widget, 'onClick') widget.buttonBox.buttons()[0].click() assert widget.onClick.called_once() + + def testPreferencesWidget(self, widget, mocker): + mocker.patch.object(widget, 'checked', create=True) + mocker.patch.object(widget, 'combo', create=True) + mocker.patch.object(widget, 'textified', create=True) + mocker.patch.object(widget, 'resetPref', create=True) + + pref = PreferencesWidget("Dummy Widget", widget.resetPref) + pref.addTextInput("blah", widget.textified) + pref.addCheckBox("ho hum", widget.checked) + pref.addComboBox("combo", ["a", "b", "c"], widget.combo, "a") + + widget.addWidget(pref) + + widget.restoreDefaultPreferences() + assert widget.resetPref.called_once() + + for child in pref.layout().children(): + if isinstance(child, QLineEdit): + child.setText("new text") + elif isinstance(child, QComboBox): + child.setCurrentIndex(1) + elif isinstance(child, QCheckBox): + child.setChecked(not child.checkState()) + + assert widget.textified.called_once() + assert widget.combo.called_once() + assert widget.checked.called_once() +