diff --git a/src/mslice/models/mslice_ads_observer.py b/src/mslice/models/mslice_ads_observer.py new file mode 100644 index 00000000..64980012 --- /dev/null +++ b/src/mslice/models/mslice_ads_observer.py @@ -0,0 +1,59 @@ +from functools import wraps +import sys + +from mantid.api import AnalysisDataServiceObserver + + +def _catch_exceptions(func): + """ + Catch all exceptions in method and print a traceback to stderr + """ + + @wraps(func) + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception: + sys.stderr.write("Error occurred in handler:\n") + import traceback + + traceback.print_exc() + + return wrapper + + +class MSliceADSObserver(AnalysisDataServiceObserver): + def __init__(self, delete_callback, clear_callback, rename_callback): + super(MSliceADSObserver, self).__init__() + self.delete_callback = delete_callback + self.clear_callback = clear_callback + self.rename_callback = rename_callback + + self.observeDelete(True) + self.observeRename(True) + self.observeClear(True) + + @_catch_exceptions + def deleteHandle(self, workspace_name, workspace): + """ + Called when the ADS deletes a workspace, removes it from the dict of tracked workspaces. + :param workspace_name: name of the workspace + :param workspace: reference to the workspace (not used) + """ + self.delete_callback(workspace_name) + + @_catch_exceptions + def renameHandle(self, old_workspace_name, new_workspace_name): + """ + Called when the ADS renames a workspace, updates the dict with the new name. + :param old_workspace_name: original name of the workspace + :param new_workspace_name: new name for the workspace + """ + self.rename_callback(old_workspace_name, new_workspace_name) + + @_catch_exceptions + def clearHandle(self): + """ + Called when the ADS has been cleared, removes all data. + """ + self.clear_callback() diff --git a/src/mslice/models/workspacemanager/workspace_provider.py b/src/mslice/models/workspacemanager/workspace_provider.py index dc16e31e..86863fa4 100644 --- a/src/mslice/models/workspacemanager/workspace_provider.py +++ b/src/mslice/models/workspacemanager/workspace_provider.py @@ -36,6 +36,10 @@ def get_visible_workspace_names(): return [key for key in iterkeys(_loaded_workspaces) if key[:2] != '__'] +def get_workspace_names(): + return [key for key in _loaded_workspaces.keys()] + + def get_workspace_name(workspace): """Returns the name of a workspace given the workspace handle""" if isinstance(workspace, string_types): diff --git a/src/mslice/plotting/plot_window/interactive_cut.py b/src/mslice/plotting/plot_window/interactive_cut.py index 1ce6dcc4..9f7adabd 100644 --- a/src/mslice/plotting/plot_window/interactive_cut.py +++ b/src/mslice/plotting/plot_window/interactive_cut.py @@ -114,7 +114,7 @@ def flip_axis(self): self.plot_cut(*self.rect.extents) def window_closing(self): - self.slice_plot.toggle_interactive_cuts() + self.slice_plot.toggle_interactive_cuts(False) self.slice_plot.plot_window.action_interactive_cuts.setChecked(False) def refresh_rect_selector(self, ax): @@ -125,8 +125,9 @@ def refresh_rect_selector(self, ax): self.rect.extents = extents self.slice_plot.set_cross_cursor() - def store_icut_cut_upon_toggle_and_reset(self): - self._cut_plotter_presenter.store_icut_cut() + def store_icut_cut_upon_toggle_and_reset(self, store=True): + if store: + self._cut_plotter_presenter.store_icut_cut() self._cut_plotter_presenter.set_icut_cut(None) def set_icut_intensity_category(self, intensity_type): diff --git a/src/mslice/plotting/plot_window/slice_plot.py b/src/mslice/plotting/plot_window/slice_plot.py index 7115579a..dc24a965 100644 --- a/src/mslice/plotting/plot_window/slice_plot.py +++ b/src/mslice/plotting/plot_window/slice_plot.py @@ -403,11 +403,11 @@ def _update_lines(self): self.update_legend() self._canvas.draw() - def toggle_interactive_cuts(self): - self.toggle_icut_button() + def toggle_interactive_cuts(self, store=True): + self.toggle_icut_button(store) self.toggle_icut() - def toggle_icut_button(self): + def toggle_icut_button(self, store=True): if not self.icut: self.manager.picking_connected(False) if self.plot_window.action_zoom_in.isChecked(): @@ -428,7 +428,7 @@ def toggle_icut_button(self): self.plot_window.action_flip_axis.setVisible(False) self._canvas.setCursor(Qt.ArrowCursor) self.icut.set_icut_intensity_category(self.intensity_type) - self.icut.store_icut_cut_upon_toggle_and_reset() + self.icut.store_icut_cut_upon_toggle_and_reset(store) self.plot_window.menu_intensity.setDisabled(False) def toggle_icut(self): diff --git a/src/mslice/presenters/workspace_manager_presenter.py b/src/mslice/presenters/workspace_manager_presenter.py index daa06f00..edea692e 100644 --- a/src/mslice/presenters/workspace_manager_presenter.py +++ b/src/mslice/presenters/workspace_manager_presenter.py @@ -4,13 +4,15 @@ from .busy import show_busy from mslice.widgets.workspacemanager.command import Command from mslice.widgets.workspacemanager import TAB_2D, TAB_NONPSD +from mslice.models.mslice_ads_observer import MSliceADSObserver from mslice.models.workspacemanager.file_io import get_save_directory from mslice.models.workspacemanager.workspace_algorithms import (save_workspaces, export_workspace_to_ads, subtract, is_pixel_workspace, combine_workspace, add_workspace_runs, scale_workspaces, remove_workspace_from_ads) from mslice.models.workspacemanager.workspace_provider import (get_workspace_handle, get_visible_workspace_names, - get_workspace_name, delete_workspace, rename_workspace) + get_workspace_names, get_workspace_name, + delete_workspace, rename_workspace) from .interfaces.workspace_manager_presenter import WorkspaceManagerPresenterInterface from .interfaces.main_presenter import MainPresenterInterface from .validation_decorators import require_main_presenter @@ -38,6 +40,7 @@ def __init__(self, workspace_view): lambda: self._workspace_manager_view._display_error('Please select a Compose action from the dropdown menu'), Command.Scale: self._scale_workspace, Command.Bose: lambda: self._scale_workspace(is_bose=True)} + self._ads_observer = MSliceADSObserver(self.delete_handle, self.clear_handle, self.rename_handle) def register_master(self, main_presenter): assert (isinstance(main_presenter, MainPresenterInterface)) @@ -228,3 +231,19 @@ def update_displayed_workspaces(self): def _clear_displayed_error(self): self._workspace_manager_view.clear_displayed_error() + + def delete_handle(self, workspace): + delete_workspace(workspace) + self.update_displayed_workspaces() + + def clear_handle(self): + for workspace in get_workspace_names(): + delete_workspace(workspace) + self.update_displayed_workspaces() + + def rename_handle(self, workspace, new_name): + if new_name is None: + return + if workspace in get_visible_workspace_names(): + rename_workspace(workspace, new_name) + self.update_displayed_workspaces() diff --git a/src/mslice/widgets/workspacemanager/workspacemanager.py b/src/mslice/widgets/workspacemanager/workspacemanager.py index 6d59938e..5909da71 100644 --- a/src/mslice/widgets/workspacemanager/workspacemanager.py +++ b/src/mslice/widgets/workspacemanager/workspacemanager.py @@ -88,12 +88,15 @@ def add_workspace(self, workspace): raise TypeError("Loaded file is not a valid workspace") def display_loaded_workspaces(self, workspaces): + to_be_removed = [] for workspace in workspaces: if workspace not in self.onscreen_workspaces: self.add_workspace(workspace) for workspace in self.onscreen_workspaces: if workspace not in workspaces: - self.remove_workspace(workspace) + to_be_removed.append(workspace) + for workspace in to_be_removed: + self.remove_workspace(workspace) def remove_workspace(self, workspace): """Remove workspace from list. diff --git a/tests/workspacemanager_presenter_ads_test.py b/tests/workspacemanager_presenter_ads_test.py new file mode 100644 index 00000000..1c2a38f9 --- /dev/null +++ b/tests/workspacemanager_presenter_ads_test.py @@ -0,0 +1,56 @@ +from __future__ import (absolute_import, division, print_function) +import unittest + +from mock import MagicMock + +from mantid.api import AnalysisDataService +from mantid.simpleapi import CreateSampleWorkspace, RenameWorkspace + +from mslice.models.mslice_ads_observer import MSliceADSObserver +from mslice.presenters.workspace_manager_presenter import WorkspaceManagerPresenter + + +class WorkspaceManagerPresenterTest(unittest.TestCase): + + def test_ensure_that_the_ads_observer_calls_delete_handle(self): + presenter = WorkspaceManagerPresenter(MagicMock()) + presenter.delete_handle = MagicMock() + self.assertTrue(isinstance(presenter._ads_observer, MSliceADSObserver)) + presenter._ads_observer = MSliceADSObserver( + presenter.delete_handle, presenter.clear_handle, presenter.rename_handle + ) + + CreateSampleWorkspace(OutputWorkspace="ws", StoreInADS=True) + AnalysisDataService.remove("ws") + + presenter.delete_handle.assert_called_once_with("ws") + + def test_ensure_that_the_ads_observer_calls_rename_handle(self): + presenter = WorkspaceManagerPresenter(MagicMock()) + presenter.rename_handle = MagicMock() + self.assertTrue(isinstance(presenter._ads_observer, MSliceADSObserver)) + presenter._ads_observer = MSliceADSObserver( + presenter.delete_handle, presenter.clear_handle, presenter.rename_handle + ) + + CreateSampleWorkspace(OutputWorkspace="ws", StoreInADS=True) + RenameWorkspace(InputWorkspace="ws", OutputWorkspace="ws1") + + presenter.rename_handle.assert_called_once_with("ws", "ws1") + + def test_ensure_that_the_ads_observer_calls_clear_handle(self): + presenter = WorkspaceManagerPresenter(MagicMock()) + presenter.clear_handle = MagicMock() + self.assertTrue(isinstance(presenter._ads_observer, MSliceADSObserver)) + presenter._ads_observer = MSliceADSObserver( + presenter.delete_handle, presenter.clear_handle, presenter.rename_handle + ) + + CreateSampleWorkspace(OutputWorkspace="ws", StoreInADS=True) + AnalysisDataService.clear(True) + + presenter.clear_handle.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/workspacemanager_presenter_test.py b/tests/workspacemanager_presenter_test.py index 3030db56..a62851da 100644 --- a/tests/workspacemanager_presenter_test.py +++ b/tests/workspacemanager_presenter_test.py @@ -38,6 +38,7 @@ def setUp(self): self.mainview = mock.create_autospec(MainView) self.main_presenter = mock.create_autospec(MainPresenterInterface) self.mainview.get_presenter = mock.Mock(return_value=self.main_presenter) + self._ads_observer = mock.Mock() def test_register_master_success(self): workspace_presenter = WorkspaceManagerPresenter(self.view) @@ -168,12 +169,12 @@ def test_remove_workspace(self, delete_ws_mock): # Create a workspace that reports a single selected workspace on calls to get_workspace_selected workspace_to_be_removed = CloneWorkspace(self.m_workspace.raw_ws, OutputWorkspace='file1') self.view.get_workspace_selected = mock.Mock(return_value=[workspace_to_be_removed]) - self.view.display_loaded_workspaces = mock.Mock() self.presenter.notify(Command.RemoveSelectedWorkspaces) self.view.get_workspace_selected.assert_called_once_with() - delete_ws_mock.assert_called_once_with(workspace_to_be_removed) - self.view.display_loaded_workspaces.assert_called_once() + delete_calls = [call(workspace_to_be_removed)] + delete_ws_mock.assert_has_calls(delete_calls) + self.assertTrue(self.view.display_loaded_workspaces.called) @patch('mslice.presenters.workspace_manager_presenter.delete_workspace') def test_remove_multiple_workspaces(self, delete_ws_mock):