# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ Layout container. """ # Standard library imports from collections import OrderedDict import sys # Third party imports from qtpy.QtCore import Qt, Slot from qtpy.QtWidgets import QMessageBox # Local imports from spyder.api.exceptions import SpyderAPIError from spyder.api.translations import get_translation from spyder.api.widgets.main_container import PluginMainContainer from spyder.plugins.layout.api import BaseGridLayoutType from spyder.plugins.layout.layouts import DefaultLayouts from spyder.plugins.layout.widgets.dialog import ( LayoutSaveDialog, LayoutSettingsDialog) # Localization _ = get_translation("spyder") class LayoutContainerActions: DefaultLayout = 'default_layout_action' MatlabLayout = 'matlab_layout_action' RStudio = 'rstudio_layout_action' HorizontalSplit = 'horizontal_split_layout_action' VerticalSplit = 'vertical_split_layout_action' SaveLayoutAction = 'save_layout_action' ShowLayoutPreferencesAction = 'show_layout_preferences_action' ResetLayout = 'reset_layout_action' # Needs to have 'Maximize pane' as name to properly register # the action shortcut MaximizeCurrentDockwidget = 'Maximize pane' # Needs to have 'Fullscreen mode' as name to properly register # the action shortcut Fullscreen = 'Fullscreen mode' # Needs to have 'Use next layout' as name to properly register # the action shortcut NextLayout = 'Use next layout' # Needs to have 'Use previous layout' as name to properly register # the action shortcut PreviousLayout = 'Use previous layout' # Needs to have 'Close pane' as name to properly register # the action shortcut CloseCurrentDockwidget = 'Close pane' # Needs to have 'Lock unlock panes' as name to properly register # the action shortcut LockDockwidgetsAndToolbars = 'Lock unlock panes' class LayoutContainer(PluginMainContainer): """ Plugin container class that handles the Spyder quick layouts functionality. """ def setup(self): # Basic attributes to handle layouts options and dialogs references self._spyder_layouts = OrderedDict() self._save_dialog = None self._settings_dialog = None self._layouts_menu = None self._current_quick_layout = None # Close current dockable plugin self._close_dockwidget_action = self.create_action( LayoutContainerActions.CloseCurrentDockwidget, text=_('Close current pane'), icon=self.create_icon('close_pane'), triggered=self._plugin.close_current_dockwidget, context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context='_' ) # Maximize current dockable plugin self._maximize_dockwidget_action = self.create_action( LayoutContainerActions.MaximizeCurrentDockwidget, text=_('Maximize current pane'), icon=self.create_icon('maximize'), toggled=lambda state: self._plugin.maximize_dockwidget(), context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context='_') # Fullscreen mode self._fullscreen_action = self.create_action( LayoutContainerActions.Fullscreen, text=_('Fullscreen mode'), triggered=self._plugin.toggle_fullscreen, context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context='_') if sys.platform == 'darwin': self._fullscreen_action.setEnabled(False) self._fullscreen_action.setToolTip(_("For fullscreen mode use the " "macOS built-in feature")) # Lock dockwidgets and toolbars self._lock_interface_action = self.create_action( LayoutContainerActions.LockDockwidgetsAndToolbars, text='', triggered=lambda checked: self._plugin.toggle_lock(), context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context='_' ) self._save_layout_action = self.create_action( LayoutContainerActions.SaveLayoutAction, _("Save current layout"), triggered=lambda: self.show_save_layout(), context=Qt.ApplicationShortcut, register_shortcut=False, ) self._show_preferences_action = self.create_action( LayoutContainerActions.ShowLayoutPreferencesAction, text=_("Layout preferences"), triggered=lambda: self.show_layout_settings(), context=Qt.ApplicationShortcut, register_shortcut=False, ) self._reset_action = self.create_action( LayoutContainerActions.ResetLayout, text=_('Reset to Spyder default'), triggered=self.reset_window_layout, register_shortcut=False, ) # Layouts shortcuts actions self._toggle_next_layout_action = self.create_action( LayoutContainerActions.NextLayout, _("Use next layout"), triggered=self.toggle_next_layout, context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context='_') self._toggle_previous_layout_action = self.create_action( LayoutContainerActions.PreviousLayout, _("Use previous layout"), triggered=self.toggle_previous_layout, context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context='_') # Layouts menu self._layouts_menu = self.create_menu( "layouts_menu", _("Window layouts")) self._plugins_menu = self.create_menu( "plugins_menu", _("Panes")) self._plugins_menu.setObjectName('checkbox-padding') def update_actions(self): pass def update_layout_menu_actions(self): """ Update layouts menu and layouts related actions. """ menu = self._layouts_menu menu.clear_actions() names = self.get_conf('names') ui_names = self.get_conf('ui_names') order = self.get_conf('order') active = self.get_conf('active') actions = [] for name in order: if name in active: if name in self._spyder_layouts: index = name name = self._spyder_layouts[index].get_name() else: index = names.index(name) name = ui_names[index] # closure required so lambda works with the default parameter def trigger(i=index, self=self): return lambda: self.quick_layout_switch(i) layout_switch_action = self.create_action( name, text=name, triggered=trigger(), register_shortcut=False, overwrite=True ) actions.append(layout_switch_action) for item in actions: self.add_item_to_menu(item, menu, section="layouts_section") for item in [self._save_layout_action, self._show_preferences_action, self._reset_action]: self.add_item_to_menu(item, menu, section="layouts_section_2") self._show_preferences_action.setEnabled(len(order) != 0) # --- Public API # ------------------------------------------------------------------------ def critical_message(self, title, message): """Expose a QMessageBox.critical dialog to be used from the plugin.""" QMessageBox.critical(self, title, message) def register_layout(self, parent_plugin, layout_type): """ Register a new layout type. Parameters ---------- parent_plugin: spyder.api.plugins.SpyderPluginV2 Plugin registering the layout type. layout_type: spyder.plugins.layout.api.BaseGridLayoutType Layout to register. """ if not issubclass(layout_type, BaseGridLayoutType): raise SpyderAPIError( "A layout must be a subclass is `BaseGridLayoutType`!") layout_id = layout_type.ID if layout_id in self._spyder_layouts: raise SpyderAPIError( "Layout with id `{}` already registered!".format(layout_id)) layout = layout_type(parent_plugin) layout._check_layout_validity() self._spyder_layouts[layout_id] = layout names = self.get_conf('names') ui_names = self.get_conf('ui_names') order = self.get_conf('order') active = self.get_conf('active') if layout_id not in names: names.append(layout_id) ui_names.append(layout.get_name()) order.append(layout_id) active.append(layout_id) self.set_conf('names', names) self.set_conf('ui_names', ui_names) self.set_conf('order', order) self.set_conf('active', active) def get_layout(self, layout_id): """ Get a registered layout by its ID. Parameters ---------- layout_id : string The ID of the layout. Raises ------ SpyderAPIError If the given id is not found in the registered layouts. Returns ------- Instance of a spyder.plugins.layout.api.BaseGridLayoutType subclass Layout. """ if layout_id not in self._spyder_layouts: raise SpyderAPIError( "Layout with id `{}` is not registered!".format(layout_id)) return self._spyder_layouts[layout_id] def show_save_layout(self): """Show the save layout dialog.""" names = self.get_conf('names') ui_names = self.get_conf('ui_names') order = self.get_conf('order') active = self.get_conf('active') dialog_names = [name for name in names if name not in self._spyder_layouts.keys()] dlg = self._save_dialog = LayoutSaveDialog(self, dialog_names) if dlg.exec_(): name = dlg.combo_box.currentText() if name in self._spyder_layouts: QMessageBox.critical( self, _("Error"), _("Layout {0} was defined programatically. " "It is not possible to overwrite programatically " "registered layouts.").format(name) ) return if name in names: answer = QMessageBox.warning( self, _("Warning"), _("Layout {0} will be overwritten. " "Do you want to continue?").format(name), QMessageBox.Yes | QMessageBox.No, ) index = order.index(name) else: answer = True if None in names: index = names.index(None) names[index] = name else: index = len(names) names.append(name) order.append(name) # Always make active a new layout even if it overwrites an # inactive layout if name not in active: active.append(name) if name not in ui_names: ui_names.append(name) if answer: self._plugin.save_current_window_settings( 'layout_{}/'.format(index), section='quick_layouts') self.set_conf('names', names) self.set_conf('ui_names', ui_names) self.set_conf('order', order) self.set_conf('active', active) self.update_layout_menu_actions() def show_layout_settings(self): """Layout settings dialog.""" names = self.get_conf('names') ui_names = self.get_conf('ui_names') order = self.get_conf('order') active = self.get_conf('active') read_only = list(self._spyder_layouts.keys()) dlg = self._settings_dialog = LayoutSettingsDialog( self, names, ui_names, order, active, read_only) if dlg.exec_(): self.set_conf('names', dlg.names) self.set_conf('ui_names', dlg.ui_names) self.set_conf('order', dlg.order) self.set_conf('active', dlg.active) self.update_layout_menu_actions() @Slot() def reset_window_layout(self): """Reset window layout to default.""" answer = QMessageBox.warning( self, _("Warning"), _("Window layout will be reset to default settings: " "this affects window position, size and dockwidgets.\n" "Do you want to continue?"), QMessageBox.Yes | QMessageBox.No, ) if answer == QMessageBox.Yes: self._plugin.setup_layout(default=True) @Slot() def toggle_previous_layout(self): """Use the previous layout from the layouts list (default + custom).""" self.toggle_layout('previous') @Slot() def toggle_next_layout(self): """Use the next layout from the layouts list (default + custom).""" self.toggle_layout('next') def toggle_layout(self, direction='next'): """Change current layout.""" names = self.get_conf('names') order = self.get_conf('order') active = self.get_conf('active') if len(active) == 0: return layout_index = [] for name in order: if name in active: layout_index.append(names.index(name)) current_layout = self._current_quick_layout dic = {'next': 1, 'previous': -1} if current_layout is None: # Start from default current_layout = names.index(DefaultLayouts.SpyderLayout) if current_layout in layout_index: current_index = layout_index.index(current_layout) else: current_index = 0 new_index = (current_index + dic[direction]) % len(layout_index) index_or_layout_id = layout_index[new_index] is_layout_id = ( names[index_or_layout_id] in self._spyder_layouts) if is_layout_id: index_or_layout_id = names[layout_index[new_index]] self.quick_layout_switch(index_or_layout_id) def quick_layout_switch(self, index_or_layout_id): """ Switch to quick layout number *index* or *layout id*. Parameters ---------- index: int or str """ possible_current_layout = self._plugin.quick_layout_switch( index_or_layout_id) if possible_current_layout is not None: if isinstance(possible_current_layout, int): self._current_quick_layout = possible_current_layout else: names = self.get_conf('names') self._current_quick_layout = names.index( possible_current_layout)