Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a preferences window #2167

Merged
merged 12 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sas/qtgui/GUITests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions src/sas/qtgui/MainWindow/GuiManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -166,6 +167,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)
Expand Down Expand Up @@ -652,6 +654,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)
Expand Down Expand Up @@ -802,6 +805,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.
Expand Down
11 changes: 11 additions & 0 deletions src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
<addaction name="actionSave"/>
<addaction name="actionSave_Analysis"/>
<addaction name="separator"/>
<addaction name="actionPreferences"/>
<addaction name="separator"/>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuEdit">
Expand Down Expand Up @@ -607,6 +609,15 @@
<string>Model Marketplace</string>
</property>
</action>
<action name="actionPreferences">
<property name="icon">
<iconset>
<normaloff>:/res/settings.png</normaloff>:/res/settings.png</iconset>
</property>
<property name="text">
<string>Preferences...</string>
</property>
</action>
</widget>
<resources/>
<connections/>
Expand Down
11 changes: 11 additions & 0 deletions src/sas/qtgui/MainWindow/media/preferences_help.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions src/sas/qtgui/UI/main_resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<file>res/report.png</file>
<file>res/reset.png</file>
<file>res/save.png</file>
<file>res/settings.png</file>
<file>res/left-round.png</file>
<file>res/right-round.png</file>
</qresource>
Expand Down
Binary file added src/sas/qtgui/UI/res/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
167 changes: 167 additions & 0 deletions src/sas/qtgui/Utilities/PreferencesPanel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import logging

from PyQt5.QtWidgets import QComboBox, QDialog, QPushButton, QWidget, QLabel, QHBoxLayout, QVBoxLayout, QLineEdit, QCheckBox
from typing import Optional, Any, List, Union, Callable

from sas.system.config.config import 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
"""
setattr(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
"""
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):
"""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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this necessary in the __init__ method? Aren't current indices/rows set to 0 anyway on instantiation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary, but a good practice to ensure the two widgets are at the same point.

# Add window actions
self.listWidget.currentItemChanged.connect(self.prefMenuChanged)
self.buttonBox.clicked.connect(self.onClick)

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably better to create separate slots for all the buttons rather than compare with the text property. Those texts can change and updating the handlers is not an obvious follow up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This was a placeholder.


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"""
config.save()
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"""
tree_location = "/user/qtgui/MainWindow/preferences_help.html"
self.parent.showHelp(tree_location)


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, 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.
:param callback: A callback method called when the combobox value is changed.
"""
layout = self._createLayoutAndTitle(title)
box = QComboBox(layout)
cb_replace_all_items_with_new(box, params, default)
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)
132 changes: 132 additions & 0 deletions src/sas/qtgui/Utilities/UI/PreferencesUI.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>preferencesUI</class>
<widget class="QDialog" name="preferencesUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>731</width>
<height>463</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Preferences</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/res/ball.ico</normaloff>:/res/ball.ico</iconset>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="18" column="0" rowspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetNoConstraint</enum>
</property>
<item>
<widget class="QListWidget" name="listWidget">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>256</width>
<height>16777215</height>
</size>
</property>
<property name="currentRow">
<number>0</number>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="enabled">
<bool>true</bool>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="lineWidth">
<number>2</number>
</property>
<property name="midLineWidth">
<number>1</number>
</property>
<property name="currentIndex">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="22" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Help|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>preferencesUI</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>preferencesUI</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
Loading