diff --git a/spyder/api/plugins/new_api.py b/spyder/api/plugins/new_api.py index d3c36580a74..fcdd1ed241a 100644 --- a/spyder/api/plugins/new_api.py +++ b/spyder/api/plugins/new_api.py @@ -1072,7 +1072,7 @@ def after_long_process(self, message=""): super().after_long_process(message) self.get_widget().stop_spinner() - def get_widget(self): + def get_widget(self) -> PluginMainWidget: """ Return the plugin main widget. """ @@ -1128,7 +1128,7 @@ def create_window(self): self.get_widget().create_window() def close_window(self, save_undocked=False): - self.get_widget().close_window(save_undocked=save_undocked) + self.get_widget()._close_window(save_undocked=save_undocked) def change_visibility(self, state, force_focus=False): self.get_widget().change_visibility(state, force_focus) diff --git a/spyder/api/widgets/main_widget.py b/spyder/api/widgets/main_widget.py index dc698ee8825..ba49ffde503 100644 --- a/spyder/api/widgets/main_widget.py +++ b/spyder/api/widgets/main_widget.py @@ -354,11 +354,11 @@ def _setup(self): text=_("Dock"), tip=_("Dock the pane"), icon=self.create_icon('dock'), - triggered=self.close_window, + triggered=self.dock_window, ) self.lock_unlock_action = self.create_action( name=PluginMainWidgetActions.LockUnlockPosition, - text=_("Unlock position"), + text=_("Move"), tip=_("Unlock to move pane to another position"), icon=self.create_icon('drag_dock_widget'), triggered=self.lock_unlock_position, @@ -387,7 +387,7 @@ def _setup(self): ) for item in [self.lock_unlock_action, self.undock_action, - self.close_action, self.dock_action]: + self.dock_action, self.close_action]: self.add_item_to_menu( item, self._options_menu, @@ -416,7 +416,6 @@ def _update_actions(self): """ show_dock_actions = self.windowwidget is None self.undock_action.setVisible(show_dock_actions) - self.close_action.setVisible(show_dock_actions) self.lock_unlock_action.setVisible(show_dock_actions) self.dock_action.setVisible(not show_dock_actions) @@ -442,13 +441,13 @@ def _on_title_bar_shown(self, visible): Actions to perform when the title bar is shown/hidden. """ if visible: - self.lock_unlock_action.setText(_('Lock position')) + self.lock_unlock_action.setText(_('Lock')) self.lock_unlock_action.setIcon(self.create_icon('lock_open')) for method_name in ['setToolTip', 'setStatusTip']: method = getattr(self.lock_unlock_action, method_name) method(_("Lock pane to the current position")) else: - self.lock_unlock_action.setText(_('Unlock position')) + self.lock_unlock_action.setText(_('Move')) self.lock_unlock_action.setIcon( self.create_icon('drag_dock_widget')) for method_name in ['setToolTip', 'setStatusTip']: @@ -703,21 +702,21 @@ def render_toolbars(self): This action can only be performed once. """ # if not self._toolbars_already_rendered: - self._main_toolbar._render() - self._corner_toolbar._render() + self._main_toolbar.render() + self._corner_toolbar.render() for __, toolbar in self._auxiliary_toolbars.items(): - toolbar._render() + toolbar.render() # self._toolbars_already_rendered = True - # ---- SpyderDockwidget handling + # ---- SpyderWindowWidget handling # ------------------------------------------------------------------------- @Slot() def create_window(self): """ - Create a QMainWindow instance containing this widget. + Create an undocked window containing this widget. """ - logger.debug("Undocking plugin") + logger.debug(f"Undocking plugin {self._name}") # Widgets self.windowwidget = window = SpyderWindowWidget(self) @@ -756,21 +755,63 @@ def create_window(self): window.show() @Slot() - def close_window(self, save_undocked=False): + def dock_window(self): + """Dock undocked window back to the main window.""" + logger.debug(f"Docking window of plugin {self._name}") + + # Reset undocked state + self.set_conf('window_was_undocked_before_hiding', False) + + # This avoids trying to close the window twice: once when calling + # _close_window below and the other when Qt calls the closeEvent of + # windowwidget + self.windowwidget.blockSignals(True) + + # Close window + self._close_window(switch_to_plugin=True) + + # Make plugin visible on main window + self.dockwidget.setVisible(True) + self.dockwidget.raise_() + + @Slot() + def close_window(self): """ - Close QMainWindow instance that contains this widget. + Close undocked window when clicking on the close window button. + + Notes + ----- + * This can either dock or hide the window, depending on whether the + user hid the window before. + * The default behavior is to dock the window, so that new users can + experiment with the dock/undock functionality without surprises. + * If the user closes the window by clicking on the `Close` action in + the plugin's Options menu or by going to the `View > Panes` menu, + then we will hide it when they click on the close button again. + That gives users the ability to show/hide plugins without + docking/undocking them first. + """ + if self.get_conf('window_was_undocked_before_hiding', default=False): + self.close_dock() + else: + self.dock_window() + + def _close_window(self, save_undocked=False, switch_to_plugin=True): + """ + Helper function to close the undocked window with different parameters. Parameters ---------- save_undocked : bool, optional True if the undocked state needs to be saved. The default is False. + switch_to_plugin : bool, optional + Whether to switch to the plugin after closing the window. The + default is True. Returns ------- None. """ - logger.debug("Docking plugin back to the main window") - if self.windowwidget is not None: # Save window geometry to restore it when undocking the plugin # again. @@ -793,15 +834,20 @@ def close_window(self, save_undocked=False): if self.dockwidget is not None: self.sig_update_ancestor_requested.emit() - self.get_plugin().switch_to_plugin() + if switch_to_plugin: + # This is necessary to restore the main window layout when + # there's a maximized plugin on it when the user requests + # to dock back this plugin. + self.get_plugin().switch_to_plugin() + self.dockwidget.setWidget(self) - self.dockwidget.setVisible(True) - self.dockwidget.raise_() self._update_actions() else: # Reset undocked state self.set_conf('undocked_on_window_close', False) + # ---- SpyderDockwidget handling + # ------------------------------------------------------------------------- def change_visibility(self, enable, force_focus=None): """Dock widget visibility has changed.""" if self.dockwidget is None: @@ -853,15 +899,40 @@ def toggle_view(self, checked): if not self.dockwidget: return - # Dock plugin if it's undocked before hiding it. - if self.windowwidget is not None: - self.close_window(save_undocked=True) + # To check if the plugin needs to be undocked at the end + undock = False if checked: self.dockwidget.show() self.dockwidget.raise_() self.is_visible = True + + # We need to undock the plugin if that was its state before + # toggling its visibility. + if ( + # Don't run this while the window is being created to not + # affect setting up the layout at startup. + not self._plugin.main.is_setting_up + and self.get_conf( + 'window_was_undocked_before_hiding', default=False + ) + ): + undock = True else: + if self.windowwidget is not None: + logger.debug(f"Closing window of plugin {self._name}") + + # This avoids trying to close the window twice: once when + # calling _close_window below and the other when Qt calls the + # closeEvent of windowwidget + self.windowwidget.blockSignals(True) + + # Dock plugin if it's undocked before hiding it. + self._close_window(switch_to_plugin=False) + + # Save undocked state to restore it afterwards. + self.set_conf('window_was_undocked_before_hiding', True) + self.dockwidget.hide() self.is_visible = False @@ -873,6 +944,15 @@ def toggle_view(self, checked): self.sig_toggle_view_changed.emit(checked) + logger.debug( + f"Plugin {self._name} is now {'visible' if checked else 'hidden'}" + ) + + if undock: + # We undock the plugin at this point so that the View menu is + # updated correctly. + self.create_window() + def create_dockwidget(self, mainwindow): """ Add to parent QMainWindow as a dock widget. @@ -897,7 +977,8 @@ def close_dock(self): """ Close the dockwidget. """ - self.toggle_view(False) + logger.debug(f"Hiding plugin {self._name}") + self.toggle_view_action.setChecked(False) def lock_unlock_position(self): """ @@ -926,7 +1007,7 @@ def set_maximized_state(self, state): # This is necessary for old API plugins interacting with new ones. self._plugin._ismaximized = state - # --- API: methods to define or override + # ---- API: methods to define or override # ------------------------------------------------------------------------ def get_title(self): """ diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index fcec08ba939..4c8eb2844e1 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -268,10 +268,12 @@ def add_item_to_menu(self, action_or_menu, menu, section=None, def _create_menu( self, menu_id: str, + parent: Optional[QWidget] = None, title: Optional[str] = None, icon: Optional[QIcon] = None, reposition: Optional[bool] = True, register: bool = True, + min_width: Optional[int] = None, MenuClass=SpyderMenu ) -> SpyderMenu: """ @@ -279,9 +281,9 @@ def _create_menu( Notes ----- - * This method should only be used directly to generate a menu that is a + * This method must only be used directly to generate a menu that is a subclass of SpyderMenu. - * Refer to the documentation for `create_menu` to learn about its args. + * Refer to the documentation for `SpyderMenu` to learn about its args. """ if register: menus = getattr(self, '_menus', None) @@ -294,9 +296,10 @@ def _create_menu( ) menu = MenuClass( - parent=self, + parent=self if parent is None else parent, menu_id=menu_id, title=title, + min_width=min_width, reposition=reposition ) diff --git a/spyder/api/widgets/toolbars.py b/spyder/api/widgets/toolbars.py index 97c32bcc5d5..d018e6a82a3 100644 --- a/spyder/api/widgets/toolbars.py +++ b/spyder/api/widgets/toolbars.py @@ -12,7 +12,7 @@ from collections import OrderedDict import os import sys -from typing import Union, Optional, Tuple, List, Dict +from typing import Dict, List, Optional, Tuple, Union import uuid # Third part imports @@ -106,11 +106,14 @@ class SpyderToolbar(QToolBar): def __init__(self, parent, title): super().__init__(parent=parent) - self._section_items = OrderedDict() - self._item_map = {} # type: Dict[str, ToolbarItem] - self._pending_items = {} # type: Dict[str, List[ToolbarItemEntry]] + + # Attributes self._title = title + self._section_items = OrderedDict() + self._item_map: Dict[str, ToolbarItem] = {} + self._pending_items: Dict[str, List[ToolbarItemEntry]] = {} self._default_section = "default_section" + self._filter = None self.setWindowTitle(title) @@ -120,18 +123,25 @@ def __init__(self, parent, title): ext_button.setIcon(ima.icon('toolbar_ext_button')) ext_button.setToolTip(_("More")) - # Set style for extension button menu. - ext_button.menu().setStyleSheet( - SpyderMenu._generate_stylesheet().toString() - ) - - ext_button_menu_style = SpyderMenuProxyStyle(None) - ext_button_menu_style.setParent(self) - ext_button.menu().setStyle(ext_button_menu_style) - - def add_item(self, action_or_widget: ToolbarItem, - section: Optional[str] = None, before: Optional[str] = None, - before_section: Optional[str] = None, omit_id: bool = False): + # Set style for extension button menu (not all extension buttons have + # it). + if ext_button.menu(): + ext_button.menu().setStyleSheet( + SpyderMenu._generate_stylesheet().toString() + ) + + ext_button_menu_style = SpyderMenuProxyStyle(None) + ext_button_menu_style.setParent(self) + ext_button.menu().setStyle(ext_button_menu_style) + + def add_item( + self, + action_or_widget: ToolbarItem, + section: Optional[str] = None, + before: Optional[str] = None, + before_section: Optional[str] = None, + omit_id: bool = False + ): """ Add action or widget item to given toolbar `section`. @@ -154,14 +164,17 @@ def add_item(self, action_or_widget: ToolbarItem, Spyder 4 plugins. Default: False """ item_id = None - if (isinstance(action_or_widget, SpyderAction) or - hasattr(action_or_widget, 'action_id')): + if ( + isinstance(action_or_widget, SpyderAction) + or hasattr(action_or_widget, 'action_id') + ): item_id = action_or_widget.action_id elif hasattr(action_or_widget, 'ID'): item_id = action_or_widget.ID if not omit_id and item_id is None and action_or_widget is not None: raise SpyderAPIError( - f'Item {action_or_widget} must declare an ID attribute.') + f'Item {action_or_widget} must declare an ID attribute.' + ) if before is not None: if before not in self._item_map: @@ -227,16 +240,12 @@ def remove_item(self, item_id: str): if len(section_items) == 0: self._section_items.pop(section) self.clear() - self._render() + self.render() except KeyError: pass - def _render(self): - """ - Create the toolbar taking into account sections and locations. - - This method is called once on widget setup. - """ + def render(self): + """Create the toolbar taking into account sections and locations.""" sec_items = [] for sec, items in self._section_items.items(): for item in items: @@ -258,9 +267,12 @@ def _render(self): add_method(item) if isinstance(item, QAction): - text_beside_icon = getattr(item, 'text_beside_icon', False) widget = self.widgetForAction(item) + if self._filter is not None: + widget.installEventFilter(self._filter) + + text_beside_icon = getattr(item, 'text_beside_icon', False) if text_beside_icon: widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) @@ -280,8 +292,9 @@ class ApplicationToolbar(SpyderToolbar): This is used by Qt to be able to save and restore the state of widgets. """ - def __init__(self, parent, title): + def __init__(self, parent, toolbar_id, title): super().__init__(parent=parent, title=title) + self.ID = toolbar_id self._style = ToolbarStyle(None) self._style.TYPE = 'Application' @@ -328,40 +341,3 @@ def __init__(self, parent=None, title=None): def set_icon_size(self, icon_size): self._icon_size = icon_size self.setIconSize(icon_size) - - def _render(self): - """ - Create the toolbar taking into account the sections and locations. - - This method is called once on widget setup. - """ - sec_items = [] - for sec, items in self._section_items.items(): - for item in items: - sec_items.append([sec, item]) - - sep = QAction(self) - sep.setSeparator(True) - sec_items.append((None, sep)) - - if sec_items: - sec_items.pop() - - for (sec, item) in sec_items: - if isinstance(item, QAction): - add_method = super().addAction - else: - add_method = super().addWidget - - add_method(item) - - if isinstance(item, QAction): - widget = self.widgetForAction(item) - widget.installEventFilter(self._filter) - - text_beside_icon = getattr(item, 'text_beside_icon', False) - if text_beside_icon: - widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - - if item.isCheckable(): - widget.setCheckable(True) diff --git a/spyder/app/tests/conftest.py b/spyder/app/tests/conftest.py index 59af0e8d053..d301ac888d8 100755 --- a/spyder/app/tests/conftest.py +++ b/spyder/app/tests/conftest.py @@ -8,6 +8,7 @@ # Standard library imports import os import os.path as osp +import random import sys import threading import traceback @@ -255,6 +256,24 @@ def generate_run_parameters(mainwindow, filename, selected=None, return {file_uuid: file_run_params} +def get_random_dockable_plugin(main_window, exclude=None): + """Get a random dockable plugin and give it focus.""" + plugins = main_window.get_dockable_plugins() + for plugin_name, plugin in plugins: + if exclude and plugin_name in exclude: + plugins.remove((plugin_name, plugin)) + + plugin = random.choice(plugins)[1] + + if not plugin.get_widget().toggle_view_action.isChecked(): + plugin.toggle_view(True) + plugin._hide_after_test = True + + plugin.switch_to_plugin() + plugin.get_widget().get_focus_widget().setFocus() + return plugin + + # ============================================================================= # ---- Pytest hooks # ============================================================================= diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 3005358ecb8..6de882872ad 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -48,10 +48,20 @@ from spyder.api.widgets.auxiliary_widgets import SpyderWindowWidget from spyder.api.plugins import Plugins from spyder.app.tests.conftest import ( - COMPILE_AND_EVAL_TIMEOUT, COMPLETION_TIMEOUT, EVAL_TIMEOUT, - generate_run_parameters, find_desired_tab_in_window, LOCATION, - open_file_in_editor, preferences_dialog_helper, read_asset_file, - reset_run_code, SHELL_TIMEOUT, start_new_kernel) + COMPILE_AND_EVAL_TIMEOUT, + COMPLETION_TIMEOUT, + EVAL_TIMEOUT, + get_random_dockable_plugin, + generate_run_parameters, + find_desired_tab_in_window, + LOCATION, + open_file_in_editor, + preferences_dialog_helper, + read_asset_file, + reset_run_code, + SHELL_TIMEOUT, + start_new_kernel +) from spyder.config.base import ( get_home_dir, get_conf_path, get_module_path, running_in_ci, running_in_ci_with_conda) @@ -1743,22 +1753,6 @@ def test_maximize_minimize_plugins(main_window, qtbot): lambda: shell.spyder_kernel_ready and shell._prompt_html is not None, timeout=SHELL_TIMEOUT) - def get_random_plugin(): - """Get a random dockable plugin and give it focus""" - plugins = main_window.get_dockable_plugins() - for plugin_name, plugin in plugins: - if plugin_name in [Plugins.Editor, Plugins.IPythonConsole]: - plugins.remove((plugin_name, plugin)) - - plugin = random.choice(plugins)[1] - - if not plugin.get_widget().toggle_view_action.isChecked(): - plugin.toggle_view(True) - plugin._hide_after_test = True - - plugin.get_widget().get_focus_widget().setFocus() - return plugin - # Wait until the window is fully up shell = main_window.ipyconsole.get_current_shellwidget() qtbot.waitUntil( @@ -1772,7 +1766,10 @@ def get_random_plugin(): max_button = main_toolbar.widgetForAction(max_action) # Maximize a random plugin - plugin_1 = get_random_plugin() + plugin_1 = get_random_dockable_plugin( + main_window, + exclude=[Plugins.Editor, Plugins.IPythonConsole] + ) qtbot.mouseClick(max_button, Qt.LeftButton) # Load test file @@ -1807,7 +1804,10 @@ def get_random_plugin(): # Maximize a plugin and check that it's unmaximized after clicking the # debug button - plugin_2 = get_random_plugin() + plugin_2 = get_random_dockable_plugin( + main_window, + exclude=[Plugins.Editor, Plugins.IPythonConsole] + ) qtbot.mouseClick(max_button, Qt.LeftButton) debug_button = main_window.debug_button with qtbot.waitSignal(shell.executed): @@ -1842,7 +1842,10 @@ def get_random_plugin(): shell.stop_debugging() # Maximize a plugin and check that it's unmaximized after running a file - plugin_3 = get_random_plugin() + plugin_3 = get_random_dockable_plugin( + main_window, + exclude=[Plugins.Editor, Plugins.IPythonConsole] + ) qtbot.mouseClick(max_button, Qt.LeftButton) run_parameters = generate_run_parameters(main_window, test_file) @@ -1855,7 +1858,10 @@ def get_random_plugin(): plugin_3.toggle_view(False) # Maximize a plugin and check that it's unmaximized after running a cell - plugin_4 = get_random_plugin() + plugin_4 = get_random_dockable_plugin( + main_window, + exclude=[Plugins.Editor, Plugins.IPythonConsole] + ) qtbot.mouseClick(max_button, Qt.LeftButton) qtbot.mouseClick(main_window.run_cell_button, Qt.LeftButton) assert not plugin_4.get_widget().get_maximized_state() @@ -1865,7 +1871,10 @@ def get_random_plugin(): # Maximize a plugin and check that it's unmaximized after running a # selection - plugin_5 = get_random_plugin() + plugin_5 = get_random_dockable_plugin( + main_window, + exclude=[Plugins.Editor, Plugins.IPythonConsole] + ) qtbot.mouseClick(max_button, Qt.LeftButton) qtbot.mouseClick(main_window.run_selection_button, Qt.LeftButton) assert not plugin_5.get_widget().get_maximized_state() @@ -5027,8 +5036,8 @@ def move_across_tabs(editorstack): qtbot.waitUntil(lambda: all(trees_update_state(treewidget))) check_symbols_number(1, treewidget) - # Hide outline from view - outline_explorer.toggle_view_action.setChecked(False) + # Dock outline back to the main window + outline_explorer.get_widget().dock_window() assert outline_explorer.get_widget().windowwidget is None # Change code again and save it to emulate what users need to do to close @@ -6798,5 +6807,206 @@ def show_env_consoles_menu(menu): assert not env_consoles_menu.get_actions()[0].isIconVisibleInMenu() +@flaky(max_runs=3) +def test_undock_plugin_and_close(main_window, qtbot): + """ + Test that the UX of plugins that are closed while being undocked works as + expected. + + This checks the functionality added in PR spyder-ide/spyder#19784. + """ + # Select a random plugin and undock it + plugin = get_random_dockable_plugin(main_window) + plugin.get_widget().undock_action.trigger() + qtbot.waitUntil(lambda: plugin.get_widget().windowwidget is not None) + + # Do a normal close and check the plugin was docked + plugin.get_widget().windowwidget.close() + qtbot.waitUntil(lambda: plugin.get_widget().is_visible) + assert not plugin.get_conf('window_was_undocked_before_hiding') + + # Undock plugin, toggle its visibility and check it's hidden + plugin.get_widget().undock_action.trigger() + qtbot.waitUntil(lambda: plugin.get_widget().windowwidget is not None) + plugin.toggle_view_action.setChecked(False) + qtbot.waitUntil(lambda: not plugin.get_widget().is_visible) + assert plugin.get_conf('window_was_undocked_before_hiding') + + # Toggle plugin's visibility and check it's undocked directly + plugin.toggle_view_action.setChecked(True) + qtbot.waitUntil(lambda: plugin.get_widget().windowwidget is not None) + + # Dock plugin and check the default dock/undock behavior is restored + plugin.get_widget().dock_action.trigger() + qtbot.waitUntil(lambda: plugin.get_widget().windowwidget is None) + + plugin.get_widget().undock_action.trigger() + qtbot.waitUntil(lambda: plugin.get_widget().windowwidget is not None) + plugin.get_widget().windowwidget.close() + qtbot.waitUntil(lambda: plugin.get_widget().is_visible) + assert not plugin.get_conf('window_was_undocked_before_hiding') + + # Undock plugin, close it with the close action and check it's hidden + plugin.get_widget().undock_action.trigger() + qtbot.waitUntil(lambda: plugin.get_widget().windowwidget is not None) + plugin.get_widget().close_action.trigger() + qtbot.waitUntil(lambda: not plugin.get_widget().is_visible) + assert plugin.get_conf('window_was_undocked_before_hiding') + + # Reset undocked state of selected plugin + plugin.set_conf('window_was_undocked_before_hiding', False) + + +@flaky(max_runs=3) +def test_outline_in_maximized_editor(main_window, qtbot): + """ + Test that the visibility of the Outline when shown with the maximized + editor works as expected. + + This is a regression test for issue spyder-ide/spyder#16265. + """ + editor = main_window.get_plugin(Plugins.Editor) + outline = main_window.get_plugin(Plugins.OutlineExplorer) + + # Grab maximize button + max_action = main_window.layouts.maximize_action + toolbar = main_window.get_plugin(Plugins.Toolbar) + main_toolbar = toolbar.get_application_toolbar(ApplicationToolbars.Main) + max_button = main_toolbar.widgetForAction(max_action) + + # Maxmimize editor + editor.get_focus_widget().setFocus() + qtbot.mouseClick(max_button, Qt.LeftButton) + + # Check outline is visible + qtbot.waitUntil(lambda: outline.get_widget().is_visible) + assert outline.get_conf('show_with_maximized_editor') + + # Check undock and lock/unlock actions are hidden in Outline's Options menu + outline.get_widget()._options_menu.popup(QPoint(100, 100)) + qtbot.waitUntil(outline.get_widget()._options_menu.isVisible) + assert not outline.get_widget().undock_action.isVisible() + assert not outline.get_widget().lock_unlock_action.isVisible() + outline.get_widget()._options_menu.hide() + + # Close Outline, unmaximize and maximize again, and check it's not visible + outline.get_widget().close_action.trigger() + qtbot.waitUntil(lambda: not outline.get_widget().is_visible) + + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + assert editor.get_focus_widget().hasFocus() + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + assert not outline.get_widget().is_visible + assert not outline.get_conf('show_with_maximized_editor') + + # Unmaximize, show Outline in regular layout, maximize and check is not + # visible, and unmaximize and check it's visible again. + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + assert editor.get_focus_widget().hasFocus() + + assert not outline.toggle_view_action.isChecked() + outline.toggle_view_action.setChecked(True) + qtbot.waitUntil(lambda: outline.get_widget().is_visible) + + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + assert not outline.get_widget().is_visible + + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + assert outline.get_widget().is_visible + + # Maximize, show Outline, unmaximize and maximize again, and check Outline + # is still visible. + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + + assert not outline.toggle_view_action.isChecked() + outline.toggle_view_action.setChecked(True) + qtbot.waitUntil(lambda: outline.get_widget().is_visible) + + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + assert editor.get_focus_widget().hasFocus() + qtbot.mouseClick(max_button, Qt.LeftButton) + qtbot.wait(500) + + assert outline.get_widget().is_visible + assert outline.get_conf('show_with_maximized_editor') + + +@flaky(max_runs=3) +def test_editor_window_outline_and_toolbars(main_window, qtbot): + """Check the behavior of the Outline and toolbars in editor windows.""" + # Create editor window. + editorwindow = main_window.editor.get_widget().create_new_window() + qtbot.waitUntil(editorwindow.isVisible) + + # Check toolbars in editor window are visible + for toolbar in editorwindow.toolbars: + assert toolbar.isVisible() + assert toolbar.toggleViewAction().isChecked() + + # Hide Outline from its close action + editorwindow.editorwidget.outlineexplorer.close_action.trigger() + assert not editorwindow.editorwidget.outlineexplorer.is_visible + + # Check splitter handle is hidden and disabled + assert editorwindow.editorwidget.splitter.handleWidth() == 0 + assert not editorwindow.editorwidget.splitter.handle(1).isEnabled() + + # Show outline again and check it's visible + editorwindow.toggle_outline_action.setChecked(True) + qtbot.waitUntil( + lambda: editorwindow.editorwidget.outlineexplorer.is_visible + ) + + # Check splitter handle is shown and active + assert editorwindow.editorwidget.splitter.handle(1).isEnabled() + assert editorwindow.editorwidget.splitter.handleWidth() > 0 + + # Hide Outline and check its visible state is preserved for new editor + # windows + editorwindow.toggle_outline_action.setChecked(False) + assert not editorwindow.editorwidget.outlineexplorer.is_visible + + editorwindow.close() + + editorwindow1 = main_window.editor.get_widget().create_new_window() + qtbot.waitUntil(editorwindow1.isVisible) + assert not editorwindow1.editorwidget.outlineexplorer.is_visible + + editorwindow1.close() + + # Hide debug toolbar in main window + main_toolbar = main_window.get_plugin(Plugins.Toolbar) + debug_toolbar_action = main_toolbar.get_action( + f"toggle_view_{ApplicationToolbars.Debug}" + ) + debug_toolbar_action.trigger() + + # Check main toolbars visibility state is synced between main and editor + # windows + editorwindow2 = main_window.editor.get_widget().create_new_window() + + for toolbar in editorwindow2.toolbars: + toolbar_action = toolbar.toggleViewAction() + + if toolbar.ID == ApplicationToolbars.Debug: + assert not toolbar.isVisible() + assert not toolbar_action.isChecked() + else: + assert toolbar.isVisible() + assert toolbar_action.isChecked() + + editorwindow2.close() + + # Restore debug toolbar + debug_toolbar_action.trigger() + + if __name__ == "__main__": pytest.main() diff --git a/spyder/config/main.py b/spyder/config/main.py index 0a5b0ebdb5d..0ec7b9af883 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -258,6 +258,7 @@ 'autosave_interval': 60, 'docstring_type': 'Numpydoc', 'strip_trailing_spaces_on_modify': False, + 'show_outline_in_editor_window': True, }), ('historylog', { @@ -296,7 +297,8 @@ 'sort_files_alphabetically': False, 'show_comments': True, 'follow_cursor': True, - 'display_variables': False + 'display_variables': False, + 'show_with_maximized_editor': True, }), ('preferences', { diff --git a/spyder/plugins/base.py b/spyder/plugins/base.py index 5b0f18104f3..55c07fc45b5 100644 --- a/spyder/plugins/base.py +++ b/spyder/plugins/base.py @@ -202,7 +202,7 @@ def __init__(self, parent=None): self._lock_unlock_action = create_action( self, - text=_("Unlock position"), + text=_("Move"), tip=_("Unlock to move pane to another position"), icon=ima.icon('drag_dock_widget'), triggered=self._lock_unlock_position, @@ -488,13 +488,13 @@ def _lock_unlock_position(self): def _on_title_bar_shown(self, visible): """Actions to perform when the title bar is shown/hidden.""" if visible: - self._lock_unlock_action.setText(_('Lock position')) + self._lock_unlock_action.setText(_('Lock')) self._lock_unlock_action.setIcon(ima.icon('lock_open')) for method_name in ['setToolTip', 'setStatusTip']: method = getattr(self._lock_unlock_action, method_name) method(_("Lock pane to the current position")) else: - self._lock_unlock_action.setText(_('Unlock position')) + self._lock_unlock_action.setText(_('Move')) self._lock_unlock_action.setIcon(ima.icon('drag_dock_widget')) for method_name in ['setToolTip', 'setStatusTip']: method = getattr(self._lock_unlock_action, method_name) diff --git a/spyder/plugins/editor/widgets/main_widget.py b/spyder/plugins/editor/widgets/main_widget.py index 1939bc06c70..9f32af84f1c 100644 --- a/spyder/plugins/editor/widgets/main_widget.py +++ b/spyder/plugins/editor/widgets/main_widget.py @@ -59,11 +59,9 @@ from spyder.plugins.editor.widgets.status import (CursorPositionStatus, EncodingStatus, EOLStatus, ReadWriteStatus, VCSStatus) -from spyder.plugins.mainmenu.api import ApplicationMenus from spyder.plugins.run.api import ( RunContext, RunConfigurationMetadata, RunConfiguration, SupportedExtensionContexts, ExtendedContext) -from spyder.plugins.toolbar.api import ApplicationToolbars from spyder.widgets.mixins import BaseEditMixin from spyder.widgets.printer import SpyderPrinter, SpyderPrintPreviewDialog from spyder.widgets.simplecodeeditor import SimpleCodeEditor @@ -319,8 +317,6 @@ def __init__(self, name, plugin, parent, ignore_last_opened_files=False): self.last_focused_editorstack = {} self.editorwindows = [] self.editorwindows_to_be_created = [] - self.toolbar_list = None - self.menu_list = None # Configuration dialog size self.dialog_size = None @@ -1694,46 +1690,6 @@ def file_saved_in_editorstack(self, editorstack_id_str, # ------------------------------------------------------------------------- def setup_other_windows(self, main, outline_plugin): """Setup toolbars and menus for 'New window' instances""" - # Menus - file_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.File).get_actions() - edit_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.Edit).get_actions() - search_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.Search).get_actions() - source_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.Source).get_actions() - run_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.Run).get_actions() - tools_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.Tools).get_actions() - help_menu_actions = main.mainmenu.get_application_menu( - ApplicationMenus.Help).get_actions() - - self.menu_list = ((_("&File"), file_menu_actions), - (_("&Edit"), edit_menu_actions), - (_("&Search"), search_menu_actions), - (_("Sour&ce"), source_menu_actions), - (_("&Run"), run_menu_actions), - (_("&Tools"), tools_menu_actions), - (_("&View"), []), - (_("&Help"), help_menu_actions)) - - # Toolbars - file_toolbar_actions = main.toolbar.get_application_toolbar( - ApplicationToolbars.File).actions() - debug_toolbar_actions = main.toolbar.get_application_toolbar( - ApplicationToolbars.Debug).actions() - run_toolbar_actions = main.toolbar.get_application_toolbar( - ApplicationToolbars.Run).actions() - - self.toolbar_list = ((_("File toolbar"), "file_toolbar", - file_toolbar_actions), - (_("Run toolbar"), "run_toolbar", - run_toolbar_actions), - (_("Debug toolbar"), "debug_toolbar", - debug_toolbar_actions)) - # Outline setup self.outline_plugin = outline_plugin @@ -1752,13 +1708,9 @@ def create_new_window(self): window = EditorMainWindow( self, self.stack_menu_actions, - self.toolbar_list, - self.menu_list, outline_plugin=self.outline_plugin ) - window.add_toolbars_to_menu("&View", window.get_toolbars()) - window.load_toolbars() window.resize(self.size()) window.show() window.editorwidget.editorsplitter.editorstack.new_window = True diff --git a/spyder/plugins/editor/widgets/window.py b/spyder/plugins/editor/widgets/window.py index ed84cb869d6..bb25fa9857d 100644 --- a/spyder/plugins/editor/widgets/window.py +++ b/spyder/plugins/editor/widgets/window.py @@ -4,7 +4,7 @@ # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) -"""EditorWidget and EditorMainWindow widget""" +"""EditorWidget and EditorMainWindow widgets.""" # pylint: disable=C0103 # pylint: disable=R0903 @@ -17,40 +17,94 @@ import sys # Third party imports +import qstylizer.style from qtpy.QtCore import QByteArray, QEvent, QPoint, QSize, Qt, Signal, Slot from qtpy.QtGui import QFont from qtpy.QtWidgets import (QAction, QApplication, QMainWindow, QSplitter, QVBoxLayout, QWidget) # Local imports +from spyder.api.plugins import Plugins +from spyder.api.config.decorators import on_conf_change +from spyder.api.config.mixins import SpyderConfigurationObserver +from spyder.api.widgets.toolbars import ApplicationToolbar from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.base import _ from spyder.plugins.editor.widgets.splitter import EditorSplitter from spyder.plugins.editor.widgets.status import (CursorPositionStatus, EncodingStatus, EOLStatus, ReadWriteStatus, VCSStatus) +from spyder.plugins.mainmenu.api import ( + ApplicationMenu, + ApplicationMenus, + MENUBAR_STYLESHEET, +) from spyder.plugins.outlineexplorer.main_widget import OutlineExplorerWidget -from spyder.py3compat import qbytearray_to_str, to_text_string -from spyder.utils.qthelpers import add_actions, create_toolbutton -from spyder.utils.stylesheet import APP_STYLESHEET, APP_TOOLBAR_STYLESHEET +from spyder.plugins.toolbar.api import ApplicationToolbars +from spyder.py3compat import qbytearray_to_str +from spyder.utils.palette import SpyderPalette +from spyder.utils.qthelpers import create_toolbutton +from spyder.utils.stylesheet import APP_STYLESHEET from spyder.widgets.findreplace import FindReplace logger = logging.getLogger(__name__) +# ---- Constants +# ----------------------------------------------------------------------------- +class EditorMainWindowMenus: + View = "view" + + +class ViewMenuSections: + Outline = "outline" + Toolbars = "toolbars" + + class EditorMainWindowActions: - CloseWindow = "close_window_action" + ToggleOutline = "toggle_outline" + + +# ---- Widgets +# ----------------------------------------------------------------------------- +class OutlineExplorerInEditorWindow(OutlineExplorerWidget): + + sig_collapse_requested = Signal() + + @Slot() + def close_dock(self): + """ + Reimplemented to preserve the widget's visible state when shown in an + editor window. + """ + self.sig_collapse_requested.emit() + +class EditorWidget(QSplitter, SpyderConfigurationObserver): + """Main widget to show in EditorMainWindow.""" -class EditorWidget(QSplitter): CONF_SECTION = 'editor' + SPLITTER_WIDTH = "7px" def __init__(self, parent, main_widget, menu_actions, outline_plugin): - QSplitter.__init__(self, parent) + super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) - statusbar = parent.statusBar() # Create a status bar + # ---- Attributes + self.editorstacks = [] + self.main_widget = main_widget + self._sizes = None + + # This needs to be done at this point to avoid an error at startup + self._splitter_css = self._generate_splitter_stylesheet() + + # ---- Find widget + self.find_widget = FindReplace(self, enable_replace=True) + self.find_widget.hide() + + # ---- Status bar + statusbar = parent.statusBar() self.vcs_status = VCSStatus(self) self.cursorpos_status = CursorPositionStatus(self) self.encoding_status = EncodingStatus(self) @@ -63,17 +117,10 @@ def __init__(self, parent, main_widget, menu_actions, outline_plugin): statusbar.insertPermanentWidget(0, self.cursorpos_status) statusbar.insertPermanentWidget(0, self.vcs_status) - self.editorstacks = [] - - self.main_widget = main_widget - - self.find_widget = FindReplace(self, enable_replace=True) - self.find_widget.hide() - - # Set up an outline but only if its corresponding plugin is available. + # ---- Outline. self.outlineexplorer = None if outline_plugin is not None: - self.outlineexplorer = OutlineExplorerWidget( + self.outlineexplorer = OutlineExplorerInEditorWindow( 'outline_explorer', outline_plugin, self, @@ -89,16 +136,20 @@ def __init__(self, parent, main_widget, menu_actions, outline_plugin): # Remove bottom section actions from Options menu because they # don't apply here. options_menu = self.outlineexplorer.get_options_menu() - for action in ['undock_pane', 'close_pane', - 'lock_unlock_position']: + for action in ['undock_pane', 'lock_unlock_position']: options_menu.remove_action(action) + # Signals self.outlineexplorer.edit_goto.connect( lambda filenames, goto, word: main_widget.load(filenames=filenames, goto=goto, word=word, editorwindow=self.parent()) ) + self.outlineexplorer.sig_collapse_requested.connect( + lambda: self.set_conf("show_outline_in_editor_window", False) + ) + # Start symbol services for all supported languages for language in outline_plugin.get_supported_languages(): self.outlineexplorer.start_symbol_services(language) @@ -106,6 +157,7 @@ def __init__(self, parent, main_widget, menu_actions, outline_plugin): # Tell Outline's treewidget that is visible self.outlineexplorer.change_tree_visibility(True) + # ---- Editor widgets editor_widgets = QWidget(self) editor_layout = QVBoxLayout() editor_layout.setSpacing(0) @@ -121,15 +173,31 @@ def __init__(self, parent, main_widget, menu_actions, outline_plugin): editor_layout.addWidget(self.editorsplitter) editor_layout.addWidget(self.find_widget) + # ---- Splitter self.splitter = QSplitter(self) self.splitter.setContentsMargins(0, 0, 0, 0) self.splitter.addWidget(editor_widgets) - if outline_plugin is not None: + if self.outlineexplorer is not None: self.splitter.addWidget(self.outlineexplorer) - self.splitter.setStretchFactor(0, 5) + + self.splitter.setStretchFactor(0, 3) self.splitter.setStretchFactor(1, 1) + + # This sets the same UX as the one users encounter when the editor is + # maximized. + self.splitter.setChildrenCollapsible(False) + self.splitter.splitterMoved.connect(self.on_splitter_moved) + if ( + self.outlineexplorer is not None + and not self.get_conf("show_outline_in_editor_window") + ): + self.outlineexplorer.close_dock() + + # ---- Style + self.splitter.setStyleSheet(self._splitter_css.toString()) + def register_editorstack(self, editorstack): logger.debug("Registering editorstack") self.__print_editorstacks() @@ -158,6 +226,25 @@ def __print_editorstacks(self): for es in self.editorstacks: logger.debug(f" {es}") + def _generate_splitter_stylesheet(self): + # Set background color to be the same as the one used in any other + # widget. This removes what appears to be some extra borders in several + # places. + css = qstylizer.style.StyleSheet() + css.QSplitter.setValues( + backgroundColor=SpyderPalette.COLOR_BACKGROUND_1 + ) + + # Make splitter handle to have the same size as the QMainWindow + # separators. That's because the editor and outline are shown like + # this when the editor is maximized. + css['QSplitter::handle'].setValues( + width=self.SPLITTER_WIDTH, + height=self.SPLITTER_WIDTH + ) + + return css + def unregister_editorstack(self, editorstack): logger.debug("Unregistering editorstack") self.main_widget.unregister_editorstack(editorstack) @@ -192,28 +279,74 @@ def on_splitter_moved(self, position, index): else: self.outlineexplorer.change_tree_visibility(True) + @on_conf_change(option='show_outline_in_editor_window') + def toggle_outlineexplorer(self, value): + """Toggle outline explorer visibility.""" + if value: + # When self._sizes is not available, the splitter sizes are set + # automatically by the ratios set for it above. + if self._sizes is not None: + self.splitter.setSizes(self._sizes) + + # Show and enable splitter handle + self._splitter_css['QSplitter::handle'].setValues( + width=self.SPLITTER_WIDTH, + height=self.SPLITTER_WIDTH + ) + self.splitter.setStyleSheet(self._splitter_css.toString()) + if self.splitter.handle(1) is not None: + self.splitter.handle(1).setEnabled(True) + else: + self._sizes = self.splitter.sizes() + self.splitter.setChildrenCollapsible(True) + + # Collapse Outline + self.splitter.moveSplitter(self.size().width(), 0) + + # Hide and disable splitter handle + self._splitter_css['QSplitter::handle'].setValues( + width="0px", + height="0px" + ) + self.splitter.setStyleSheet(self._splitter_css.toString()) + if self.splitter.handle(1) is not None: + self.splitter.handle(1).setEnabled(False) + + self.splitter.setChildrenCollapsible(False) + class EditorMainWindow(QMainWindow, SpyderWidgetMixin): + CONF_SECTION = "editor" + sig_window_state_changed = Signal(object) - def __init__(self, main_widget, menu_actions, toolbar_list, menu_list, - outline_plugin, parent=None): - # Parent needs to be `None` if the the created widget is meant to be + def __init__(self, main_widget, menu_actions, outline_plugin, parent=None): + # Parent needs to be `None` if the created widget is meant to be # independent. See spyder-ide/spyder#17803 super().__init__(parent, class_parent=main_widget) self.setAttribute(Qt.WA_DeleteOnClose) + # ---- Attributes self.main_widget = main_widget self.window_size = None + self.toolbars = [] - self.editorwidget = EditorWidget(self, main_widget, menu_actions, - outline_plugin) + # ---- Main widget + self.editorwidget = EditorWidget( + self, + main_widget, + menu_actions, + outline_plugin + ) self.sig_window_state_changed.connect( - self.editorwidget.on_window_state_changed) + self.editorwidget.on_window_state_changed + ) self.setCentralWidget(self.editorwidget) - # Setting interface theme + # ---- Style self.setStyleSheet(str(APP_STYLESHEET)) + if not sys.platform == "darwin": + self.menuBar().setStyleSheet(str(MENUBAR_STYLESHEET)) # Give focus to current editor to update/show all status bar widgets editorstack = self.editorwidget.editorsplitter.editorstack @@ -224,67 +357,59 @@ def __init__(self, main_widget, menu_actions, toolbar_list, menu_list, self.setWindowTitle("Spyder - %s" % main_widget.windowTitle()) self.setWindowIcon(main_widget.windowIcon()) - self.toolbars = [] - if toolbar_list: - for title, object_name, actions in toolbar_list: - toolbar = self.addToolBar(title) - toolbar.setObjectName(object_name) - toolbar.setStyleSheet(str(APP_TOOLBAR_STYLESHEET)) - toolbar.setMovable(False) - add_actions(toolbar, actions) - self.toolbars.append(toolbar) - - self.menus = [] - if menu_list: - quit_action = self.create_action( - EditorMainWindowActions.CloseWindow, - _("Close window"), - icon=self.create_icon("close_pane"), - tip=_("Close this window"), - triggered=self.close - ) - for index, (title, actions) in enumerate(menu_list): - menu = self.menuBar().addMenu(title) - if index == 0: - # File menu - add_actions(menu, actions+[None, quit_action]) - else: - add_actions(menu, actions) - self.menus.append(menu) - - def get_toolbars(self): - """Get the toolbars.""" - return self.toolbars - - def add_toolbars_to_menu(self, menu_title, actions): - """Add toolbars to a menu.""" - # Six is the position of the view menu in menus list - # that you can find in plugins/editor.py setup_other_windows. - if self.menus: - view_menu = self.menus[6] - if actions == self.toolbars and view_menu: - toolbars = [] - for toolbar in self.toolbars: - action = toolbar.toggleViewAction() - toolbars.append(action) - add_actions(view_menu, toolbars) - - def load_toolbars(self): - """Loads the last visible toolbars from the .ini file.""" - toolbars_names = self.get_conf( - 'last_visible_toolbars', section='main', default=[] - ) - if toolbars_names: - dic = {} - for toolbar in self.toolbars: - dic[toolbar.objectName()] = toolbar - toolbar.toggleViewAction().setChecked(False) - toolbar.setVisible(False) - for name in toolbars_names: - if name in dic: - dic[name].toggleViewAction().setChecked(True) - dic[name].setVisible(True) - + # ---- Add toolbars + toolbar_list = [ + ApplicationToolbars.File, + ApplicationToolbars.Run, + ApplicationToolbars.Debug + ] + + for toolbar_id in toolbar_list: + # This is necessary to run tests for this widget without Spyder's + # main window + try: + toolbar = self.get_toolbar(toolbar_id, plugin=Plugins.Toolbar) + except KeyError: + continue + + new_toolbar = ApplicationToolbar(self, toolbar_id, toolbar._title) + for action in toolbar.actions(): + new_toolbar.add_item(action) + + new_toolbar.render() + new_toolbar.setMovable(False) + + self.addToolBar(new_toolbar) + self.toolbars.append(new_toolbar) + + # ---- Add menus + menu_list = [ + ApplicationMenus.File, + ApplicationMenus.Edit, + ApplicationMenus.Search, + ApplicationMenus.Source, + ApplicationMenus.Run, + ApplicationMenus.Tools, + EditorMainWindowMenus.View, + ApplicationMenus.Help + ] + + for menu_id in menu_list: + if menu_id == EditorMainWindowMenus.View: + view_menu = self._create_view_menu() + self.menuBar().addMenu(view_menu) + else: + # This is necessary to run tests for this widget without + # Spyder's main window + try: + self.menuBar().addMenu( + self.get_menu(menu_id, plugin=Plugins.MainMenu) + ) + except KeyError: + continue + + # ---- Qt methods + # ------------------------------------------------------------------------- def resizeEvent(self, event): """Reimplement Qt method""" if not self.isMaximized() and not self.isFullScreen(): @@ -311,6 +436,8 @@ def changeEvent(self, event): self.sig_window_state_changed.emit(self.windowState()) super().changeEvent(event) + # ---- Public API + # ------------------------------------------------------------------------- def get_layout_settings(self): """Return layout state""" splitsettings = self.editorwidget.editorsplitter.get_layout_settings() @@ -343,6 +470,55 @@ def set_layout_settings(self, settings): if splitsettings is not None: self.editorwidget.editorsplitter.set_layout_settings(splitsettings) + # ---- Private API + # ------------------------------------------------------------------------- + def _create_view_menu(self): + # Create menu + view_menu = self._create_menu( + menu_id=EditorMainWindowMenus.View, + parent=self, + title=_("&View"), + register=False, + MenuClass=ApplicationMenu + ) + + # Create Outline action + self.toggle_outline_action = self.create_action( + EditorMainWindowActions.ToggleOutline, + _("Outline"), + toggled=True, + option="show_outline_in_editor_window" + ) + + view_menu.add_action( + self.toggle_outline_action, + section=ViewMenuSections.Outline + ) + + # Add toolbar toggle view actions + visible_toolbars = self.get_conf( + 'last_visible_toolbars', + section='toolbar' + ) + + for toolbar in self.toolbars: + toolbar_action = toolbar.toggleViewAction() + toolbar_action.action_id = f'toolbar_{toolbar.ID}' + + if toolbar.ID not in visible_toolbars: + toolbar_action.setChecked(False) + toolbar.setVisible(False) + else: + toolbar_action.setChecked(True) + toolbar.setVisible(True) + + view_menu.add_action( + toolbar_action, + section=ViewMenuSections.Toolbars + ) + + return view_menu + class EditorMainWidgetExample(QSplitter): def __init__(self): @@ -391,7 +567,7 @@ def __init__(self): def go_to_file(self, fname, lineno, text='', start_column=None): editorstack = self.editorstacks[0] - editorstack.set_current_filename(to_text_string(fname)) + editorstack.set_current_filename(str(fname)) editor = editorstack.get_current_editor() editor.go_to_line(lineno, word=text, start_column=start_column) diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index 41ac4d413d5..e4faca1da67 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -57,10 +57,10 @@ # The current versions are: # # * Spyder 4: Version 0 (it was the default). -# * Spyder 5.0.0 to 5.0.5: Version 1 (a bump was required due to the new API). +# * Spyder 5.0.0: Version 1 (a bump was required due to the new API). # * Spyder 5.1.0: Version 2 (a bump was required due to the migration of # Projects to the new API). -# * Spyder 5.2.0: Version 3 (a bump was required due to the migration of +# * Spyder 5.2.0: Version 3 (a bump was required due to the migration of the # IPython Console to the new API) # * Spyder 6.0.0: Version 4 (a bump was required due to the migration of # Editor to the new API) @@ -253,19 +253,25 @@ def _update_lock_interface_action(self): self.lock_interface_action.setIcon(icon) self.lock_interface_action.setText(text) - def _reapply_docktabbar_style(self): - """Reapply dock tabbar style if necessary.""" + def _reapply_docktabbar_style(self, force=False): + """Reapply dock tabbar style.""" saved_state_version = self.get_conf( "window_state_version", default=WINDOW_STATE_VERSION ) - # Reapplying style by installing the tab event filter if the window - # state version changed or the previous session ran a Spyder version - # older than 6.0.0a5, which is when this change became necessary. + # Reapplying style by installing the tab event filter again. if ( + # Check if the window state version changed. This covers the case + # of starting a Spyder 5 session after a Spyder 6 one, but only if + # users have 5.5.4 or greater. saved_state_version < WINDOW_STATE_VERSION - # 82.2.0 is the conf version for 6.0 alpha5 + # The previous session ran a Spyder version older than 6.0.0a5, + # which is when this change became necessary (82.2.0 is the conf + # version for that release) or parse(self.old_conf_version) < parse("82.2.0") + # This is used when switching to a different layout, which also + # requires this. + or force ): plugins = self.get_dockable_plugins() for plugin in plugins: @@ -442,6 +448,9 @@ def quick_layout_switch(self, index_or_layout_id): ---------- index_or_layout_id: int or str """ + # We need to do this first so the new layout is applied as expected. + self.unmaximize_dockwidget() + section = 'quick_layouts' container = self.get_container() try: @@ -490,6 +499,10 @@ def quick_layout_switch(self, index_or_layout_id): action = plugin._toggle_view_action action.setChecked(plugin.dockwidget.isVisible()) + # This is necessary to restore the style for dock tabbars after the + # switch + self._reapply_docktabbar_style(force=True) + return index_or_layout_id def load_window_settings(self, prefix, default=False, section='main'): @@ -682,6 +695,12 @@ def maximize_dockwidget(self, restore=False): First call: maximize current dockwidget Second call (or restore=True): restore original window layout """ + editor = self.get_plugin(Plugins.Editor, error=False) + outline_explorer = self.get_plugin( + Plugins.OutlineExplorer, + error=False + ) + if self._state_before_maximizing is None: if restore: return @@ -704,14 +723,14 @@ def maximize_dockwidget(self, restore=False): if plugin.isAncestorOf(focus_widget): self._last_plugin = plugin - # Only plugins that have a dockwidget are part of widgetlist, - # so last_plugin can be None after the above "for" cycle. - # For example, this happens if, after Spyder has started, focus - # is set to the Working directory toolbar (which doesn't have - # a dockwidget) and then you press the Maximize button + # This prevents a possible error when the value of _last_plugin + # turns out to be None. if self._last_plugin is None: - # Using the Editor as default plugin to maximize - self._last_plugin = self.get_plugin(Plugins.Editor) + # Use the Editor as default plugin to maximize + if editor is not None: + self._last_plugin = editor + else: + return # Maximize last_plugin self._last_plugin.dockwidget.toggleViewAction().setDisabled(True) @@ -736,13 +755,10 @@ def maximize_dockwidget(self, restore=False): self._last_plugin.show() self._last_plugin._visibility_changed(True) - if self._last_plugin is self.main.editor: - # Automatically show the outline if the editor was maximized: - outline_explorer = self.get_plugin(Plugins.OutlineExplorer) - self.main.addDockWidget( - Qt.RightDockWidgetArea, - outline_explorer.dockwidget) - outline_explorer.dockwidget.show() + if self._last_plugin is editor: + # Automatically show the outline if the editor was maximized + if outline_explorer is not None: + outline_explorer.dock_with_maximized_editor() else: # Restore original layout (before maximizing current dockwidget) try: @@ -752,6 +768,7 @@ def maximize_dockwidget(self, restore=False): except AttributeError: # Old API self._last_plugin.dockwidget.setWidget(self._last_plugin) + self._last_plugin.dockwidget.toggleViewAction().setEnabled(True) self.main.setCentralWidget(None) @@ -766,6 +783,11 @@ def maximize_dockwidget(self, restore=False): self._state_before_maximizing, version=WINDOW_STATE_VERSION ) self._state_before_maximizing = None + + if self._last_plugin is editor: + if outline_explorer is not None: + outline_explorer.hide_from_maximized_editor() + try: # New API self._last_plugin.get_widget().get_focus_widget().setFocus() @@ -1050,7 +1072,14 @@ def tabify_helper(plugin, next_to_plugins): return False # Get the actual plugins from their names - next_to_plugins = [self.get_plugin(p) for p in next_to_plugins] + next_to_plugins = [ + self.get_plugin(p, error=False) for p in next_to_plugins + ] + + # Remove not available plugins from next_to_plugins + next_to_plugins = [ + p for p in next_to_plugins if p is not None + ] if plugin.get_conf('first_time', True): # This tabifies external and internal plugins that are loaded for diff --git a/spyder/plugins/mainmenu/api.py b/spyder/plugins/mainmenu/api.py index b39a4a2adb8..779b2949711 100644 --- a/spyder/plugins/mainmenu/api.py +++ b/spyder/plugins/mainmenu/api.py @@ -10,7 +10,51 @@ # Local imports from spyder.api.widgets.menus import SpyderMenu +from spyder.utils.palette import SpyderPalette +from spyder.utils.stylesheet import AppStyle, SpyderStyleSheet + +# ---- Stylesheet +# ----------------------------------------------------------------------------- +class _MenuBarStylesheet(SpyderStyleSheet): + """Stylesheet for menubars used in Spyder.""" + + def set_stylesheet(self): + css = self.get_stylesheet() + + # Set the same color as the one used for the app toolbar + css.QMenuBar.setValues( + backgroundColor=SpyderPalette.COLOR_BACKGROUND_4 + ) + + # Give more padding and margin to items + css['QMenuBar::item'].setValues( + padding=f'{2 * AppStyle.MarginSize}px', + margin='0px 2px' + ) + + # Remove padding when pressing main menus + css['QMenuBar::item:pressed'].setValues( + padding='0px' + ) + + # Set hover and pressed state of items in the menu bar + for state in ['selected', 'pressed']: + # Don't use a different color for the QMenuBar pressed state + # because a lighter color has too little contrast with the text. + bg_color = SpyderPalette.COLOR_BACKGROUND_5 + + css[f"QMenuBar::item:{state}"].setValues( + backgroundColor=bg_color, + borderRadius=SpyderPalette.SIZE_BORDER_RADIUS + ) + + +MENUBAR_STYLESHEET = _MenuBarStylesheet() + + +# ---- Menu constants +# ----------------------------------------------------------------------------- class ApplicationContextMenu: Documentation = 'context_documentation_section' About = 'context_about_section' @@ -103,6 +147,8 @@ class HelpMenuSections: About = 'about_section' +# ---- App menu class +# ----------------------------------------------------------------------------- class ApplicationMenu(SpyderMenu): """ Spyder main window application menu. diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py index 9fe3da8458b..2fd5603657a 100644 --- a/spyder/plugins/mainmenu/plugin.py +++ b/spyder/plugins/mainmenu/plugin.py @@ -14,9 +14,6 @@ import sys from typing import Dict, List, Tuple, Optional, Union -# Third-party imports -import qstylizer.style - # Local imports from spyder.api.config.fonts import SpyderFontType from spyder.api.exceptions import SpyderAPIError @@ -24,10 +21,13 @@ from spyder.api.plugins import SpyderPluginV2, SpyderDockablePlugin, Plugins from spyder.api.translations import _ from spyder.api.widgets.menus import SpyderMenu -from spyder.plugins.mainmenu.api import ApplicationMenu, ApplicationMenus +from spyder.api.widgets.mixins import SpyderMenuMixin +from spyder.plugins.mainmenu.api import ( + ApplicationMenu, + ApplicationMenus, + MENUBAR_STYLESHEET, +) from spyder.utils.qthelpers import SpyderAction -from spyder.utils.palette import SpyderPalette -from spyder.utils.stylesheet import AppStyle # Extended typing definitions @@ -37,7 +37,7 @@ ItemQueue = Dict[str, List[ItemSectionBefore]] -class MainMenu(SpyderPluginV2): +class MainMenu(SpyderPluginV2, SpyderMenuMixin): NAME = 'mainmenu' CONF_SECTION = NAME CONF_FILE = False @@ -67,7 +67,7 @@ def on_initialize(self): if not sys.platform == 'darwin': app_font = self.get_font(font_type=SpyderFontType.Interface) self.main.menuBar().setFont(app_font) - self.main.menuBar().setStyleSheet(self._stylesheet) + self.main.menuBar().setStyleSheet(str(MENUBAR_STYLESHEET)) # Create Application menus using plugin public API create_app_menu = self.create_application_menu @@ -116,39 +116,6 @@ def _hide_options_menus(self): # Old API plugin_instance._options_menu.hide() - @property - def _stylesheet(self): - css = qstylizer.style.StyleSheet() - - # Set the same color as the one used for the app toolbar - css.QMenuBar.setValues( - backgroundColor=SpyderPalette.COLOR_BACKGROUND_4 - ) - - # Give more padding and margin to items - css['QMenuBar::item'].setValues( - padding=f'{2 * AppStyle.MarginSize}px', - margin='0px 2px' - ) - - # Remove padding when pressing main menus - css['QMenuBar::item:pressed'].setValues( - padding='0px' - ) - - # Set hover and pressed state of items in the menu bar - for state in ['selected', 'pressed']: - # Don't use a different color for the QMenuBar pressed state - # because a lighter color has too little contrast with the text. - bg_color = SpyderPalette.COLOR_BACKGROUND_5 - - css[f"QMenuBar::item:{state}"].setValues( - backgroundColor=bg_color, - borderRadius=SpyderPalette.SIZE_BORDER_RADIUS - ) - - return css.toString() - # ---- Public API # ------------------------------------------------------------------------ def create_application_menu( @@ -174,11 +141,12 @@ def create_application_menu( 'Menu with id "{}" already added!'.format(menu_id) ) - menu = ApplicationMenu( - self.main, + menu = self._create_menu( menu_id=menu_id, + parent=self.main, title=title, - min_width=min_width + min_width=min_width, + MenuClass=ApplicationMenu ) self._APPLICATION_MENUS[menu_id] = menu self.main.menuBar().addMenu(menu) diff --git a/spyder/plugins/outlineexplorer/main_widget.py b/spyder/plugins/outlineexplorer/main_widget.py index c4f6855196a..e8188b95b99 100644 --- a/spyder/plugins/outlineexplorer/main_widget.py +++ b/spyder/plugins/outlineexplorer/main_widget.py @@ -54,12 +54,13 @@ def __init__(self, name, plugin, parent=None, context=None): super().__init__(name, plugin, parent) + self.in_maximized_editor = False + self.treewidget = OutlineExplorerTreeWidget(self) self.treewidget.sig_display_spinner.connect(self.start_spinner) self.treewidget.sig_hide_spinner.connect(self.stop_spinner) self.treewidget.sig_update_configuration.connect( self.sig_update_configuration) - self.treewidget.header().hide() layout = QHBoxLayout() @@ -160,7 +161,19 @@ def setup(self): ) def update_actions(self): - pass + if self.in_maximized_editor or self.windowwidget: + for action in [self.undock_action, self.lock_unlock_action]: + if action.isVisible(): + action.setVisible(False) + else: + # Avoid error at startup because these actions are not available + # at that time. + try: + for action in [self.undock_action, self.lock_unlock_action]: + if not action.isVisible(): + action.setVisible(True) + except AttributeError: + pass def change_visibility(self, enable, force_focus=None): """Reimplemented to tell treewidget what the visibility state is.""" @@ -183,7 +196,27 @@ def create_window(self): """ super().create_window() self.windowwidget.sig_window_state_changed.connect( - self._handle_undocked_window_state) + self._handle_undocked_window_state + ) + + @Slot() + def close_dock(self): + """ + Reimplemented to preserve the widget's visible state when editor is + maximized. + """ + if self.in_maximized_editor: + self.set_conf('show_with_maximized_editor', False) + super().close_dock() + + def toggle_view(self, checked): + """Reimplemented to handle the case when the editor is maximized.""" + if self.in_maximized_editor: + self.set_conf('show_with_maximized_editor', checked) + if checked: + self._plugin.dock_with_maximized_editor() + return + super().toggle_view(checked) # ---- Public API # ------------------------------------------------------------------------- diff --git a/spyder/plugins/outlineexplorer/plugin.py b/spyder/plugins/outlineexplorer/plugin.py index 51dd7574707..610efaab570 100644 --- a/spyder/plugins/outlineexplorer/plugin.py +++ b/spyder/plugins/outlineexplorer/plugin.py @@ -109,6 +109,15 @@ def _restore_scrollbar_position(self): if scrollbar_pos is not None: explorer.treewidget.set_scrollbar_position(scrollbar_pos) + def _set_toggle_view_action_state(self): + """Set state of the toogle view action.""" + self.get_widget().blockSignals(True) + if self.get_widget().is_visible: + self.get_widget().toggle_view_action.setChecked(True) + else: + self.get_widget().toggle_view_action.setChecked(False) + self.get_widget().blockSignals(False) + # ----- Public API # ------------------------------------------------------------------------- @Slot(dict, str) @@ -132,3 +141,22 @@ def update_all_editors(self): def get_supported_languages(self): """List of languages with symbols support.""" return self.get_widget().get_supported_languages() + + def dock_with_maximized_editor(self): + """ + Actions to take when the plugin is docked next to the editor when the + latter is maximized. + """ + self.get_widget().in_maximized_editor = True + if self.get_conf('show_with_maximized_editor'): + self.main.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget) + self.dockwidget.show() + self._set_toggle_view_action_state() + + def hide_from_maximized_editor(self): + """ + Actions to take when the plugin is hidden after the editor is + unmaximized. + """ + self.get_widget().in_maximized_editor = False + self._set_toggle_view_action_state() diff --git a/spyder/plugins/toolbar/container.py b/spyder/plugins/toolbar/container.py index 3a9a9966a61..3999684a8a0 100644 --- a/spyder/plugins/toolbar/container.py +++ b/spyder/plugins/toolbar/container.py @@ -11,7 +11,7 @@ # Standard library imports from collections import OrderedDict from spyder.utils.qthelpers import SpyderAction -from typing import Optional, Union, Tuple +from typing import Dict, List, Optional, Tuple, Union # Third party imports from qtpy.QtCore import QSize, Slot @@ -25,7 +25,7 @@ from spyder.api.utils import get_class_values from spyder.api.widgets.toolbars import ApplicationToolbar from spyder.plugins.toolbar.api import ApplicationToolbars -from spyder.utils.registries import TOOLBAR_REGISTRY +from spyder.utils.registries import ACTION_REGISTRY, TOOLBAR_REGISTRY # Type annotations @@ -65,7 +65,7 @@ def __init__(self, name, plugin, parent=None): self._ADDED_TOOLBARS = OrderedDict() self._toolbarslist = [] self._visible_toolbars = [] - self._ITEMS_QUEUE = {} # type: Dict[str, List[ItemInfo]] + self._ITEMS_QUEUE: Dict[str, List[ItemInfo]] = {} # ---- Private Methods # ------------------------------------------------------------------------ @@ -81,8 +81,10 @@ def _get_visible_toolbars(self): """Collect the visible toolbars.""" toolbars = [] for toolbar in self._toolbarslist: - if (toolbar.toggleViewAction().isChecked() - and toolbar not in toolbars): + if ( + toolbar.toggleViewAction().isChecked() + and toolbar not in toolbars + ): toolbars.append(toolbar) self._visible_toolbars = toolbars @@ -139,7 +141,10 @@ def update_actions(self): # ---- Public API # ------------------------------------------------------------------------ def create_application_toolbar( - self, toolbar_id: str, title: str) -> ApplicationToolbar: + self, + toolbar_id: str, + title: str + ) -> ApplicationToolbar: """ Create an application toolbar and add it to the main window. @@ -157,14 +162,15 @@ def create_application_toolbar( """ if toolbar_id in self._APPLICATION_TOOLBARS: raise SpyderAPIError( - 'Toolbar with ID "{}" already added!'.format(toolbar_id)) + 'Toolbar with ID "{}" already added!'.format(toolbar_id) + ) - toolbar = ApplicationToolbar(self, title) - toolbar.ID = toolbar_id + toolbar = ApplicationToolbar(self, toolbar_id, title) toolbar.setObjectName(toolbar_id) TOOLBAR_REGISTRY.register_reference( - toolbar, toolbar_id, self.PLUGIN_NAME, self.CONTEXT_NAME) + toolbar, toolbar_id, self.PLUGIN_NAME, self.CONTEXT_NAME + ) self._APPLICATION_TOOLBARS[toolbar_id] = toolbar self._add_missing_toolbar_elements(toolbar, toolbar_id) @@ -234,13 +240,15 @@ def remove_application_toolbar(self, toolbar_id: str, mainwindow=None): if mainwindow: mainwindow.removeToolBar(toolbar) - def add_item_to_application_toolbar(self, - item: ToolbarItem, - toolbar_id: Optional[str] = None, - section: Optional[str] = None, - before: Optional[str] = None, - before_section: Optional[str] = None, - omit_id: bool = False): + def add_item_to_application_toolbar( + self, + item: ToolbarItem, + toolbar_id: Optional[str] = None, + section: Optional[str] = None, + before: Optional[str] = None, + before_section: Optional[str] = None, + omit_id: bool = False + ): """ Add action or widget `item` to given application toolbar `section`. @@ -271,8 +279,11 @@ def add_item_to_application_toolbar(self, toolbar.add_item(item, section=section, before=before, before_section=before_section, omit_id=omit_id) - def remove_item_from_application_toolbar(self, item_id: str, - toolbar_id: Optional[str] = None): + def remove_item_from_application_toolbar( + self, + item_id: str, + toolbar_id: Optional[str] = None + ): """ Remove action or widget from given application toolbar by id. @@ -315,7 +326,7 @@ def get_application_toolbar(self, toolbar_id: str) -> ApplicationToolbar: return self._APPLICATION_TOOLBARS[toolbar_id] - def get_application_toolbars(self): + def get_application_toolbars(self) -> List[ApplicationToolbar]: """ Return all created application toolbars. @@ -349,7 +360,9 @@ def load_last_visible_toolbars(self): self._visible_toolbars = toolbars else: - self._get_visible_toolbars() + # This is necessary to set the toolbars in EditorMainWindow the + # first time Spyder starts. + self.save_last_visible_toolbars() for toolbar in self._visible_toolbars: toolbar.setVisible(toolbars_visible) @@ -367,6 +380,11 @@ def create_toolbars_menu(self): for toolbar_id, toolbar in self._ADDED_TOOLBARS.items(): if toolbar: action = toolbar.toggleViewAction() + + # This is necessary to show the same visible toolbars both in + # MainWindow and EditorMainWindow. + action.triggered.connect(self.save_last_visible_toolbars) + if not PYSIDE2: # Modifying __class__ of a QObject created by C++ [1] seems # to invalidate the corresponding Python object when PySide @@ -378,7 +396,18 @@ def create_toolbars_menu(self): # and QMainWindow.addToolbar(QString), which return a # pointer to an already existing QObject. action.__class__ = QActionID - action.action_id = f'toolbar_{toolbar_id}' + + # Register action + id_ = f'toggle_view_{toolbar_id}' + action.action_id = id_ + + ACTION_REGISTRY.register_reference( + action, + id_, + self._plugin.NAME + ) + + # Add action to menu section = ( main_section if toolbar_id in default_toolbars diff --git a/spyder/plugins/toolbar/plugin.py b/spyder/plugins/toolbar/plugin.py index 19545740bcd..ebe352a2628 100644 --- a/spyder/plugins/toolbar/plugin.py +++ b/spyder/plugins/toolbar/plugin.py @@ -89,7 +89,7 @@ def on_mainwindow_visible(self): container = self.get_container() for toolbar in container.get_application_toolbars(): - toolbar._render() + toolbar.render() container.create_toolbars_menu() container.load_last_visible_toolbars() diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 4d03bdf8390..07c550a693a 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -808,7 +808,7 @@ def do_nothing(): options_button]: self.add_item_to_toolbar(item, toolbar) - toolbar._render() + toolbar.render() # ---- Stack widget (empty) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 2272408852a..a5f2c01b55f 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -1948,7 +1948,7 @@ def set_data_and_check(self, data) -> bool: section=DataframeEditorToolbarSections.ColumnAndRest ) - self.toolbar._render() + self.toolbar.render() return True diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 36108a63727..b99c53bdec4 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -276,7 +276,7 @@ def do_nothing(): ]: self.add_item_to_toolbar(item, self.toolbar) - self.toolbar._render() + self.toolbar.render() def _show_callable_attributes(self, value: bool): """ diff --git a/spyder/plugins/workingdirectory/container.py b/spyder/plugins/workingdirectory/container.py index cb60a3d6adb..58efbbd47d9 100644 --- a/spyder/plugins/workingdirectory/container.py +++ b/spyder/plugins/workingdirectory/container.py @@ -25,6 +25,7 @@ from spyder.api.widgets.main_container import PluginMainContainer from spyder.api.widgets.toolbars import ApplicationToolbar from spyder.config.base import get_home_dir +from spyder.plugins.toolbar.api import ApplicationToolbars from spyder.utils.misc import getcwd_or_home from spyder.utils.stylesheet import APP_TOOLBAR_STYLESHEET from spyder.widgets.comboboxes import PathComboBox @@ -53,10 +54,6 @@ class WorkingDirectoryToolbarItems: # ---- Widgets # ---------------------------------------------------------------------------- -class WorkingDirectoryToolbar(ApplicationToolbar): - ID = 'working_directory_toolbar' - - class WorkingDirectoryComboBox(PathComboBox): """Working directory combo box.""" @@ -188,7 +185,11 @@ def setup(self): # Widgets title = _('Current working directory') - self.toolbar = WorkingDirectoryToolbar(self, title) + self.toolbar = ApplicationToolbar( + self, + ApplicationToolbars.WorkingDirectory, + title + ) self.pathedit = WorkingDirectoryComboBox(self) spacer = WorkingDirectorySpacer(self) diff --git a/spyder/plugins/workingdirectory/tests/test_workingdirectory.py b/spyder/plugins/workingdirectory/tests/test_workingdirectory.py index 08c3d4d7ea8..76308bfd070 100644 --- a/spyder/plugins/workingdirectory/tests/test_workingdirectory.py +++ b/spyder/plugins/workingdirectory/tests/test_workingdirectory.py @@ -41,7 +41,6 @@ def get_plugin(self, plugin, error=True): @pytest.fixture def setup_workingdirectory(qtbot, request, tmpdir): """Setup working directory plugin.""" - CONF.reset_to_defaults() use_startup_wdir = request.node.get_closest_marker('use_startup_wdir') use_cli_wdir = request.node.get_closest_marker('use_cli_wdir') @@ -98,7 +97,6 @@ def test_get_workingdir_startup(setup_workingdirectory): # Asert working directory is the expected one assert folders[-1] == NEW_DIR + '_startup' - CONF.reset_to_defaults() @pytest.mark.use_cli_wdir @@ -115,7 +113,6 @@ def test_get_workingdir_cli(setup_workingdirectory): # Asert working directory is the expected one assert folders[-1] == NEW_DIR + '_cli' - CONF.reset_to_defaults() def test_file_goto(qtbot, setup_workingdirectory): diff --git a/spyder/utils/stylesheet.py b/spyder/utils/stylesheet.py index d96f0d232ff..d551ad4dd5b 100644 --- a/spyder/utils/stylesheet.py +++ b/spyder/utils/stylesheet.py @@ -185,24 +185,23 @@ def _customize_stylesheet(self): spacing='0px', ) - # Remove margins around separators + # Remove margins around separators and decrease size a bit css['QMainWindow::separator:horizontal'].setValues( marginTop='0px', - marginBottom='0px' + marginBottom='0px', + # This is summed to the separator padding (2px) + width="3px", + # Hide image because the default image is not visible at this size + image="none" ) css['QMainWindow::separator:vertical'].setValues( marginLeft='0px', marginRight='0px', - height='3px' - ) - - # TODO: Remove when the editor is migrated to the new API! - css["QMenu::item"].setValues( - height='1.4em', - padding='4px 8px 4px 8px', - fontFamily=font_family, - fontSize=f'{font_size}pt' + # This is summed to the separator padding (2px) + height='3px', + # Hide image because the default image is not visible at this size + image="none" ) # Increase padding for QPushButton's @@ -266,8 +265,7 @@ def _customize_stylesheet(self): minHeight=f'{AppStyle.ComboBoxMinHeight - 0.25}em' ) - # We need to substract here a tiny bit bigger value to match the size - # of comboboxes and lineedits + # Do the same for spinboxes css.QSpinBox.setValues( minHeight=f'{AppStyle.ComboBoxMinHeight - 0.25}em' ) @@ -285,6 +283,27 @@ def _customize_stylesheet(self): left='0px', ) + # Decrease splitter handle size to be a bit smaller than QMainWindow + # separators. + css['QSplitter::handle'].setValues( + padding="0px", + ) + + css['QSplitter::handle:horizontal'].setValues( + width="5px", + image="none" + ) + + css['QSplitter::handle:vertical'].setValues( + height="5px", + image="none" + ) + + # Make splitter handle color match the one of QMainWindow separators + css['QSplitter::handle:hover'].setValues( + backgroundColor=SpyderPalette.COLOR_BACKGROUND_6, + ) + # Add padding to tooltips css.QToolTip.setValues( padding="1px 2px", diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 4135b8ee223..fbba71d0610 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -1654,7 +1654,7 @@ def __init__(self, parent, data, namespacebrowser=None, section=CollectionsEditorToolbarSections.ViewAndRest ) - toolbar._render() + toolbar.render() # Update the toolbar actions state self.editor.refresh_menu()