diff --git a/installers/sasview.spec b/installers/sasview.spec index fca9705b5f..01582c444a 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -15,6 +15,7 @@ datas = [ ('../src/sas/example_data', 'example_data'), ('../src/sas/qtgui/Utilities/Reports/report_style.css', 'sas/qtgui/Utilities/Reports'), ('../src/sas/qtgui/Perspectives/Fitting/plugin_models', 'plugin_models'), + ('../src/sas/qtgui/Utilities/WhatsNew/messages', 'sas/qtgui/Utilities/WhatsNew/messages'), ('../src/sas/system/log.ini', 'sas/system/'), ('../../sasmodels/sasmodels','sasmodels'), ('../docs/sphinx-docs/build/html','doc') diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 2ebc711792..008baa0011 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -69,6 +69,7 @@ from sas.qtgui.Utilities.AddMultEditor import AddMultEditor from sas.qtgui.Utilities.ImageViewer import ImageViewer from sas.qtgui.Utilities.FileConverter import FileConverterWidget +from sas.qtgui.Utilities.WhatsNew.WhatsNew import WhatsNew import sas from sas import config @@ -134,6 +135,9 @@ def __init__(self, parent=None): "_downloads", "Tutorial.pdf")) + if self.WhatsNew.has_new_messages(): + self.actionWhatsNew() + def info(self, type, value, tb): logger.error("".join(traceback.format_exception(type, value, tb))) @@ -199,6 +203,7 @@ def addWidgets(self): self.ResolutionCalculator = ResolutionCalculatorPanel(self) self.DataOperation = DataOperationUtilityPanel(self) self.FileConverter = FileConverterWidget(self) + self.WhatsNew = WhatsNew(self) def loadAllPerspectives(self): # Close any existing perspectives to prevent multiple open instances @@ -619,6 +624,9 @@ def actionWelcome(self): self._workspace.workspace.addSubWindow(self.welcomePanel) self.welcomePanel.show() + def actionWhatsNew(self): + self.WhatsNew.show() + def showWelcomeMessage(self): """ Show the Welcome panel, when required """ # Assure the welcome screen is requested @@ -734,7 +742,8 @@ def addTriggers(self): self._workspace.actionAbout.triggered.connect(self.actionAbout) self._workspace.actionWelcomeWidget.triggered.connect(self.actionWelcome) self._workspace.actionCheck_for_update.triggered.connect(self.actionCheck_for_update) - + self._workspace.actionWhat_s_New.triggered.connect(self.actionWhatsNew) + self.communicate.sendDataToGridSignal.connect(self.showBatchOutput) self.communicate.resultPlotUpdateSignal.connect(self.showFitResults) diff --git a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui index 44b176e8aa..07b358fdc6 100755 --- a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui +++ b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui @@ -158,6 +158,7 @@ + @@ -619,6 +620,11 @@ Preferences... + + + What's New + + diff --git a/src/sas/qtgui/MainWindow/UnitTesting/GuiManagerTest.py b/src/sas/qtgui/MainWindow/UnitTesting/GuiManagerTest.py index c3bebe9533..98b33833c5 100644 --- a/src/sas/qtgui/MainWindow/UnitTesting/GuiManagerTest.py +++ b/src/sas/qtgui/MainWindow/UnitTesting/GuiManagerTest.py @@ -21,10 +21,15 @@ from sas.qtgui.UnitTesting.TestUtils import QtSignalSpy from sas.qtgui.Utilities.HidableDialog import HidableDialog +from sas.system import config class GuiManagerTest: '''Test the Main Window functionality''' + def __init__(self): + config.override_with_defaults() # Disable saving of test file + config.LAST_WHATS_NEW_HIDDEN_VERSION = "999.999.999" # Give a very large version number + @pytest.fixture(autouse=True) def manager(self, qapp): '''Create/Destroy the GUI Manager''' diff --git a/src/sas/qtgui/MainWindow/UnitTesting/MainWindowTest.py b/src/sas/qtgui/MainWindow/UnitTesting/MainWindowTest.py index 4571eee6a5..4d78026778 100644 --- a/src/sas/qtgui/MainWindow/UnitTesting/MainWindowTest.py +++ b/src/sas/qtgui/MainWindow/UnitTesting/MainWindowTest.py @@ -13,8 +13,14 @@ from sas.qtgui.Perspectives.Fitting import FittingPerspective from sas.qtgui.Utilities.HidableDialog import HidableDialog, ShowAgainResult +from sas.system import config class MainWindowTest: """Test the Main Window GUI""" + + def __init__(self): + config.override_with_defaults() # Disable saving of test file + config.LAST_WHATS_NEW_HIDDEN_VERSION = "999.999.999" # Give a very large version number + @pytest.fixture(autouse=True) def widget(self, qapp): '''Create/Destroy the GUI''' diff --git a/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py b/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py new file mode 100644 index 0000000000..4ead84f1bb --- /dev/null +++ b/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py @@ -0,0 +1,152 @@ +from collections import defaultdict + +from PySide6 import QtWidgets +from PySide6.QtWidgets import QDialog, QWidget, QTextBrowser, QVBoxLayout, QHBoxLayout, QPushButton, QCheckBox + +from sas.system.version import __version__ as sasview_version +import importlib.resources as resources + +from sas.system import config + + +from sas.qtgui.Utilities.WhatsNew.newer import strictly_newer_than, reduced_version, newest + +def whats_new_messages(): + """ Accumulate all files that are newer than the value in the config""" + + out = defaultdict(list) + message_dir = resources.files("sas.qtgui.Utilities.WhatsNew.messages") + for message_dir in message_dir.iterdir(): + # Get short filename + if message_dir.is_dir(): + + newer = False + + try: + newer = strictly_newer_than(message_dir.name, config.LAST_WHATS_NEW_HIDDEN_VERSION) + + except ValueError: + pass + + if newer: + for file in message_dir.iterdir(): + if file.name.endswith(".html"): + out[message_dir.name].append(file) + + + return out + + +class WhatsNew(QDialog): + """ What's New window: displays messages about what is new in this version of SasView + + It will find all files in messages.[version] if [version] is newer than the last time + the "don't show me again" option was chosen + + To add new messages, just dump a (self-contained) html file into the appropriate folder + + """ + def __init__(self, parent=None): + super().__init__() + + self.setWindowTitle(f"What's New in SasView {sasview_version}") + + self.browser = QTextBrowser() + + # Layout stuff + self.mainLayout = QVBoxLayout() + self.buttonBar = QWidget() + self.buttonLayout = QHBoxLayout() + + + # Buttons + self.buttonBar.setLayout(self.buttonLayout) + + self.closeButton = QPushButton("Close") + self.nextButton = QPushButton("Next") + + self.showAgain = QCheckBox("Show on Startup") + self.showAgain.setChecked(True) + + self.buttonLayout.addWidget(self.showAgain) + self.buttonLayout.addWidget(self.closeButton) + self.buttonLayout.addWidget(self.nextButton) + + # Viewer + self.setLayout(self.mainLayout) + self.mainLayout.addWidget(self.browser) + self.mainLayout.addWidget(self.buttonBar) + + # Callbacks + self.closeButton.clicked.connect(self.close_me) + self.nextButton.clicked.connect(self.next_file) + + # # Gather new files + new_messages = whats_new_messages() + new_message_directories = [key for key in new_messages.keys()] + new_message_directories.sort(key=reduced_version) + + self.all_messages = [] + + for version in new_messages: + self.all_messages += new_messages[version] + + self.max_index = len(self.all_messages) + self.current_index = 0 + + self.show_file() + + self.setModal(True) + + def next_file(self): + self.current_index += 1 + self.current_index %= self.max_index + self.show_file() + + def show_file(self): + if len(self.all_messages) > 0: + filename = self.all_messages[self.current_index] + with open(filename, 'r') as fid: + data = fid.read() + self.browser.setText(data) + else: + self.browser.setText("

