Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 docs/sphinx-docs/source/user/menu_bar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ onto Data Explorer.

A *SasView* session can also be saved and reloaded as an 'Analysis' (an individual model fit or invariant
calculation, etc), or as a 'Project' (everything you have done since starting your *SasView* session).
Finally, a session can be closed so a new project can be created. This will clear all plots, data and
content in all the perspectives, even those which are not currently visible.

Edit
----
Expand Down
9 changes: 9 additions & 0 deletions src/sas/qtgui/MainWindow/DataExplorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2150,3 +2150,12 @@ def setCheckItems(self, status=QtCore.Qt.Unchecked):
if item.isCheckable():
item.setCheckState(status)
model.blockSignals(False)

def reset(self):
"""
Reset the data explorer to an empty state
"""
self.closeAllPlots()
self.model.clear()
self.theory_model.clear()

39 changes: 36 additions & 3 deletions src/sas/qtgui/MainWindow/GuiManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from packaging.version import Version
from PySide6.QtCore import QLocale, Qt
from PySide6.QtGui import QStandardItem
from PySide6.QtWidgets import QDockWidget, QLabel, QProgressBar, QTextBrowser
from PySide6.QtWidgets import QDockWidget, QLabel, QMessageBox, QProgressBar, QTextBrowser
from twisted.internet import reactor

import sas
Expand Down Expand Up @@ -706,6 +706,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.actionClose_Project.triggered.connect(self.actionClose_Project)
self._workspace.actionPreferences.triggered.connect(self.actionOpen_Preferences)
self._workspace.actionQuit.triggered.connect(self.actionQuit)
# Edit
Expand Down Expand Up @@ -809,13 +810,14 @@ def actionOpen_Analysis(self):
self.filesWidget.loadAnalysis()


def actionSave_Project(self):
def actionSave_Project(self) -> bool:
"""
Menu Save Project
return: True if save was successful, False otherwise
"""
filename = self.filesWidget.saveProject()
if not filename:
return
return False

# datasets
all_data = self.filesWidget.getSerializedData()
Expand All @@ -840,6 +842,7 @@ def actionSave_Project(self):

with open(filename, 'w') as outfile:
GuiUtils.saveData(outfile, final_data)
return True

def actionSave_Analysis(self):
"""
Expand Down Expand Up @@ -1319,6 +1322,24 @@ def actionAbout(self):
about = About()
about.exec()

def actionClose_Project(self):
"""
Menu File/Close Project
"""
# Make sure this is what the user really wants
reply = QMessageBox.question(self._parent, 'Close Project',
"Do you want to save the project before closing?\n"
"All unsaved changes will be lost if you don't save.",
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
QMessageBox.Cancel)
if reply == QMessageBox.Save:
Copy link
Contributor

Choose a reason for hiding this comment

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

If the saving project dialog is cancelled, the project reset still happens with no project saved, which may not be ideal.

Maybe if (reply == QMessageBox.Save and self.actionSave_Project()) or reply == QMessageBox.Discard:? This would require the save project to return something Truthy on success.

Copy link
Member Author

@rozyczko rozyczko Nov 18, 2025

Choose a reason for hiding this comment

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

Errr.. nope :) we have

        if reply == QMessageBox.Save:
         ...
        elif reply == QMessageBox.Discard:
         ....
        # else Cancel, do nothing

The else branch is just noop but with comment added

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think my original point was clear. Try the following steps:

  1. Open a project or do some work in SasView
  2. Choose File -> Close Project
  3. Click the 'Save' button
  4. In the resulting file browser dialog, click Cancel
  5. The result is no saved project, and all work cleared from SasView

I don't think users will appreciate all of their work being thrown away without warning.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah! This makes sense. Indeed cancelling the "Save Project" does the wrong thing. thanks!

saved = self.actionSave_Project()
if saved:
self.resetProject()
elif reply == QMessageBox.Discard:
self.resetProject()
# else Cancel, do nothing

def actionCheck_for_update(self):
"""
Menu Help/Check for Update
Expand Down Expand Up @@ -1396,3 +1417,15 @@ def saveCustomConfig(self):
Save the config file based on current session values
"""
config.save()

def resetProject(self):
"""
Reset the project to an empty state
"""
# perspectives
for per in self.loadedPerspectives.values():
if hasattr(per, 'reset'):
per.reset()
# file manager
self.filesWidget.reset()

11 changes: 9 additions & 2 deletions src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<x>0</x>
<y>0</y>
<width>915</width>
<height>20</height>
<height>26</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
Expand All @@ -42,6 +42,8 @@
<addaction name="separator"/>
<addaction name="actionPreferences"/>
<addaction name="separator"/>
<addaction name="actionClose_Project"/>
<addaction name="separator"/>
Comment on lines +45 to +46
Copy link
Contributor

Choose a reason for hiding this comment

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

Logically, this would make more sense to be above the Preferences option.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think I fully agree - closing and quitting actions are usually on the bottom of the list, cf. Window menu.
For File you choose to either Close Project or Quit - these two should belong to the same "category".

<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuEdit">
Expand Down Expand Up @@ -632,13 +634,18 @@
<action name="actionMuMag_Fitter">
<property name="text">
<string>MuMag Fitter (Experimental)</string>
</property>
</property>
</action>
<action name="actionWhat_s_New">
<property name="text">
<string>What's New</string>
</property>
</action>
<action name="actionClose_Project">
<property name="text">
<string>Close Project</string>
</property>
</action>
</widget>
<resources/>
<connections/>
Expand Down
7 changes: 7 additions & 0 deletions src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,10 @@ def getReport(self) -> ReportData | None:
report.add_plot(self.idf_figure)

return report.report_data

def reset(self):
"""
Reset the corfunc perspective to an empty state
"""
self.removeData([self._model_item] if self._model_item else None)

9 changes: 9 additions & 0 deletions src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,15 @@ def getTabByName(self, name):
return tab
return None

def reset(self):
"""
Reset the fitting perspective to an empty state
"""
while self.count() > 0:
self.closeTabByIndex(0)
# Add an empty fit tab
self.addFit(None)

@property
def supports_reports(self) -> bool:
return True
Expand Down
6 changes: 6 additions & 0 deletions src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,3 +1151,9 @@ def allowSwap(self):
Tell the caller that we can't swap data
"""
return False

def reset(self):
"""
Reset the fitting perspective to an empty state
"""
self.removeData([self._model_item] if self._model_item else None)
10 changes: 9 additions & 1 deletion src/sas/qtgui/Perspectives/Inversion/InversionPerspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ def closeTabByIndex(self, index):
# The tab might have already been deleted previously
pass


def closeTabByName(self, tab_name):
"""
Given name of the tab - close it
Expand Down Expand Up @@ -435,3 +434,12 @@ def updateFromParameters(self, params):
inversion_widget = self.currentWidget()
if isinstance(inversion_widget, InversionWidget):
inversion_widget.updateFromParameters(params)

def reset(self):
"""
Reset the Inversion perspective to an empty state
"""
self.tabs.clear()
self.clear()
self.maxIndex = 1
self.addData(None)
Original file line number Diff line number Diff line change
Expand Up @@ -813,3 +813,10 @@ def clearStatistics(self):
self.txtDiameterMean.setText("")
self.txtDiameterMode.setText("")
self.txtDiameterMedian.setText("")

def reset(self):
"""
Reset the size distribution perspective to an empty state
"""
self.removeData([self._model_item] if self._model_item else None)
self.resetWindow()