Skip to content

Commit 4d98408

Browse files
authored
Fixes save crash issue (#169)
1 parent e115204 commit 4d98408

File tree

6 files changed

+133
-50
lines changed

6 files changed

+133
-50
lines changed

installer/linux/build_installer.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ if [ -n "$HELP" ]; then
4343
echo "-l <dir>, --local <dir> Clone SScanSS 2 from local directory (requires git)"
4444
echo "-d <dir>, --build-dir <dir> Specify build directory (temp directory will be used if not provided)"
4545
echo "-t <arg>, --tag <arg> Clone specific tag of SScanSS 2 from local (requires git) or web"
46-
echo "-r, --remote Clone SScanSS 2 from Github repo"
46+
echo "-r, --remote Clone SScanSS 2 from GitHub repo"
4747
exit 0
4848
fi
4949

sscanss/app/window/presenter.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, view):
3333
raise FileNotFoundError("No instrument description file was found.")
3434

3535
self.worker = None
36-
36+
self.can_discard = False # Need a better solution
3737
self.recent_list_size = 10 # Maximum size of the recent project list
3838

3939
def notifyError(self, message, exception):
@@ -119,34 +119,42 @@ def projectCreationError(self, exception, args):
119119

120120
self.notifyError(msg, exception)
121121

122-
def saveProject(self, save_as=False):
122+
def saveProject(self, save_as=False, callback=None):
123123
"""Saves a project to a file. A file dialog should be opened for the first save
124124
after which the function will save to the same location. if save_as is True a dialog is
125125
opened every time
126126
127127
:param save_as: indicates if file dialog should be used
128128
:type save_as: bool
129+
:param callback: callback to call after success save
130+
:type callback: Optional[Callable[None, None]]
129131
"""
130132
# Avoids saving when there are no changes
131133
if self.view.undo_stack.isClean() and self.model.save_path and not save_as:
132134
return
133-
134135
filename = self.model.save_path
135136
if save_as or not filename:
136137
filename = self.view.showSaveDialog('hdf5 File (*.h5)', title='Save Project')
137138
if not filename:
138139
return
139-
140140
error_msg = f'An error occurred while attempting to save this project ({filename}).'
141+
142+
def on_success():
143+
self._saveProjectSuccess()
144+
if callback is not None:
145+
callback()
146+
141147
self.useWorker(self._saveProjectHelper, [filename],
142148
message='Saving Project to File',
143149
on_failure=lambda e: self.notifyError(error_msg, e),
144-
on_complete=self.view.progress_dialog.close)
150+
on_success=on_success)
145151

146152
def _saveProjectHelper(self, filename):
147153
self.model.saveProjectData(filename)
148154
self.updateRecentProjects(filename)
149155
self.model.save_path = filename
156+
157+
def _saveProjectSuccess(self):
150158
self.view.showProjectName()
151159
self.view.undo_stack.setClean()
152160

@@ -157,9 +165,6 @@ def openProject(self, filename=''):
157165
:param filename: full path of file
158166
:type filename: str
159167
"""
160-
if not self.confirmSave():
161-
return
162-
163168
if not filename:
164169
filename = self.view.showOpenDialog('hdf5 File (*.h5)',
165170
title='Open Project',
@@ -206,29 +211,26 @@ def projectOpenError(self, exception, args):
206211

207212
self.notifyError(msg, exception)
208213

209-
def confirmSave(self):
214+
def confirmSave(self, callback):
210215
"""Checks if the project is saved and asks the user to save if necessary
211216
212-
:return: indicates if the project is saved or user chose to discard changes
213-
:rtype: bool
217+
:param callback: callback to call if the project is saved or discarded
218+
:type callback: Callable[None, None]
214219
"""
215220
if self.model.project_data is None or self.view.undo_stack.isClean():
216-
return True
221+
callback()
222+
return
217223

224+
self.can_discard = False
218225
reply = self.view.showSaveDiscardMessage(self.model.project_data['name'])
219-
220226
if reply == MessageReplyType.Save:
221227
if self.model.save_path:
222-
self.saveProject()
223-
return True
228+
self.saveProject(callback=callback)
224229
else:
225-
self.saveProject(save_as=True)
226-
return self.view.undo_stack.isClean()
227-
230+
self.saveProject(save_as=True, callback=callback)
228231
elif reply == MessageReplyType.Discard:
229-
return True
230-
else:
231-
return False
232+
self.can_discard = True
233+
callback()
232234

233235
def updateRecentProjects(self, filename):
234236
"""Adds a filename entry to the front of the recent projects list
@@ -767,7 +769,7 @@ def alignSample(self, matrix):
767769