You should not see this!!!

") + + def close_me(self): + if not self.showAgain.isChecked(): + # We choose the newest, for backwards compatability, i.e. we never reduce the last version + config.LAST_WHATS_NEW_HIDDEN_VERSION = newest(sasview_version, config.LAST_WHATS_NEW_HIDDEN_VERSION) + + self.close() + + def has_new_messages(self) -> bool: + """ Should the window be shown? """ + return bool(self.all_messages) + + + +def maybe_show_whats_new(): + global whats_new_window + """ Show the What's New dialogue if it is wanted """ + + if whats_new_messages(): + whats_new_window = WhatsNew() + whats_new_window.show() + + +def main(): + """ Demo/testing window""" + + from sas.qtgui.convertUI import main + + main() + + app = QtWidgets.QApplication([]) + + maybe_show_whats_new() + + app.exec_() + + +if __name__ == "__main__": + main() diff --git a/src/sas/qtgui/Utilities/WhatsNew/__init__.py b/src/sas/qtgui/Utilities/WhatsNew/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/Utilities/WhatsNew/messages/6.0.0/1.html b/src/sas/qtgui/Utilities/WhatsNew/messages/6.0.0/1.html new file mode 100644 index 0000000000..83c62844f8 --- /dev/null +++ b/src/sas/qtgui/Utilities/WhatsNew/messages/6.0.0/1.html @@ -0,0 +1,11 @@ + + + + +

