class ProjectTest(unittest.TestCase): def setUp(self): self.project = Project() def tearDown(self): ADS.clear() def test_save_calls_save_as_when_last_location_is_not_none(self): self.project.save_as = mock.MagicMock() self.project.save() self.assertEqual(self.project.save_as.call_count, 1) def test_save_does_not_call_save_as_when_last_location_is_not_none(self): self.project.save_as = mock.MagicMock() self.project.last_project_location = "1" self.assertEqual(self.project.save_as.call_count, 0) def test_save_saves_project_successfully(self): working_directory = tempfile.mkdtemp() self.project.last_project_location = working_directory CreateSampleWorkspace(OutputWorkspace="ws1") self.project._offer_overwriting_gui = mock.MagicMock(return_value=QMessageBox.Yes) self.project.save() self.assertTrue(os.path.isdir(working_directory)) file_list = os.listdir(working_directory) self.assertTrue(os.path.basename(working_directory) + ".mtdproj" in file_list) self.assertTrue("ws1.nxs" in file_list) self.assertEqual(self.project._offer_overwriting_gui.call_count, 1) def test_save_as_saves_project_successfully(self): working_directory = tempfile.mkdtemp() self.project._save_file_dialog = mock.MagicMock(return_value=working_directory) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as() self.assertEqual(self.project._save_file_dialog.call_count, 1) self.assertTrue(os.path.isdir(working_directory)) file_list = os.listdir(working_directory) self.assertTrue(os.path.basename(working_directory) + ".mtdproj" in file_list) self.assertTrue("ws1.nxs" in file_list) def test_load_calls_loads_successfully(self): working_directory = tempfile.mkdtemp() return_value_for_load = os.path.join(working_directory, os.path.basename(working_directory) + ".mtdproj") self.project._save_file_dialog = mock.MagicMock(return_value=working_directory) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as() self.assertEqual(self.project._save_file_dialog.call_count, 1) ADS.clear() self.project._load_file_dialog = mock.MagicMock(return_value=return_value_for_load) self.project.load() self.assertEqual(self.project._load_file_dialog.call_count, 1) self.assertEqual(["ws1"], ADS.getObjectNames()) def test_offer_save_does_nothing_if_saved_is_true(self): self.assertEqual(self.project.offer_save(None), None) def test_offer_save_does_something_if_saved_is_false(self): self.project._offer_save_message_box = mock.MagicMock(return_value=QMessageBox.Yes) self.project.save = mock.MagicMock() # Add something to the ads so __saved is set to false CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(self.project.offer_save(None), False) self.assertEqual(self.project.save.call_count, 1) self.assertEqual(self.project._offer_save_message_box.call_count, 1) def test_adding_to_ads_calls_any_change_handle(self): self.project.anyChangeHandle = mock.MagicMock() CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_removing_from_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() ADS.remove("ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_grouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") self.project.anyChangeHandle = mock.MagicMock() GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") # Called twice because group is made and then added to the ADS self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_renaming_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() RenameWorkspace(InputWorkspace="ws1", OutputWorkspace="ws2") # Called twice because first workspace is removed and second is added self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_ungrouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") self.project.anyChangeHandle = mock.MagicMock() UnGroupWorkspace(InputWorkspace="NewGroup") # 1 for removing old group and 1 for something else but 2 seems right self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_group_updated_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") CreateSampleWorkspace(OutputWorkspace="ws3") self.project.anyChangeHandle = mock.MagicMock() ADS.addToGroup("NewGroup", "ws3") self.assertEqual(1, self.project.anyChangeHandle.call_count)
class MainWindow(QMainWindow): DOCKOPTIONS = QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks def __init__(self): QMainWindow.__init__(self) # -- instance attributes -- self.setWindowTitle(MAIN_WINDOW_TITLE) self.setObjectName(MAIN_WINDOW_OBJECT_NAME) # widgets self.messagedisplay = None self.ipythonconsole = None self.workspacewidget = None self.editor = None self.algorithm_selector = None self.plot_selector = None self.interface_manager = None self.script_repository = None self.widgets = [] # Widget layout map: required for use in Qt.connection self._layout_widget_info = None # Menus self.file_menu = None self.file_menu_actions = None self.view_menu = None self.view_menu_actions = None self.view_menu_layouts = None self.interfaces_menu = None self.help_menu = None self.help_menu_actions = None # Allow splash screen text to be overridden in set_splash self.splash = SPLASH # Layout self.setDockOptions(self.DOCKOPTIONS) # Project self.project = None self.project_recovery = None # Interfaces self.interface_manager = None self.interface_executor = None self.interface_list = None def setup(self): # menus must be done first so they can be filled by the # plugins in register_plugin self.create_menus() # widgets # Log message display must be imported first self.set_splash("Loading message display") from workbench.plugins.logmessagedisplay import LogMessageDisplay self.messagedisplay = LogMessageDisplay(self) # this takes over stdout/stderr self.messagedisplay.register_plugin() self.widgets.append(self.messagedisplay) self.set_splash("Loading Algorithm Selector") from workbench.plugins.algorithmselectorwidget import AlgorithmSelector self.algorithm_selector = AlgorithmSelector(self) self.algorithm_selector.register_plugin() self.widgets.append(self.algorithm_selector) self.set_splash("Loading Plot Selector") from workbench.plugins.plotselectorwidget import PlotSelector self.plot_selector = PlotSelector(self) self.plot_selector.register_plugin() self.widgets.append(self.plot_selector) self.set_splash("Loading code editing widget") from workbench.plugins.editor import MultiFileEditor self.editor = MultiFileEditor(self) self.messagedisplay.display.setActiveScript( self.editor.editors.current_tab_filename) self.editor.register_plugin() self.widgets.append(self.editor) self.editor.editors.sig_code_exec_start.connect( self.messagedisplay.script_executing) self.editor.editors.sig_file_name_changed.connect( self.messagedisplay.file_name_modified) self.editor.editors.sig_current_tab_changed.connect( self.messagedisplay.current_tab_changed) self.set_splash("Loading IPython console") from workbench.plugins.jupyterconsole import JupyterConsole self.ipythonconsole = JupyterConsole(self) self.ipythonconsole.register_plugin() self.widgets.append(self.ipythonconsole) from workbench.plugins.workspacewidget import WorkspaceWidget self.workspacewidget = WorkspaceWidget(self) self.workspacewidget.register_plugin() prompt = CONF.get('project/prompt_on_deleting_workspace') self.workspacewidget.workspacewidget.enableDeletePrompt(bool(prompt)) self.widgets.append(self.workspacewidget) # set the link between the algorithm and workspace widget self.algorithm_selector.algorithm_selector.set_get_selected_workspace_fn( self.workspacewidget.workspacewidget.getSelectedWorkspaceNames) # Set up the project, recovery and interface manager objects self.project = Project(GlobalFigureManager, find_all_windows_that_are_savable) self.project_recovery = ProjectRecovery( globalfiguremanager=GlobalFigureManager, multifileinterpreter=self.editor.editors, main_window=self) self.interface_executor = PythonCodeExecution() self.interface_executor.sig_exec_error.connect( lambda errobj: logger.warning(str(errobj))) self.interface_manager = InterfaceManager() # uses default configuration as necessary self.setup_default_layouts() self.create_actions() self.readSettings(CONF) self.config_updated() def post_mantid_init(self): """Run any setup that requires mantid to have been initialized """ self.redirect_python_warnings() self.populate_menus() self.algorithm_selector.refresh() # turn on algorithm factory notifications from mantid.api import AlgorithmFactory algorithm_factory = AlgorithmFactory.Instance() algorithm_factory.enableNotifications() def set_splash(self, msg=None): if not self.splash: return if msg: self.splash.showMessage( msg, Qt.AlignBottom | Qt.AlignLeft | Qt.AlignAbsolute, QColor(Qt.black)) QApplication.processEvents(QEventLoop.AllEvents) def create_menus(self): self.file_menu = self.menuBar().addMenu("&File") self.view_menu = self.menuBar().addMenu("&View") self.interfaces_menu = self.menuBar().addMenu('&Interfaces') self.help_menu = self.menuBar().addMenu('&Help') def create_actions(self): # --- general application menu options -- # file menu action_open = create_action(self, "Open Script", on_triggered=self.open_file, shortcut="Ctrl+O", shortcut_context=Qt.ApplicationShortcut) action_load_project = create_action(self, "Open Project", on_triggered=self.load_project) action_save_script = create_action( self, "Save Script", on_triggered=self.save_script, shortcut="Ctrl+S", shortcut_context=Qt.ApplicationShortcut) action_save_script_as = create_action(self, "Save Script as...", on_triggered=self.save_script_as) action_generate_ws_script = create_action( self, "Generate Recovery Script", on_triggered=self.generate_script_from_workspaces) action_save_project = create_action(self, "Save Project", on_triggered=self.save_project) action_save_project_as = create_action( self, "Save Project as...", on_triggered=self.save_project_as) action_manage_directories = create_action( self, "Manage User Directories", on_triggered=self.open_manage_directories) action_script_repository = create_action( self, "Script Repository", on_triggered=self.open_script_repository) action_settings = create_action(self, "Settings", on_triggered=self.open_settings_window) action_quit = create_action(self, "&Quit", on_triggered=self.close, shortcut="Ctrl+Q", shortcut_context=Qt.ApplicationShortcut) self.file_menu_actions = [ action_open, action_load_project, None, action_save_script, action_save_script_as, action_generate_ws_script, None, action_save_project, action_save_project_as, None, action_settings, None, action_manage_directories, None, action_script_repository, None, action_quit ] # view menu action_restore_default = create_action( self, "Restore Default Layout", on_triggered=self.setup_default_layouts, shortcut="Shift+F10", shortcut_context=Qt.ApplicationShortcut) self.view_menu_layouts = self.view_menu.addMenu("&User Layouts") self.populate_layout_menu() self.view_menu_actions = [action_restore_default, None ] + self.create_widget_actions() # help menu action_mantid_help = create_action( self, "Mantid Help", on_triggered=self.open_mantid_help, shortcut='F1', shortcut_context=Qt.ApplicationShortcut) action_algorithm_descriptions = create_action( self, 'Algorithm Descriptions', on_triggered=self.open_algorithm_descriptions_help) action_mantid_concepts = create_action( self, "Mantid Concepts", on_triggered=self.open_mantid_concepts_help) action_mantid_homepage = create_action( self, "Mantid Homepage", on_triggered=self.open_mantid_homepage) action_mantid_forum = create_action( self, "Mantid Forum", on_triggered=self.open_mantid_forum) action_about = create_action(self, "About Mantid Workbench", on_triggered=self.open_about) self.help_menu_actions = [ action_mantid_help, action_mantid_concepts, action_algorithm_descriptions, None, action_mantid_homepage, action_mantid_forum, None, action_about ] def create_widget_actions(self): """ Creates menu actions to show/hide dockable widgets. This uses all widgets that are in self.widgets :return: A list of show/hide actions for all widgets """ widget_actions = [] for widget in self.widgets: action = widget.dockwidget.toggleViewAction() widget_actions.append(action) return widget_actions def populate_menus(self): # Link to menus add_actions(self.file_menu, self.file_menu_actions) add_actions(self.view_menu, self.view_menu_actions) add_actions(self.help_menu, self.help_menu_actions) self.populate_interfaces_menu() def launch_custom_python_gui(self, filename): self.interface_executor.execute(open(filename).read(), filename) def launch_custom_cpp_gui(self, interface_name, submenu=None): """Create a new interface window if one does not already exist, else show existing window""" object_name = 'custom-cpp-interface-' + interface_name window = find_window(object_name, QMainWindow) if window is None: interface = self.interface_manager.createSubWindow(interface_name) interface.setObjectName(object_name) interface.setAttribute(Qt.WA_DeleteOnClose, True) # make indirect interfaces children of workbench if submenu == "Indirect": interface.setParent(self, interface.windowFlags()) interface.show() else: if window.windowState() == Qt.WindowMinimized: window.setWindowState(Qt.WindowActive) else: window.raise_() def populate_interfaces_menu(self): """Populate then Interfaces menu with all Python and C++ interfaces""" self.interfaces_menu.clear() interface_dir = ConfigService['mantidqt.python_interfaces_directory'] self.interface_list = self._discover_python_interfaces(interface_dir) self._discover_cpp_interfaces(self.interface_list) hidden_interfaces = ConfigService[ 'interfaces.categories.hidden'].split(';') keys = list(self.interface_list.keys()) keys.sort() for key in keys: if key not in hidden_interfaces: submenu = self.interfaces_menu.addMenu(key) names = self.interface_list[key] names.sort() for name in names: if '.py' in name: action = submenu.addAction( name.replace('.py', '').replace('_', ' ')) script = os.path.join(interface_dir, name) action.triggered.connect( lambda checked_py, script=script: self. launch_custom_python_gui(script)) else: action = submenu.addAction(name) action.triggered.connect( lambda checked_cpp, name=name, key=key: self. launch_custom_cpp_gui(name, key)) def redirect_python_warnings(self): """By default the warnings module writes warnings to sys.stderr. stderr is assumed to be an error channel so we don't confuse warnings with errors this redirects warnings from the warnings module to mantid.logger.warning """ import warnings def to_mantid_warning(*args, **kwargs): logger.warning(warnings.formatwarning(*args, **kwargs)) warnings.showwarning = to_mantid_warning def _discover_python_interfaces(self, interface_dir): """Return a dictionary mapping a category to a set of named Python interfaces""" items = ConfigService['mantidqt.python_interfaces'].split() # list of custom interfaces that are not qt4/qt5 compatible GUI_BLACKLIST = ['Frequency_Domain_Analysis_Old.py'] # detect the python interfaces interfaces = {} for item in items: key, scriptname = item.split('/') if not os.path.exists(os.path.join(interface_dir, scriptname)): logger.warning('Failed to find script "{}" in "{}"'.format( scriptname, interface_dir)) continue if scriptname in GUI_BLACKLIST: logger.information('Not adding gui "{}"'.format(scriptname)) continue interfaces.setdefault(key, []).append(scriptname) return interfaces def _discover_cpp_interfaces(self, interfaces): """Return a dictionary mapping a category to a set of named C++ interfaces""" cpp_interface_factory = UserSubWindowFactory.Instance() interface_names = cpp_interface_factory.keys() for name in interface_names: categories = cpp_interface_factory.categories(name) if len(categories) == 0: categories = ["General"] for category in categories: if category in interfaces.keys(): interfaces[category].append(name) else: interfaces[category] = [name] return interfaces def add_dockwidget(self, plugin): """Create a dockwidget around a plugin and add the dock to window""" dockwidget, location = plugin.create_dockwidget() self.addDockWidget(location, dockwidget) # ----------------------- Layout --------------------------------- def populate_layout_menu(self): self.view_menu_layouts.clear() try: layout_dict = CONF.get("MainWindow/user_layouts") except KeyError: layout_dict = {} layout_keys = sorted(layout_dict.keys()) layout_options = [] for item in layout_keys: layout_options.append( self.create_load_layout_action(item, layout_dict[item])) layout_options.append(None) action_settings = create_action( self, "Settings", on_triggered=self.open_settings_layout_window) layout_options.append(action_settings) add_actions(self.view_menu_layouts, layout_options) def create_load_layout_action(self, layout_name, layout): action_load_layout = create_action( self, layout_name, on_triggered=lambda: self.restoreState(layout)) return action_load_layout def prep_window_for_reset(self): """Function to reset all dock widgets to a state where they can be ordered by setup_default_layout""" for widget in self.widgets: widget.dockwidget.setFloating( False) # Bring back any floating windows self.addDockWidget(Qt.LeftDockWidgetArea, widget.dockwidget) # Un-tabify all widgets widget.toggle_view(False) def setup_default_layouts(self): """Set the default layouts of the child widgets""" # layout definition logmessages = self.messagedisplay ipython = self.ipythonconsole workspacewidget = self.workspacewidget editor = self.editor algorithm_selector = self.algorithm_selector plot_selector = self.plot_selector default_layout = { 'widgets': [ # column 0 [[workspacewidget], [algorithm_selector, plot_selector]], # column 1 [[editor, ipython]], # column 2 [[logmessages]] ], 'width-fraction': [ 0.25, # column 0 width 0.50, # column 1 width 0.25 ], # column 2 width 'height-fraction': [ [0.5, 0.5], # column 0 row heights [1.0], # column 1 row heights [1.0] ] # column 2 row heights } size = self.size() # Preserve size on reset self.arrange_layout(default_layout) self.resize(size) def arrange_layout(self, layout): """Arrange the layout of the child widgets according to the supplied layout""" self.prep_window_for_reset() widgets_layout = layout['widgets'] with widget_updates_disabled(self): # flatten list widgets = [ item for column in widgets_layout for row in column for item in row ] # show everything for w in widgets: w.toggle_view(True) # split everything on the horizontal for i in range(len(widgets) - 1): first, second = widgets[i], widgets[i + 1] self.splitDockWidget(first.dockwidget, second.dockwidget, Qt.Horizontal) # now arrange the rows for column in widgets_layout: for i in range(len(column) - 1): first_row, second_row = column[i], column[i + 1] self.splitDockWidget(first_row[0].dockwidget, second_row[0].dockwidget, Qt.Vertical) # and finally tabify those in the same position for column in widgets_layout: for row in column: for i in range(len(row) - 1): first, second = row[i], row[i + 1] self.tabifyDockWidget(first.dockwidget, second.dockwidget) # Raise front widget per row row[0].dockwidget.show() row[0].dockwidget.raise_() # ----------------------- Events --------------------------------- def closeEvent(self, event): if self.project.is_saving or self.project.is_loading: event.ignore() self.project.inform_user_not_possible() return # Check whether or not to save project if not self.project.saved: # Offer save if self.project.offer_save(self): # Cancel has been clicked event.ignore() return # Close editors if self.editor.app_closing(): # write out any changes to the mantid config file ConfigService.saveConfig(ConfigService.getUserFilename()) # write current window information to global settings object self.writeSettings(CONF) # Close all open plots # We don't want this at module scope here import matplotlib.pyplot as plt # noqa plt.close('all') app = QApplication.instance() if app is not None: app.closeAllWindows() # Kill the project recovery thread and don't restart should a save be in progress and clear out current # recovery checkpoint as it is closing properly self.project_recovery.stop_recovery_thread() self.project_recovery.closing_workbench = True self.project_recovery.remove_current_pid_folder() self.interface_manager.closeHelpWindow() event.accept() else: # Cancel was pressed when closing an editor event.ignore() # ----------------------- Slots --------------------------------- def open_file(self): # todo: when more file types are added this should # live in its own type filepath, _ = QFileDialog.getOpenFileName(self, "Open File...", "", "Python (*.py)") if not filepath: return self.editor.open_file_in_new_tab(filepath) def save_script(self): self.editor.save_current_file() def save_script_as(self): self.editor.save_current_file_as() def generate_script_from_workspaces(self): task = BlockingAsyncTaskWithCallback( target=self._generate_script_from_workspaces, blocking_cb=QApplication.processEvents) task.start() def _generate_script_from_workspaces(self): script = "from mantid.simpleapi import *\n\n" + get_all_workspace_history_from_ads( ) QAppThreadCall(self.editor.open_script_in_new_tab)(script) def save_project(self): self.project.save() def save_project_as(self): self.project.save_as() def load_project(self): self.project.load() def open_manage_directories(self): manageuserdirectories.ManageUserDirectories.openManageUserDirectories() def open_script_repository(self): self.script_repository = ScriptRepositoryView(self) self.script_repository.loadScript.connect( self.editor.open_file_in_new_tab) self.script_repository.setAttribute(Qt.WA_DeleteOnClose, True) self.script_repository.show() def open_settings_window(self): settings = SettingsPresenter(self) settings.show() def open_settings_layout_window(self): settings = SettingsPresenter(self) settings.show() settings.general_settings.focus_layout_box() def config_updated(self): """ Updates the widgets that depend on settings from the Workbench Config. """ self.editor.load_settings_from_config(CONF) self.project.load_settings_from_config(CONF) self.algorithm_selector.refresh() self.populate_interfaces_menu() self.workspacewidget.refresh_workspaces() def open_algorithm_descriptions_help(self): self.interface_manager.showAlgorithmHelp('') def open_mantid_concepts_help(self): self.interface_manager.showConceptHelp('') def open_mantid_help(self): self.interface_manager.showHelpPage('') def open_mantid_homepage(self): self.interface_manager.showWebPage('https://www.mantidproject.org') def open_mantid_forum(self): self.interface_manager.showWebPage('https://forum.mantidproject.org/') def open_about(self): about = AboutPresenter(self) about.show() def readSettings(self, settings): qapp = QApplication.instance() qapp.setAttribute(Qt.AA_UseHighDpiPixmaps) if hasattr(Qt, 'AA_EnableHighDpiScaling'): qapp.setAttribute(Qt.AA_EnableHighDpiScaling, settings.get('high_dpi_scaling')) # get the saved window geometry window_size = settings.get('MainWindow/size') if not isinstance(window_size, QSize): window_size = QSize(*window_size) window_pos = settings.get('MainWindow/position') if not isinstance(window_pos, QPoint): window_pos = QPoint(*window_pos) if settings.has('MainWindow/font'): font_string = settings.get('MainWindow/font').split(',') font = QFontDatabase().font(font_string[0], font_string[-1], int(font_string[1])) qapp.setFont(font) # make sure main window is smaller than the desktop desktop = QDesktopWidget() # this gives the maximum screen number if the position is off screen screen = desktop.screenNumber(window_pos) # recalculate the window size desktop_geom = desktop.screenGeometry(screen) w = min(desktop_geom.size().width(), window_size.width()) h = min(desktop_geom.size().height(), window_size.height()) window_size = QSize(w, h) # and position it on the supplied desktop screen x = max(window_pos.x(), desktop_geom.left()) y = max(window_pos.y(), desktop_geom.top()) window_pos = QPoint(x, y) # set the geometry self.resize(window_size) self.move(window_pos) # restore window state if settings.has('MainWindow/state'): self.restoreState(settings.get('MainWindow/state')) else: self.setWindowState(Qt.WindowMaximized) # read in settings for children AlgorithmInputHistory().readSettings(settings) for widget in self.widgets: if hasattr(widget, 'readSettings'): widget.readSettings(settings) def writeSettings(self, settings): settings.set('MainWindow/size', self.size()) # QSize settings.set('MainWindow/position', self.pos()) # QPoint settings.set('MainWindow/state', self.saveState()) # QByteArray # write out settings for children AlgorithmInputHistory().writeSettings(settings) for widget in self.widgets: if hasattr(widget, 'writeSettings'): widget.writeSettings(settings)
class ProjectTest(unittest.TestCase): def setUp(self): self.fgfm = FakeGlobalFigureManager() self.fgfm.figs = [] self.project = Project(self.fgfm, fake_window_finding_function) def tearDown(self): ADS.clear() def test_save_calls_save_as_when_last_location_is_not_none(self): self.project.save_as = mock.MagicMock() self.project.save() self.assertEqual(self.project.save_as.call_count, 1) def test_save_does_not_call_save_as_when_last_location_is_not_none(self): self.project.save_as = mock.MagicMock() self.project.last_project_location = "1" self.assertEqual(self.project.save_as.call_count, 0) def test_save_saves_project_successfully(self): working_file = os.path.join(tempfile.mkdtemp(), "temp" + ".mtdproj") self.project.last_project_location = working_file CreateSampleWorkspace(OutputWorkspace="ws1") self.project._offer_overwriting_gui = mock.MagicMock(return_value=QMessageBox.Yes) self.project.save() self.assertTrue(os.path.isfile(working_file)) file_list = os.listdir(os.path.dirname(working_file)) self.assertTrue(os.path.basename(working_file) in file_list) self.assertTrue("ws1.nxs" in file_list) self.assertEqual(self.project._offer_overwriting_gui.call_count, 1) def test_save_as_saves_project_successfully(self): working_file = os.path.join(tempfile.mkdtemp(), "temp" + ".mtdproj") working_directory = os.path.dirname(working_file) self.project._save_file_dialog = mock.MagicMock(return_value=working_file) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as() self.assertEqual(self.project._save_file_dialog.call_count, 1) self.assertTrue(os.path.isfile(working_file)) self.assertTrue(os.path.isdir(working_directory)) file_list = os.listdir(working_directory) self.assertTrue(os.path.basename(working_file) in file_list) self.assertTrue("ws1.nxs" in file_list) def test_load_calls_loads_successfully(self): working_directory = tempfile.mkdtemp() return_value_for_load = os.path.join(working_directory, os.path.basename(working_directory) + ".mtdproj") self.project._save_file_dialog = mock.MagicMock(return_value=return_value_for_load) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as() self.assertEqual(self.project._save_file_dialog.call_count, 1) ADS.clear() self.project._load_file_dialog = mock.MagicMock(return_value=return_value_for_load) self.project.load() self.assertEqual(self.project._load_file_dialog.call_count, 1) self.assertEqual(["ws1"], ADS.getObjectNames()) def test_offer_save_does_nothing_if_saved_is_true(self): self.assertEqual(self.project.offer_save(None), None) def test_offer_save_does_something_if_saved_is_false(self): self.project._offer_save_message_box = mock.MagicMock(return_value=QMessageBox.Yes) self.project.save = mock.MagicMock(return_value=None) # Add something to the ads so __saved is set to false CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(self.project.offer_save(None), False) self.assertEqual(self.project.save.call_count, 1) self.assertEqual(self.project._offer_save_message_box.call_count, 1) def test_offer_save_does_nothing_if_save_is_cancelled(self): self.project._offer_save_message_box = mock.MagicMock(return_value=QMessageBox.Yes) self.project.save = mock.MagicMock(return_value=True) # Add something to the ads so __saved is set to false CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(self.project.offer_save(None), True) def test_adding_to_ads_calls_any_change_handle(self): self.project.anyChangeHandle = mock.MagicMock() CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_removing_from_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() ADS.remove("ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_grouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") self.project.anyChangeHandle = mock.MagicMock() GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") # Called twice because group is made and then added to the ADS self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_renaming_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() RenameWorkspace(InputWorkspace="ws1", OutputWorkspace="ws2") # Called twice because first workspace is removed and second is added self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_ungrouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") self.project.anyChangeHandle = mock.MagicMock() UnGroupWorkspace(InputWorkspace="NewGroup") # 1 for removing old group and 1 for something else but 2 seems right self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_group_updated_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") CreateSampleWorkspace(OutputWorkspace="ws3") self.project.anyChangeHandle = mock.MagicMock() ADS.addToGroup("NewGroup", "ws3") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_large_file_dialog_appears_for_large_file(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project._get_project_size = mock.MagicMock(return_value= int(ConfigService.getString("projectSaving.warningSize")) + 1) self.project._offer_large_size_confirmation = mock.MagicMock() self.project._save() self.assertEqual(self.project._offer_large_size_confirmation.call_count, 1) def test_large_file_dialog_does_not_appear_for_small_file(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project._get_project_size = mock.MagicMock(return_value= int(ConfigService.getString("projectSaving.warningSize")) - 1) self.project._offer_large_size_confirmation = mock.MagicMock() self.project._save() self.assertEqual(self.project._offer_large_size_confirmation.call_count, 0) def test_is_loading_is_False_after_error_thrown_during_load(self): with mock.patch.object(self.project, '_load_file_dialog', lambda: _raise(IOError)): try: self.project.load() except IOError: pass self.assertFalse(self.project.is_loading) def test_is_loading_is_False_after_None_returned_from_load_dialog(self): # None is returned from the load dialog when a user clicks Cancel with mock.patch.object(self.project, '_load_file_dialog', lambda: None): try: self.project.load() except IOError: pass self.assertFalse(self.project.is_loading) def test_is_saving_is_False_if_error_thrown_during_save(self): with mock.patch.object(self.project, '_get_project_size', lambda x: _raise(IOError)): try: self.project._save() except IOError: pass self.assertFalse(self.project.is_saving)
class MainWindow(QMainWindow): DOCKOPTIONS = QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks def __init__(self): QMainWindow.__init__(self) # -- instance attributes -- self.setWindowTitle("Mantid Workbench") self.setObjectName("Mantid Workbench") # widgets self.messagedisplay = None self.ipythonconsole = None self.workspacewidget = None self.editor = None self.algorithm_selector = None self.plot_selector = None self.widgets = [] # Widget layout map: required for use in Qt.connection self._layout_widget_info = None # Menus self.file_menu = None self.file_menu_actions = None self.editor_menu = None self.view_menu = None self.view_menu_actions = None self.interfaces_menu = None # Allow splash screen text to be overridden in set_splash self.splash = SPLASH # Layout self.setDockOptions(self.DOCKOPTIONS) # Project self.project = None def setup(self): # menus must be done first so they can be filled by the # plugins in register_plugin self.create_menus() # widgets # Log message display must be imported first self.set_splash("Loading message display") from workbench.plugins.logmessagedisplay import LogMessageDisplay self.messagedisplay = LogMessageDisplay(self) # this takes over stdout/stderr self.messagedisplay.register_plugin() self.widgets.append(self.messagedisplay) self.set_splash("Loading Algorithm Selector") from workbench.plugins.algorithmselectorwidget import AlgorithmSelector self.algorithm_selector = AlgorithmSelector(self) self.algorithm_selector.register_plugin() self.widgets.append(self.algorithm_selector) self.set_splash("Loading Plot Selector") from workbench.plugins.plotselectorwidget import PlotSelector self.plot_selector = PlotSelector(self) self.plot_selector.register_plugin() self.widgets.append(self.plot_selector) self.set_splash("Loading code editing widget") from workbench.plugins.editor import MultiFileEditor self.editor = MultiFileEditor(self) self.editor.register_plugin() self.widgets.append(self.editor) self.set_splash("Loading IPython console") from workbench.plugins.jupyterconsole import JupyterConsole self.ipythonconsole = JupyterConsole(self) self.ipythonconsole.register_plugin() self.widgets.append(self.ipythonconsole) from workbench.plugins.workspacewidget import WorkspaceWidget self.workspacewidget = WorkspaceWidget(self) self.workspacewidget.register_plugin() self.widgets.append(self.workspacewidget) # Set up the project object self.project = Project() # uses default configuration as necessary self.readSettings(CONF) self.setup_layout() self.create_actions() self.populate_menus() def post_mantid_init(self): """Run any setup that requires mantid to have been initialized """ self.populate_interfaces_menu() self.algorithm_selector.refresh() def set_splash(self, msg=None): if not self.splash: return if msg: self.splash.showMessage(msg, Qt.AlignBottom | Qt.AlignLeft | Qt.AlignAbsolute, QColor(Qt.black)) QApplication.processEvents(QEventLoop.AllEvents) def create_menus(self): self.file_menu = self.menuBar().addMenu("&File") self.editor_menu = self.menuBar().addMenu("&Editor") self.view_menu = self.menuBar().addMenu("&View") self.interfaces_menu = self.menuBar().addMenu('&Interfaces') def create_actions(self): # --- general application menu options -- # file menu action_open = create_action(self, "Open Script", on_triggered=self.open_file, shortcut="Ctrl+O", shortcut_context=Qt.ApplicationShortcut) action_load_project = create_action(self, "Open Project", on_triggered=self.load_project) action_save_script = create_action(self, "Save Script", on_triggered=self.save_script, shortcut="Ctrl+S", shortcut_context=Qt.ApplicationShortcut) action_save_project = create_action(self, "Save Project", on_triggered=self.save_project) action_save_project_as = create_action(self, "Save Project as...", on_triggered=self.save_project_as) action_manage_directories = create_action(self, "Manage User Directories", on_triggered=self.open_manage_directories) action_quit = create_action(self, "&Quit", on_triggered=self.close, shortcut="Ctrl+Q", shortcut_context=Qt.ApplicationShortcut) self.file_menu_actions = [action_open, action_load_project, None, action_save_script, action_save_project, action_save_project_as, None, action_manage_directories, None, action_quit] # view menu action_restore_default = create_action(self, "Restore Default Layout", on_triggered=self.prep_window_for_reset, shortcut="Shift+F10", shortcut_context=Qt.ApplicationShortcut) self.view_menu_actions = [action_restore_default, None] + self.create_widget_actions() def create_widget_actions(self): """ Creates menu actions to show/hide dockable widgets. This uses all widgets that are in self.widgets :return: A list of show/hide actions for all widgets """ widget_actions = [] for widget in self.widgets: action = widget.dockwidget.toggleViewAction() widget_actions.append(action) return widget_actions def populate_menus(self): # Link to menus add_actions(self.file_menu, self.file_menu_actions) add_actions(self.view_menu, self.view_menu_actions) def launch_custom_gui(self, filename): executioner = PythonCodeExecution() executioner.sig_exec_error.connect(lambda errobj: logger.warning(str(errobj))) executioner.execute(open(filename).read(), filename) def populate_interfaces_menu(self): interface_dir = ConfigService['mantidqt.python_interfaces_directory'] items = ConfigService['mantidqt.python_interfaces'].split() # list of custom interfaces that are not qt4/qt5 compatible GUI_BLACKLIST = ['ISIS_Reflectometry_Old.py', 'ISIS_SANS_v2_experimental.py', 'Frequency_Domain_Analysis.py', 'Elemental_Analysis.py'] # detect the python interfaces interfaces = {} for item in items: key, scriptname = item.split('/') if not os.path.exists(os.path.join(interface_dir, scriptname)): logger.warning('Failed to find script "{}" in "{}"'.format(scriptname, interface_dir)) continue if scriptname in GUI_BLACKLIST: logger.information('Not adding gui "{}"'.format(scriptname)) continue temp = interfaces.get(key, []) temp.append(scriptname) interfaces[key] = temp # add the interfaces to the menu keys = list(interfaces.keys()) keys.sort() for key in keys: submenu = self.interfaces_menu.addMenu(key) names = interfaces[key] names.sort() for name in names: action = submenu.addAction(name.replace('.py', '').replace('_', ' ')) script = os.path.join(interface_dir, name) action.triggered.connect(lambda checked, script=script: self.launch_custom_gui(script)) def add_dockwidget(self, plugin): """Create a dockwidget around a plugin and add the dock to window""" dockwidget, location = plugin.create_dockwidget() self.addDockWidget(location, dockwidget) # ----------------------- Layout --------------------------------- def setup_layout(self): """Assume this is a first run of the application and set layouts accordingly""" self.setup_default_layouts() def prep_window_for_reset(self): """Function to reset all dock widgets to a state where they can be ordered by setup_default_layout""" for widget in self.widgets: widget.dockwidget.setFloating(False) # Bring back any floating windows self.addDockWidget(Qt.LeftDockWidgetArea, widget.dockwidget) # Un-tabify all widgets self.setup_default_layouts() def setup_default_layouts(self): """Set or reset the layouts of the child widgets""" # layout definition logmessages = self.messagedisplay ipython = self.ipythonconsole workspacewidget = self.workspacewidget editor = self.editor algorithm_selector = self.algorithm_selector plot_selector = self.plot_selector default_layout = { 'widgets': [ # column 0 [[workspacewidget], [algorithm_selector, plot_selector]], # column 1 [[editor, ipython]], # column 2 [[logmessages]] ], 'width-fraction': [0.25, # column 0 width 0.50, # column 1 width 0.25], # column 2 width 'height-fraction': [[0.5, 0.5], # column 0 row heights [1.0], # column 1 row heights [1.0]] # column 2 row heights } with widget_updates_disabled(self): widgets_layout = default_layout['widgets'] # flatten list widgets = [item for column in widgets_layout for row in column for item in row] # show everything for w in widgets: w.toggle_view(True) # split everything on the horizontal for i in range(len(widgets) - 1): first, second = widgets[i], widgets[i + 1] self.splitDockWidget(first.dockwidget, second.dockwidget, Qt.Horizontal) # now arrange the rows for column in widgets_layout: for i in range(len(column) - 1): first_row, second_row = column[i], column[i + 1] self.splitDockWidget(first_row[0].dockwidget, second_row[0].dockwidget, Qt.Vertical) # and finally tabify those in the same position for column in widgets_layout: for row in column: for i in range(len(row) - 1): first, second = row[i], row[i + 1] self.tabifyDockWidget(first.dockwidget, second.dockwidget) # Raise front widget per row row[0].dockwidget.show() row[0].dockwidget.raise_() # ----------------------- Events --------------------------------- def closeEvent(self, event): # Check whether or not to save project if not self.project.saved: # Offer save if self.project.offer_save(self): # Cancel has been clicked event.ignore() return # Close editors if self.editor.app_closing(): self.writeSettings(CONF) # write current window information to global settings object # Close all open plots # We don't want this at module scope here import matplotlib.pyplot as plt # noqa plt.close('all') app = QApplication.instance() if app is not None: app.closeAllWindows() event.accept() else: # Cancel was pressed when closing an editor event.ignore() # ----------------------- Slots --------------------------------- def open_file(self): # todo: when more file types are added this should # live in its own type filepath, _ = QFileDialog.getOpenFileName(self, "Open File...", "", "Python (*.py)") if not filepath: return self.editor.open_file_in_new_tab(filepath) def save_script(self): self.editor.save_current_file() def save_project(self): self.project.save() def save_project_as(self): self.project.save_as() def load_project(self): self.project.load() def open_manage_directories(self): ManageUserDirectories(self).exec_() def readSettings(self, settings): qapp = QApplication.instance() qapp.setAttribute(Qt.AA_UseHighDpiPixmaps) if hasattr(Qt, 'AA_EnableHighDpiScaling'): qapp.setAttribute(Qt.AA_EnableHighDpiScaling, settings.get('high_dpi_scaling')) # get the saved window geometry window_size = settings.get('MainWindow/size') if not isinstance(window_size, QSize): window_size = QSize(*window_size) window_pos = settings.get('MainWindow/position') if not isinstance(window_pos, QPoint): window_pos = QPoint(*window_pos) # make sure main window is smaller than the desktop desktop = QDesktopWidget() # this gives the maximum screen number if the position is off screen screen = desktop.screenNumber(window_pos) # recalculate the window size desktop_geom = desktop.screenGeometry(screen) w = min(desktop_geom.size().width(), window_size.width()) h = min(desktop_geom.size().height(), window_size.height()) window_size = QSize(w, h) # and position it on the supplied desktop screen x = max(window_pos.x(), desktop_geom.left()) y = max(window_pos.y(), desktop_geom.top()) window_pos = QPoint(x, y) # set the geometry self.resize(window_size) self.move(window_pos) # restore window state if settings.has('MainWindow/state'): self.restoreState(settings.get('MainWindow/state')) else: self.setWindowState(Qt.WindowMaximized) # read in settings for children AlgorithmInputHistory().readSettings(settings) for widget in self.widgets: if hasattr(widget, 'readSettings'): widget.readSettings(settings) def writeSettings(self, settings): settings.set('MainWindow/size', self.size()) # QSize settings.set('MainWindow/position', self.pos()) # QPoint settings.set('MainWindow/state', self.saveState()) # QByteArray # write out settings for children AlgorithmInputHistory().writeSettings(settings) for widget in self.widgets: if hasattr(widget, 'writeSettings'): widget.writeSettings(settings)
class ProjectTest(unittest.TestCase): def setUp(self): self.fgfm = FakeGlobalFigureManager() self.fgfm.figs = [] self.project = Project(self.fgfm, fake_window_finding_function) def tearDown(self): ADS.clear() def test_save_calls_save_as_when_last_location_is_not_none(self): self.project.save_as = mock.MagicMock() self.project.save() self.assertEqual(self.project.save_as.call_count, 1) def test_save_does_not_call_save_as_when_last_location_is_not_none(self): self.project.save_as = mock.MagicMock() self.project.last_project_location = "1" self.assertEqual(self.project.save_as.call_count, 0) def test_save_saves_project_successfully(self): working_directory = tempfile.mkdtemp() self.project.last_project_location = working_directory CreateSampleWorkspace(OutputWorkspace="ws1") self.project._offer_overwriting_gui = mock.MagicMock( return_value=QMessageBox.Yes) self.project.save() self.assertTrue(os.path.isdir(working_directory)) file_list = os.listdir(working_directory) self.assertTrue( os.path.basename(working_directory) + ".mtdproj" in file_list) self.assertTrue("ws1.nxs" in file_list) self.assertEqual(self.project._offer_overwriting_gui.call_count, 1) def test_save_as_saves_project_successfully(self): working_directory = tempfile.mkdtemp() self.project._save_file_dialog = mock.MagicMock( return_value=working_directory) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as() self.assertEqual(self.project._save_file_dialog.call_count, 1) self.assertTrue(os.path.isdir(working_directory)) file_list = os.listdir(working_directory) self.assertTrue( os.path.basename(working_directory) + ".mtdproj" in file_list) self.assertTrue("ws1.nxs" in file_list) def test_load_calls_loads_successfully(self): working_directory = tempfile.mkdtemp() return_value_for_load = os.path.join( working_directory, os.path.basename(working_directory) + ".mtdproj") self.project._save_file_dialog = mock.MagicMock( return_value=working_directory) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as() self.assertEqual(self.project._save_file_dialog.call_count, 1) ADS.clear() self.project._load_file_dialog = mock.MagicMock( return_value=return_value_for_load) self.project.load() self.assertEqual(self.project._load_file_dialog.call_count, 1) self.assertEqual(["ws1"], ADS.getObjectNames()) def test_offer_save_does_nothing_if_saved_is_true(self): self.assertEqual(self.project.offer_save(None), None) def test_offer_save_does_something_if_saved_is_false(self): self.project._offer_save_message_box = mock.MagicMock( return_value=QMessageBox.Yes) self.project.save = mock.MagicMock() # Add something to the ads so __saved is set to false CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(self.project.offer_save(None), False) self.assertEqual(self.project.save.call_count, 1) self.assertEqual(self.project._offer_save_message_box.call_count, 1) def test_adding_to_ads_calls_any_change_handle(self): self.project.anyChangeHandle = mock.MagicMock() CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_removing_from_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() ADS.remove("ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_grouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") self.project.anyChangeHandle = mock.MagicMock() GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") # Called twice because group is made and then added to the ADS self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_renaming_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() RenameWorkspace(InputWorkspace="ws1", OutputWorkspace="ws2") # Called twice because first workspace is removed and second is added self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_ungrouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") self.project.anyChangeHandle = mock.MagicMock() UnGroupWorkspace(InputWorkspace="NewGroup") # 1 for removing old group and 1 for something else but 2 seems right self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_group_updated_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") CreateSampleWorkspace(OutputWorkspace="ws3") self.project.anyChangeHandle = mock.MagicMock() ADS.addToGroup("NewGroup", "ws3") self.assertEqual(1, self.project.anyChangeHandle.call_count)
class ProjectTest(unittest.TestCase): _folders_to_remove = set() def setUp(self): self.fgfm = FakeGlobalFigureManager() self.fgfm.figs = [] self.project = Project(self.fgfm, fake_window_finding_function) def tearDown(self): ADS.clear() for folder in self._folders_to_remove: try: shutil.rmtree(folder) except OSError as exc: warnings.warn("Could not remove folder at \"{}\"\n" "Error message:\n{}".format(folder, exc)) self._folders_to_remove.clear() def test_save_calls_save_as_when_last_location_is_not_none(self): self.project.open_project_save_dialog = mock.MagicMock() self.project.save() self.assertEqual(self.project.open_project_save_dialog.call_count, 1) def test_save_does_not_call_save_as_when_last_location_is_not_none(self): self.project.open_project_save_dialog = mock.MagicMock() self.project.last_project_location = "1" self.assertEqual(self.project.open_project_save_dialog.call_count, 0) def test_save_saves_project_successfully(self): temp_file_path = tempfile.mkdtemp() self._folders_to_remove.add(temp_file_path) working_file = os.path.join(temp_file_path, "temp" + ".mtdproj") self.project.last_project_location = working_file self.project.remember_workspace_saving_option = True CreateSampleWorkspace(OutputWorkspace="ws1") self.project._offer_overwriting_gui = mock.MagicMock( return_value=QMessageBox.Yes) self.project.save() self.assertTrue(os.path.isfile(working_file)) file_list = os.listdir(os.path.dirname(working_file)) self.assertTrue(os.path.basename(working_file) in file_list) self.assertTrue("ws1.nxs" in file_list) self.assertEqual(self.project._offer_overwriting_gui.call_count, 1) def test_save_as_saves_project_successfully(self): temp_file_path = tempfile.mkdtemp() self._folders_to_remove.add(temp_file_path) working_file = os.path.join(temp_file_path, "temp" + ".mtdproj") working_directory = os.path.dirname(working_file) CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as(working_file) self.assertTrue(os.path.isfile(working_file)) self.assertTrue(os.path.isdir(working_directory)) file_list = os.listdir(working_directory) self.assertTrue(os.path.basename(working_file) in file_list) self.assertTrue("ws1.nxs" in file_list) def test_load_calls_loads_successfully(self): working_directory = tempfile.mkdtemp() self._folders_to_remove.add(working_directory) return_value_for_load = os.path.join( working_directory, os.path.basename(working_directory) + ".mtdproj") CreateSampleWorkspace(OutputWorkspace="ws1") self.project.save_as(return_value_for_load) ADS.clear() self.project._load_file_dialog = mock.MagicMock( return_value=return_value_for_load) self.project.load() self.assertEqual(self.project._load_file_dialog.call_count, 1) self.assertEqual(["ws1"], ADS.getObjectNames()) def test_offer_save_does_nothing_if_saved_is_true(self): self.assertEqual(self.project.offer_save(None), None) def test_offer_save_does_something_if_saved_is_false(self): self.project._offer_save_message_box = mock.MagicMock( return_value=QMessageBox.Yes) self.project.save = mock.MagicMock(return_value=None) # Add something to the ads so __saved is set to false CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(self.project.offer_save(None), False) self.assertEqual(self.project.save.call_count, 1) self.assertEqual(self.project._offer_save_message_box.call_count, 1) def test_offer_save_does_nothing_if_save_is_cancelled(self): self.project._offer_save_message_box = mock.MagicMock( return_value=QMessageBox.Yes) self.project.save = mock.MagicMock(return_value=True) # Add something to the ads so __saved is set to false CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(self.project.offer_save(None), True) def test_adding_to_ads_calls_any_change_handle(self): self.project.anyChangeHandle = mock.MagicMock() CreateSampleWorkspace(OutputWorkspace="ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_removing_from_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() ADS.remove("ws1") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_grouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") self.project.anyChangeHandle = mock.MagicMock() GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") # Called twice because group is made and then added to the ADS self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_renaming_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project.anyChangeHandle = mock.MagicMock() RenameWorkspace(InputWorkspace="ws1", OutputWorkspace="ws2") # Called twice because first workspace is removed and second is added self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_ungrouping_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") self.project.anyChangeHandle = mock.MagicMock() UnGroupWorkspace(InputWorkspace="NewGroup") # 1 for removing old group and 1 for something else but 2 seems right self.assertEqual(2, self.project.anyChangeHandle.call_count) def test_group_updated_in_ads_calls_any_change_handle(self): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="NewGroup") CreateSampleWorkspace(OutputWorkspace="ws3") self.project.anyChangeHandle = mock.MagicMock() ADS.addToGroup("NewGroup", "ws3") self.assertEqual(1, self.project.anyChangeHandle.call_count) def test_large_file_dialog_appears_for_large_file(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project._get_project_size = mock.MagicMock(return_value=int( ConfigService.getString("projectSaving.warningSize")) + 1) self.project._offer_large_size_confirmation = mock.MagicMock() self.project._save() self.assertEqual( self.project._offer_large_size_confirmation.call_count, 1) def test_large_file_dialog_does_not_appear_for_small_file(self): CreateSampleWorkspace(OutputWorkspace="ws1") self.project._get_project_size = mock.MagicMock(return_value=int( ConfigService.getString("projectSaving.warningSize")) - 1) self.project._offer_large_size_confirmation = mock.MagicMock() self.project._save() self.assertEqual( self.project._offer_large_size_confirmation.call_count, 0) def test_is_loading_is_False_after_error_thrown_during_load(self): with mock.patch.object(self.project, '_load_file_dialog', lambda: _raise(IOError)): try: self.project.load() except IOError: pass self.assertFalse(self.project.is_loading) def test_is_loading_is_False_after_None_returned_from_load_dialog(self): # None is returned from the load dialog when a user clicks Cancel with mock.patch.object(self.project, '_load_file_dialog', lambda: None): try: self.project.load() except IOError: pass self.assertFalse(self.project.is_loading) def test_is_saving_is_False_if_error_thrown_during_save(self): with mock.patch.object(self.project, '_get_project_size', lambda x: _raise(IOError)): try: self.project._save() except IOError: pass self.assertFalse(self.project.is_saving) @mock.patch('mantidqt.project.project.ProjectSaver.save_project') def test_workspace_groups_are_not_duplicated_when_saving(self, saver): CreateSampleWorkspace(OutputWorkspace="ws1") CreateSampleWorkspace(OutputWorkspace="ws2") GroupWorkspaces(InputWorkspaces="ws1,ws2", OutputWorkspace="newGroup") CreateSampleWorkspace(OutputWorkspace="ws3") self.project.plot_gfm.figs = "mocked_figs" self.project.interface_populating_function = mock.MagicMock( return_value="mocked_interfaces") self.project._save() saver.assert_called_with(file_name=self.project.last_project_location, workspace_to_save=['newGroup', 'ws3'], plots_to_save="mocked_figs", interfaces_to_save="mocked_interfaces") @staticmethod def create_altered_and_unaltered_mock_workspaces(): # Create a mock unaltered workspace so it's history only contains Load. unaltered_workspace = mock.Mock() unaltered_workspace_history = mock.Mock() unaltered_workspace.getHistory.return_value = unaltered_workspace_history unaltered_workspace_history.size.return_value = 1 unaltered_workspace_history.getAlgorithm(0).name.return_value = "Load" # Create a mock altered workspaces with history length > 1. altered_workspace = mock.Mock() altered_workspace.name.return_value = "altered_workspace" altered_workspace_history = mock.Mock() altered_workspace.getHistory.return_value = altered_workspace_history altered_workspace_history.size.return_value = 2 return [altered_workspace, unaltered_workspace] @mock.patch('mantidqt.project.project.AnalysisDataService') def test_filter_unaltered_workspaces_function_removes_workspaces_that_have_only_been_loaded( self, mock_ads): workspaces = self.create_altered_and_unaltered_mock_workspaces() # When retrieveWorkspaces is called just return what is passed in. mock_ads.retrieveWorkspaces = lambda x: x altered_workspaces = self.project._filter_unaltered_workspaces( workspaces) self.assertEqual(len(altered_workspaces), 1) self.assertEqual(altered_workspaces[0], "altered_workspace") @mock.patch('mantidqt.project.project.AnalysisDataService') def test_filter_plots_removes_plots_that_use_unaltered_workspaces( self, mock_ads): workspaces = self.create_altered_and_unaltered_mock_workspaces() # When retrieveWorkspaces is called just return what is passed in. mock_ads.retrieveWorkspaces = lambda x: x # Create a plot for each workspace figure_managers = {} for i, ws in enumerate(workspaces): fig_manager = mock.Mock() mock_ax = mock.Mock(spec=MantidAxes) mock_ax.tracked_workspaces = [ws] fig_manager.canvas.figure.axes = [mock_ax] figure_managers[i] = fig_manager filtered_figure_managers = self.project._filter_plots_with_unaltered_workspaces( plots=figure_managers, workspaces=[workspaces[0]]) self.assertEqual(len(filtered_figure_managers), 1) def test_saving_project_with_save_altered_workspaces_only_calls_filter_functions( self): self.project.save_altered_workspaces_only = True self.project._filter_plots_with_unaltered_workspaces = mock.Mock() self.project._filter_unaltered_workspaces = mock.Mock(return_value=[]) self.project._save() self.project._filter_plots_with_unaltered_workspaces.assert_called_once( ) self.project._filter_unaltered_workspaces.assert_called_once()
class MainWindow(QMainWindow): DOCKOPTIONS = QMainWindow.AllowTabbedDocks | QMainWindow.AllowNestedDocks def __init__(self): QMainWindow.__init__(self) # -- instance attributes -- self.setWindowTitle("Mantid Workbench") self.setObjectName("Mantid Workbench") # widgets self.messagedisplay = None self.ipythonconsole = None self.workspacewidget = None self.editor = None self.algorithm_selector = None self.plot_selector = None self.widgets = [] # Widget layout map: required for use in Qt.connection self._layout_widget_info = None # Menus self.file_menu = None self.file_menu_actions = None self.editor_menu = None self.view_menu = None self.view_menu_actions = None self.interfaces_menu = None # Allow splash screen text to be overridden in set_splash self.splash = SPLASH # Layout self.setDockOptions(self.DOCKOPTIONS) # Project self.project = None def setup(self): # menus must be done first so they can be filled by the # plugins in register_plugin self.create_menus() # widgets # Log message display must be imported first self.set_splash("Loading message display") from workbench.plugins.logmessagedisplay import LogMessageDisplay self.messagedisplay = LogMessageDisplay(self) # this takes over stdout/stderr self.messagedisplay.register_plugin() self.widgets.append(self.messagedisplay) self.set_splash("Loading Algorithm Selector") from workbench.plugins.algorithmselectorwidget import AlgorithmSelector self.algorithm_selector = AlgorithmSelector(self) self.algorithm_selector.register_plugin() self.widgets.append(self.algorithm_selector) self.set_splash("Loading Plot Selector") from workbench.plugins.plotselectorwidget import PlotSelector self.plot_selector = PlotSelector(self) self.plot_selector.register_plugin() self.widgets.append(self.plot_selector) self.set_splash("Loading code editing widget") from workbench.plugins.editor import MultiFileEditor self.editor = MultiFileEditor(self) self.editor.register_plugin() self.widgets.append(self.editor) self.set_splash("Loading IPython console") from workbench.plugins.jupyterconsole import JupyterConsole self.ipythonconsole = JupyterConsole(self) self.ipythonconsole.register_plugin() self.widgets.append(self.ipythonconsole) from workbench.plugins.workspacewidget import WorkspaceWidget self.workspacewidget = WorkspaceWidget(self) self.workspacewidget.register_plugin() self.widgets.append(self.workspacewidget) # Set up the project object self.project = Project() # uses default configuration as necessary self.readSettings(CONF) self.setup_layout() self.create_actions() self.populate_menus() def post_mantid_init(self): """Run any setup that requires mantid to have been initialized """ self.populate_interfaces_menu() self.algorithm_selector.refresh() def set_splash(self, msg=None): if not self.splash: return if msg: self.splash.showMessage( msg, Qt.AlignBottom | Qt.AlignLeft | Qt.AlignAbsolute, QColor(Qt.black)) QApplication.processEvents(QEventLoop.AllEvents) def create_menus(self): self.file_menu = self.menuBar().addMenu("&File") self.editor_menu = self.menuBar().addMenu("&Editor") self.view_menu = self.menuBar().addMenu("&View") self.interfaces_menu = self.menuBar().addMenu('&Interfaces') def create_actions(self): # --- general application menu options -- # file menu action_open = create_action(self, "Open Script", on_triggered=self.open_file, shortcut="Ctrl+O", shortcut_context=Qt.ApplicationShortcut) action_load_project = create_action(self, "Open Project", on_triggered=self.load_project) action_save_script = create_action( self, "Save Script", on_triggered=self.save_script, shortcut="Ctrl+S", shortcut_context=Qt.ApplicationShortcut) action_save_project = create_action(self, "Save Project", on_triggered=self.save_project) action_save_project_as = create_action( self, "Save Project as...", on_triggered=self.save_project_as) action_manage_directories = create_action( self, "Manage User Directories", on_triggered=self.open_manage_directories) action_quit = create_action(self, "&Quit", on_triggered=self.close, shortcut="Ctrl+Q", shortcut_context=Qt.ApplicationShortcut) self.file_menu_actions = [ action_open, action_load_project, None, action_save_script, action_save_project, action_save_project_as, None, action_manage_directories, None, action_quit ] # view menu action_restore_default = create_action( self, "Restore Default Layout", on_triggered=self.prep_window_for_reset, shortcut="Shift+F10", shortcut_context=Qt.ApplicationShortcut) self.view_menu_actions = [action_restore_default, None ] + self.create_widget_actions() def create_widget_actions(self): """ Creates menu actions to show/hide dockable widgets. This uses all widgets that are in self.widgets :return: A list of show/hide actions for all widgets """ widget_actions = [] for widget in self.widgets: action = widget.dockwidget.toggleViewAction() widget_actions.append(action) return widget_actions def populate_menus(self): # Link to menus add_actions(self.file_menu, self.file_menu_actions) add_actions(self.view_menu, self.view_menu_actions) def launch_custom_gui(self, filename): executioner = PythonCodeExecution() executioner.sig_exec_error.connect( lambda errobj: logger.warning(str(errobj))) executioner.execute(open(filename).read(), filename) def populate_interfaces_menu(self): interface_dir = ConfigService['mantidqt.python_interfaces_directory'] items = ConfigService['mantidqt.python_interfaces'].split() # list of custom interfaces that are not qt4/qt5 compatible GUI_BLACKLIST = [ 'ISIS_Reflectometry_Old.py', 'ISIS_SANS_v2_experimental.py', 'Frequency_Domain_Analysis.py', 'Elemental_Analysis.py' ] # detect the python interfaces interfaces = {} for item in items: key, scriptname = item.split('/') if not os.path.exists(os.path.join(interface_dir, scriptname)): logger.warning('Failed to find script "{}" in "{}"'.format( scriptname, interface_dir)) continue if scriptname in GUI_BLACKLIST: logger.information('Not adding gui "{}"'.format(scriptname)) continue temp = interfaces.get(key, []) temp.append(scriptname) interfaces[key] = temp # add the interfaces to the menu keys = list(interfaces.keys()) keys.sort() for key in keys: submenu = self.interfaces_menu.addMenu(key) names = interfaces[key] names.sort() for name in names: action = submenu.addAction( name.replace('.py', '').replace('_', ' ')) script = os.path.join(interface_dir, name) action.triggered.connect(lambda checked, script=script: self. launch_custom_gui(script)) def add_dockwidget(self, plugin): """Create a dockwidget around a plugin and add the dock to window""" dockwidget, location = plugin.create_dockwidget() self.addDockWidget(location, dockwidget) # ----------------------- Layout --------------------------------- def setup_layout(self): """Assume this is a first run of the application and set layouts accordingly""" self.setup_default_layouts() def prep_window_for_reset(self): """Function to reset all dock widgets to a state where they can be ordered by setup_default_layout""" for widget in self.widgets: widget.dockwidget.setFloating( False) # Bring back any floating windows self.addDockWidget(Qt.LeftDockWidgetArea, widget.dockwidget) # Un-tabify all widgets self.setup_default_layouts() def setup_default_layouts(self): """Set or reset the layouts of the child widgets""" # layout definition logmessages = self.messagedisplay ipython = self.ipythonconsole workspacewidget = self.workspacewidget editor = self.editor algorithm_selector = self.algorithm_selector plot_selector = self.plot_selector default_layout = { 'widgets': [ # column 0 [[workspacewidget], [algorithm_selector, plot_selector]], # column 1 [[editor, ipython]], # column 2 [[logmessages]] ], 'width-fraction': [ 0.25, # column 0 width 0.50, # column 1 width 0.25 ], # column 2 width 'height-fraction': [ [0.5, 0.5], # column 0 row heights [1.0], # column 1 row heights [1.0] ] # column 2 row heights } with widget_updates_disabled(self): widgets_layout = default_layout['widgets'] # flatten list widgets = [ item for column in widgets_layout for row in column for item in row ] # show everything for w in widgets: w.toggle_view(True) # split everything on the horizontal for i in range(len(widgets) - 1): first, second = widgets[i], widgets[i + 1] self.splitDockWidget(first.dockwidget, second.dockwidget, Qt.Horizontal) # now arrange the rows for column in widgets_layout: for i in range(len(column) - 1): first_row, second_row = column[i], column[i + 1] self.splitDockWidget(first_row[0].dockwidget, second_row[0].dockwidget, Qt.Vertical) # and finally tabify those in the same position for column in widgets_layout: for row in column: for i in range(len(row) - 1): first, second = row[i], row[i + 1] self.tabifyDockWidget(first.dockwidget, second.dockwidget) # Raise front widget per row row[0].dockwidget.show() row[0].dockwidget.raise_() # ----------------------- Events --------------------------------- def closeEvent(self, event): # Check whether or not to save project if not self.project.saved: # Offer save if self.project.offer_save(self): # Cancel has been clicked event.ignore() return # Close editors if self.editor.app_closing(): self.writeSettings( CONF ) # write current window information to global settings object # Close all open plots # We don't want this at module scope here import matplotlib.pyplot as plt # noqa plt.close('all') app = QApplication.instance() if app is not None: app.closeAllWindows() event.accept() else: # Cancel was pressed when closing an editor event.ignore() # ----------------------- Slots --------------------------------- def open_file(self): # todo: when more file types are added this should # live in its own type filepath, _ = QFileDialog.getOpenFileName(self, "Open File...", "", "Python (*.py)") if not filepath: return self.editor.open_file_in_new_tab(filepath) def save_script(self): self.editor.save_current_file() def save_project(self): self.project.save() def save_project_as(self): self.project.save_as() def load_project(self): self.project.load() def open_manage_directories(self): ManageUserDirectories(self).exec_() def readSettings(self, settings): qapp = QApplication.instance() qapp.setAttribute(Qt.AA_UseHighDpiPixmaps) if hasattr(Qt, 'AA_EnableHighDpiScaling'): qapp.setAttribute(Qt.AA_EnableHighDpiScaling, settings.get('high_dpi_scaling')) # get the saved window geometry window_size = settings.get('MainWindow/size') if not isinstance(window_size, QSize): window_size = QSize(*window_size) window_pos = settings.get('MainWindow/position') if not isinstance(window_pos, QPoint): window_pos = QPoint(*window_pos) # make sure main window is smaller than the desktop desktop = QDesktopWidget() # this gives the maximum screen number if the position is off screen screen = desktop.screenNumber(window_pos) # recalculate the window size desktop_geom = desktop.screenGeometry(screen) w = min(desktop_geom.size().width(), window_size.width()) h = min(desktop_geom.size().height(), window_size.height()) window_size = QSize(w, h) # and position it on the supplied desktop screen x = max(window_pos.x(), desktop_geom.left()) y = max(window_pos.y(), desktop_geom.top()) window_pos = QPoint(x, y) # set the geometry self.resize(window_size) self.move(window_pos) # restore window state if settings.has('MainWindow/state'): self.restoreState(settings.get('MainWindow/state')) else: self.setWindowState(Qt.WindowMaximized) # read in settings for children AlgorithmInputHistory().readSettings(settings) for widget in self.widgets: if hasattr(widget, 'readSettings'): widget.readSettings(settings) def writeSettings(self, settings): settings.set('MainWindow/size', self.size()) # QSize settings.set('MainWindow/position', self.pos()) # QPoint settings.set('MainWindow/state', self.saveState()) # QByteArray # write out settings for children AlgorithmInputHistory().writeSettings(settings) for widget in self.widgets: if hasattr(widget, 'writeSettings'): widget.writeSettings(settings)