768770
def alignSampleWithPose(self, pose):
769771
"""Aligns the sample on instrument using specified 6D pose. Pose contains 3D translation
770-
(X, Y, Z) and 3D orientation (XYZ euler angles)
772+
(X, Y, Z) and 3D orientation (ZYX euler angles)
771773
772774
:param pose: position and orientation
773775
:type pose: List[float]

sscanss/app/window/view.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@
2020
MAIN_WINDOW_TITLE = 'SScanSS 2'
2121

2222

23+
class RecentFileCallback:
24+
"""A callback to open a project with the given path
25+
26+
:param view: instance of main window
27+
:type view: MainWindow
28+
:param filename: path of the file
29+
:type filename: str
30+
"""
31+
def __init__(self, view, filename):
32+
33+
self.view = view
34+
self.filename = filename
35+
36+
def __call__(self):
37+
return self.view.presenter.openProject(self.filename)
38+
39+
2340
class MainWindow(QtWidgets.QMainWindow):
2441
"""Creates the main view for the sscanss app"""
2542
def __init__(self):
@@ -67,13 +84,13 @@ def createActions(self):
6784
self.new_project_action.setStatusTip('Create a new project')
6885
self.new_project_action.setIcon(QtGui.QIcon(IconEngine('file.png')))
6986
self.new_project_action.setShortcut(QtGui.QKeySequence.StandardKey.New)
70-
self.new_project_action.triggered.connect(self.showNewProjectDialog)
87+
self.new_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.showNewProjectDialog))
7188

7289
self.open_project_action = QtGui.QAction('&Open Project', self)
7390
self.open_project_action.setStatusTip('Open an existing project')
7491
self.open_project_action.setIcon(QtGui.QIcon(IconEngine('folder-open.png')))
7592
self.open_project_action.setShortcut(QtGui.QKeySequence.StandardKey.Open)
76-
self.open_project_action.triggered.connect(lambda: self.presenter.openProject())
93+
self.open_project_action.triggered.connect(lambda: self.presenter.confirmSave(self.presenter.openProject()))
7794

7895
self.save_project_action = QtGui.QAction('&Save Project', self)
7996
self.save_project_action.setStatusTip('Save project')
@@ -717,14 +734,22 @@ def populateRecentMenu(self):
717734
if self.recent_projects:
718735
for project in self.recent_projects:
719736
recent_project_action = QtGui.QAction(project, self)
720-
recent_project_action.triggered.connect(lambda ignore, p=project: self.presenter.openProject(p))
737+
callback = RecentFileCallback(self, project)
738+
recent_project_action.triggered.connect(lambda ignore, c=callback: self.presenter.confirmSave(c))
721739
self.recent_menu.addAction(recent_project_action)
722740
else:
723741
recent_project_action = QtGui.QAction('None', self)
724742
self.recent_menu.addAction(recent_project_action)
725743

744+
def canClose(self):
745+
if self.undo_stack.isClean() or self.presenter.can_discard:
746+
return True
747+
else:
748+
self.presenter.confirmSave(self.close)
749+
return False
750+
726751
def closeEvent(self, event):
727-
if self.presenter.confirmSave():
752+
if self.canClose():
728753
settings.system.setValue(settings.Key.Geometry.value, self.saveGeometry())
729754
if self.recent_projects:
730755
settings.system.setValue(settings.Key.Recent_Projects.value, self.recent_projects)
@@ -836,9 +861,6 @@ def __createChangeCollimatorAction(self, name, active, detector):
836861

837862
def showNewProjectDialog(self):
838863
"""Opens the new project dialog"""
839-
if not self.presenter.confirmSave():
840-
return
841-
842864
self.createNonModalDialog(ProjectDialog)
843865
self.non_modal_dialog.updateRecentProjects(self.recent_projects)
844866
self.non_modal_dialog.show()
@@ -1157,7 +1179,7 @@ def __init__(self, parent):
11571179
self.worker.job_failed.connect(self.onFailure)
11581180

11591181
def check(self, startup=False):
1160-
"""Asynchronously checks for new release using the Github release API and notifies the user when
1182+
"""Asynchronously checks for new release using the GitHub release API and notifies the user when
11611183
update is found, not found or an error occurred. When startup is true, the user will
11621184
only be notified if update is found.
11631185

tests/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def click_check_box(check_box):
170170
:param check_box: widget to click
171171
:type check_box: QtWidgets.QCheckBox
172172
"""
173-
pos = QPoint(2, check_box.height() // 2)
173+
pos = QPoint(10, check_box.height() // 2)
174174
QTest.mouseClick(check_box, Qt.MouseButton.LeftButton, pos=pos)
175175

176176

tests/test_main_presenter.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,41 +175,50 @@ def testUpdateRecentProjects(self):
175175
self.assertEqual(self.view_mock.recent_projects, [r'C:\folder\test.png'])
176176

177177
def testConfirmSave(self):
178-
# confirmSave should return True when project_data is None
178+
callback = mock.Mock()
179+
# confirmSave should call callback when project_data is None
179180
self.model_mock.return_value.project_data = None
180-
self.assertTrue(self.presenter.confirmSave())
181+
self.presenter.confirmSave(callback)
182+
callback.assert_called_once()
181183
self.view_mock.showSaveDiscardMessage.assert_not_called()
182184

183-
# confirmSave should return True when there are no unsaved changes
185+
# confirmSave should call callback when there are no unsaved changes
184186
# and the save-discard message should not be called
185187
self.model_mock.return_value.project_data = self.test_project_data
186188
self.view_mock.undo_stack.setClean()
187-
self.assertTrue(self.presenter.confirmSave())
189+
self.presenter.confirmSave(callback)
190+
self.assertEqual(callback.call_count, 2)
188191
self.view_mock.showSaveDiscardMessage.assert_not_called()
189192

190193
# confirmSave should return False when user selects cancel on
191194
# the save-discard message box
192195
self.view_mock.undo_stack.resetClean()
193196
self.view_mock.showSaveDiscardMessage.return_value = MessageReplyType.Cancel
194-
self.assertFalse(self.presenter.confirmSave())
197+
self.presenter.confirmSave(callback)
198+
self.assertEqual(callback.call_count, 2)
195199

196200
# confirmSave should return True when user selects discard on
197201
# the save-discard message box
202+
self.assertFalse(self.presenter.can_discard)
198203
self.view_mock.showSaveDiscardMessage.return_value = MessageReplyType.Discard
199-
self.assertTrue(self.presenter.confirmSave())
204+
self.presenter.confirmSave(callback)
205+
self.assertEqual(callback.call_count, 3)
206+
self.assertTrue(self.presenter.can_discard)
200207

201208
# confirmSave should call save (if save path exist) then return True
202209
# when user selects save on the save-discard message box
203210
self.model_mock.return_value.save_path = self.test_filename_1
204211
self.presenter.saveProject = mock.create_autospec(self.presenter.saveProject)
205212
self.view_mock.showSaveDiscardMessage.return_value = MessageReplyType.Save
206-
self.assertTrue(self.presenter.confirmSave())
207-
self.presenter.saveProject.assert_called_with()
213+
self.presenter.confirmSave(callback)
214+
self.assertEqual(callback.call_count, 3)
215+
self.assertFalse(self.presenter.can_discard)
216+
self.presenter.saveProject.assert_called_with(callback=callback)
208217

209218
# confirmSave should call save_as (if save_path does not exist)
210219
self.model_mock.return_value.save_path = ""
211-
self.presenter.confirmSave()
212-
self.presenter.saveProject.assert_called_with(save_as=True)
220+
self.presenter.confirmSave(callback)
221+
self.presenter.saveProject.assert_called_with(save_as=True, callback=callback)
213222

214223
def testConfirmClearStack(self):
215224
self.view_mock.undo_stack = mock.Mock()

0 commit comments

Comments
 (0)