+ Some Handy Tips +

+ +Have you tried randomly pressing buttons? + + \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/WhatsNew/messages/6.0.0/2.html b/src/sas/qtgui/Utilities/WhatsNew/messages/6.0.0/2.html new file mode 100644 index 0000000000..b9f937665f --- /dev/null +++ b/src/sas/qtgui/Utilities/WhatsNew/messages/6.0.0/2.html @@ -0,0 +1,10 @@ + + + +

+ Welcome to SasView - What's New +

+ +Lot's of things + + \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/WhatsNew/messages/__init__.py b/src/sas/qtgui/Utilities/WhatsNew/messages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sas/qtgui/Utilities/WhatsNew/newer.py b/src/sas/qtgui/Utilities/WhatsNew/newer.py new file mode 100644 index 0000000000..0adff7cd15 --- /dev/null +++ b/src/sas/qtgui/Utilities/WhatsNew/newer.py @@ -0,0 +1,43 @@ +from typing import Tuple +import re + +def reduced_version(version_string: str) -> Tuple[int, int, int]: + """ Convert a version string into the three numbers we care about for the purposes + of the WhatsNew dialog (i.e. strip a,b suffixes etc, make into three ints""" + + version_string = re.sub(r"[^\.0-9]+.*", "", version_string) + + parts = version_string.split(".") + + if len(parts) > 3: + raise ValueError(f"{version_string} not a valid version string") + + + parts = [int(part) for part in parts] + + return tuple(parts + [0]*(3-len(parts))) + + +def strictly_newer_than(version_a: str, version_b: str) -> bool: + """ Is the version string "version_a" string strictly newer than "version_b" """ + + numeric_a = reduced_version(version_a) + numeric_b = reduced_version(version_b) + + for i in range(3): + if numeric_a[i] > numeric_b[i]: + return True + elif numeric_a[i] < numeric_b[i]: + return False + + return False + +def newest(version_a: str, version_b: str) -> str: + """Return the newest of two versions by the comparison used in the what's new box, + if they are equally new, return the first one. + """ + + if strictly_newer_than(version_b, version_a): + return version_b + + return version_a \ No newline at end of file diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py index d3fd556955..6edf0c2a25 100644 --- a/src/sas/system/config/config.py +++ b/src/sas/system/config/config.py @@ -208,6 +208,9 @@ def __init__(self): # Default fitting optimizer self.FITTING_DEFAULT_OPTIMIZER = 'lm' + # What's New variables + self.LAST_WHATS_NEW_HIDDEN_VERSION = "5.0.0" + # # Lock the class down, this is necessary both for # securing the class, and for setting up reading/writing files