From 9f651830578d5fbefce07ebaf1f0cbdf1c711f39 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 9 Dec 2023 18:31:01 +0000 Subject: [PATCH 01/21] Change SpyderWidgetMixin.create_toolbar() to create a SpyderToolbar The toolbar is styled as a pane toolbar. This function used to return a QToolBar, but it was not used anywhere. --- spyder/api/widgets/mixins.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index e9b68508581..fcb5829eb50 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -30,12 +30,14 @@ ) from spyder.api.exceptions import SpyderAPIError from spyder.api.widgets.menus import SpyderMenu +from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.manager import CONF from spyder.utils.icon_manager import ima from spyder.utils.image_path_manager import get_image_path from spyder.utils.qthelpers import create_action, create_toolbutton from spyder.utils.registries import ( ACTION_REGISTRY, MENU_REGISTRY, TOOLBAR_REGISTRY, TOOLBUTTON_REGISTRY) +from spyder.utils.stylesheet import PANES_TOOLBAR_STYLESHEET class SpyderToolButtonMixin: @@ -166,11 +168,17 @@ def create_stretcher(self, id_=None): stretcher.ID = id_ return stretcher - def create_toolbar(self, name: str) -> QToolBar: + def create_toolbar(self, name: str) -> SpyderToolbar: """ Create a Spyder toolbar. + + Parameters + ---------- + name: str + Name of the toolbar to create. """ - toolbar = QToolBar(self) + toolbar = SpyderToolbar(self, name) + toolbar.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) TOOLBAR_REGISTRY.register_reference( toolbar, name, self.PLUGIN_NAME, self.CONTEXT_NAME) return toolbar From 7d76a88403fc10cdfcca2745bc2c5201ac44b04b Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 9 Dec 2023 19:02:19 +0000 Subject: [PATCH 02/21] Add `register` parameter to SpyderWidgetMixin.create_xxx() The new parameter is added to the funcions create_menu(), create_toolbar() and create_toolbutton. It indicates whether the menu, toolbar or toolbuttion is added to the global registry. This is to allow the caller to prevent adding temporary menus, toolbars and toolbuttons, such as those in dialog boxes, to the registry forever. --- spyder/api/widgets/mixins.py | 47 ++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index fcb5829eb50..b76ffdd3cec 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -48,7 +48,7 @@ class SpyderToolButtonMixin: def create_toolbutton(self, name, text=None, icon=None, tip=None, toggled=None, triggered=None, autoraise=True, text_beside_icon=False, - section=None, option=None): + section=None, option=None, register=False): """ Create a Spyder toolbutton. """ @@ -74,7 +74,7 @@ def create_toolbutton(self, name, text=None, icon=None, id_=name, plugin=self.PLUGIN_NAME, context_name=self.CONTEXT_NAME, - register_toolbutton=True + register_toolbutton=register ) toolbutton.name = name @@ -168,7 +168,8 @@ def create_stretcher(self, id_=None): stretcher.ID = id_ return stretcher - def create_toolbar(self, name: str) -> SpyderToolbar: + def create_toolbar(self, name: str, + register: bool = True) -> SpyderToolbar: """ Create a Spyder toolbar. @@ -176,11 +177,14 @@ def create_toolbar(self, name: str) -> SpyderToolbar: ---------- name: str Name of the toolbar to create. + register: bool + Whether to register the toolbar in the global registry. """ toolbar = SpyderToolbar(self, name) toolbar.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) - TOOLBAR_REGISTRY.register_reference( - toolbar, name, self.PLUGIN_NAME, self.CONTEXT_NAME) + if register: + TOOLBAR_REGISTRY.register_reference( + toolbar, name, self.PLUGIN_NAME, self.CONTEXT_NAME) return toolbar def get_toolbar(self, name: str, context: Optional[str] = None, @@ -263,6 +267,7 @@ def _create_menu( title: Optional[str] = None, icon: Optional[QIcon] = None, reposition: Optional[bool] = True, + register: bool = True, MenuClass=SpyderMenu ) -> SpyderMenu: """ @@ -274,14 +279,15 @@ def _create_menu( subclass of SpyderMenu. * Refer to the documentation for `create_menu` to learn about its args. """ - menus = getattr(self, '_menus', None) - if menus is None: - self._menus = OrderedDict() + if register: + menus = getattr(self, '_menus', None) + if menus is None: + self._menus = OrderedDict() - if menu_id in self._menus: - raise SpyderAPIError( - 'Menu name "{}" already in use!'.format(menu_id) - ) + if menu_id in self._menus: + raise SpyderAPIError( + 'Menu name "{}" already in use!'.format(menu_id) + ) menu = MenuClass( parent=self, @@ -294,11 +300,12 @@ def _create_menu( menu.menuAction().setIconVisibleInMenu(True) menu.setIcon(icon) - MENU_REGISTRY.register_reference( - menu, menu_id, self.PLUGIN_NAME, self.CONTEXT_NAME - ) + if register: + MENU_REGISTRY.register_reference( + menu, menu_id, self.PLUGIN_NAME, self.CONTEXT_NAME + ) + self._menus[menu_id] = menu - self._menus[menu_id] = menu return menu def create_menu( @@ -307,6 +314,7 @@ def create_menu( title: Optional[str] = None, icon: Optional[QIcon] = None, reposition: Optional[bool] = True, + register: bool = True ) -> SpyderMenu: """ Create a menu for Spyder. @@ -320,7 +328,9 @@ def create_menu( icon: QIcon or None Icon to use for the menu. reposition: bool, optional (default True) - Whether to vertically reposition the menu due to it's padding. + Whether to vertically reposition the menu due to its padding. + register: bool + Whether to register the menu in the global registry. Returns ------- @@ -331,7 +341,8 @@ def create_menu( menu_id=menu_id, title=title, icon=icon, - reposition=reposition + reposition=reposition, + register=register ) def get_menu( From 0f8ad73f95d51968f23b76eef9ba84d42f6fff89 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 9 Dec 2023 19:21:28 +0000 Subject: [PATCH 03/21] Make SpyderWidgetMixin inherit from SpyderToolbarMixin This is to allow dialog boxes to use the mixin to create toolbars. --- spyder/api/widgets/mixins.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index b76ffdd3cec..b65e2f34b6f 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -639,15 +639,12 @@ def update_actions(self, options): raise NotImplementedError('') -class SpyderWidgetMixin(SpyderActionMixin, SpyderMenuMixin, - SpyderConfigurationObserver, SpyderToolButtonMixin): +class SpyderWidgetMixin(SpyderActionMixin, SpyderConfigurationObserver, + SpyderMenuMixin, SpyderToolbarMixin, + SpyderToolButtonMixin): """ Basic functionality for all Spyder widgets and Qt items. - This mixin does not include toolbar handling as that is limited to the - application with the coreui plugin or the PluginMainWidget for dockable - plugins. - This provides a simple management of widget options, as well as Qt helpers for defining the actions a widget provides. """ From c2e437cd0f5c3002ea187187923d398764b24c98 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sun, 10 Dec 2023 21:08:15 +0000 Subject: [PATCH 04/21] Use SpyderWidgetMixin.create_menu in Variable Explorer editors Use the function create_menu from SpyderWidgetMixin to create menus in the array editor, the collections editor, the dataframe editor and the object explorer. This is to standardize the appearance and the code. --- .../variableexplorer/widgets/arrayeditor.py | 5 ++--- .../variableexplorer/widgets/dataframeeditor.py | 15 +++++++-------- .../widgets/objectexplorer/objectexplorer.py | 11 ++++++----- spyder/widgets/collectionseditor.py | 9 ++++----- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 43318b66fb1..a0ae261b9fb 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -37,7 +37,6 @@ # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.api.widgets.comboboxes import SpyderComboBox -from spyder.api.widgets.menus import SpyderMenu from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.base import _ @@ -445,7 +444,7 @@ def setEditorData(self, editor, index): #TODO: Implement "Paste" (from clipboard) feature -class ArrayView(QTableView): +class ArrayView(QTableView, SpyderWidgetMixin): """Array view class""" def __init__(self, parent, model, dtype, shape): QTableView.__init__(self, parent) @@ -544,7 +543,7 @@ def setup_menu(self): icon=ima.icon('editcopy'), triggered=self.copy, context=Qt.WidgetShortcut) - menu = SpyderMenu(self) + menu = self.create_menu('Editor menu', register=False) add_actions(menu, [self.copy_action, ]) return menu diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 6acd935ccca..87c7660d711 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -52,8 +52,7 @@ # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType -from spyder.api.config.mixins import SpyderConfigurationAccessor -from spyder.api.widgets.menus import SpyderMenu +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.base import _ from spyder.py3compat import (is_text_string, is_type_text_string, @@ -562,7 +561,7 @@ def reset(self): self.endResetModel() -class DataFrameView(QTableView, SpyderConfigurationAccessor): +class DataFrameView(QTableView, SpyderWidgetMixin): """ Data Frame view class. @@ -691,7 +690,7 @@ def setup_menu_header(self): triggered=self.edit_header_item ) header_menu = [edit_header_action] - menu = SpyderMenu(self) + menu = self.create_menu('DataFrameView header menu', register=False) add_actions(menu, header_menu) return menu @@ -824,7 +823,7 @@ def setup_menu(self): (_("Float"), float), (_("Str"), to_text_string) ) - convert_to_menu = SpyderMenu(self) + convert_to_menu = self.create_menu('Convert submenu', register=False) self.convert_to_action.setMenu(convert_to_menu) self.convert_to_actions = [] for name, func in functions: @@ -839,7 +838,7 @@ def slot(): ) ] - menu = SpyderMenu(self) + menu = self.create_menu('DataFrameView menu', register=False) add_actions(convert_to_menu, self.convert_to_actions) add_actions(menu, menu_actions) @@ -1605,7 +1604,7 @@ def data(self, index, role): return None -class DataFrameEditor(BaseDialog, SpyderConfigurationAccessor): +class DataFrameEditor(BaseDialog, SpyderWidgetMixin): """ Dialog for displaying and editing DataFrame and related objects. @@ -1797,7 +1796,7 @@ def setup_menu_header(self, header): triggered=lambda: self.edit_header_item(header=header) ) header_menu = [edit_header_action] - menu = SpyderMenu(self) + menu = self.create_menu('Context header menu', register=False) add_actions(menu, header_menu) return menu diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 7a1d79d9014..10ef5411186 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -18,13 +18,12 @@ from qtpy.QtGui import QKeySequence, QTextOption from qtpy.QtWidgets import ( QAbstractItemView, QAction, QButtonGroup, QGroupBox, QHBoxLayout, - QHeaderView, QMenu, QMessageBox, QPushButton, QRadioButton, QSplitter, + QHeaderView, QMessageBox, QPushButton, QRadioButton, QSplitter, QToolButton, QVBoxLayout, QWidget) # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType -from spyder.api.config.mixins import SpyderConfigurationAccessor -from spyder.api.widgets.menus import SpyderMenu +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.base import _ from spyder.config.manager import CONF from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog @@ -45,7 +44,7 @@ EDITOR_NAME = 'Object' -class ObjectExplorer(BaseDialog, SpyderConfigurationAccessor, SpyderFontsMixin): +class ObjectExplorer(BaseDialog, SpyderFontsMixin, SpyderWidgetMixin): """Object explorer main widget window.""" CONF_SECTION = 'variable_explorer' @@ -274,7 +273,9 @@ def _setup_menu(self, show_callable_attributes=False, self.options_button.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) self.options_button.setPopupMode(QToolButton.InstantPopup) - self.show_cols_submenu = SpyderMenu(self) + self.show_cols_submenu = self.create_menu( + 'Options menu', register=False + ) self.options_button.setMenu(self.show_cols_submenu) self.show_cols_submenu.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) self.tools_layout.addWidget(self.options_button) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index d9678ae3a37..d4288b7554f 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -45,8 +45,7 @@ # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType -from spyder.api.config.mixins import SpyderConfigurationAccessor -from spyder.api.widgets.menus import SpyderMenu +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.base import _, running_under_pytest from spyder.py3compat import (is_binary_string, to_text_string, @@ -588,7 +587,7 @@ def sectionResizeEvent(self, logicalIndex, oldSize, newSize): self.sig_user_resized_section.emit(logicalIndex, oldSize, newSize) -class BaseTableView(QTableView, SpyderConfigurationAccessor): +class BaseTableView(QTableView, SpyderWidgetMixin): """Base collection editor table view""" CONF_SECTION = 'variable_explorer' @@ -712,7 +711,7 @@ def setup_menu(self): icon=ima.icon('outline_explorer'), triggered=self.view_item) - menu = SpyderMenu(self) + menu = self.create_menu('Editor menu', register=False) self.menu_actions = [ self.edit_action, self.copy_action, @@ -736,7 +735,7 @@ def setup_menu(self): ] add_actions(menu, self.menu_actions) - self.empty_ws_menu = SpyderMenu(self) + self.empty_ws_menu = self.create_menu('Empty ws', register=False) add_actions( self.empty_ws_menu, [self.insert_action, self.paste_action] From ed9989ac1f5a05a70b48e1967e273c1794107755 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 9 Dec 2023 22:15:54 +0000 Subject: [PATCH 05/21] Use SpyderWidgetMixin.create_toolbar in Variable Explorer editors Use the function create_toolbar from SpyderWidgetMixin to create toolbars and toolbuttons in the array editor, the collections editor, the dataframe editor and the object explorer. This is to standardize the appearance and the code. The biggest change is in the object explorer, where the toolbar is clearly styled differently (and consistent with the rest of Spyder). --- .../variableexplorer/widgets/arrayeditor.py | 5 +- .../widgets/dataframeeditor.py | 5 +- .../widgets/objectexplorer/objectexplorer.py | 65 ++++++++++--------- spyder/widgets/collectionseditor.py | 8 +-- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index a0ae261b9fb..591a5155a8a 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -38,7 +38,6 @@ from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.api.widgets.comboboxes import SpyderComboBox from spyder.api.widgets.mixins import SpyderWidgetMixin -from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.base import _ from spyder.config.manager import CONF from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog @@ -47,7 +46,6 @@ from spyder.utils.icon_manager import ima from spyder.utils.qthelpers import ( add_actions, create_action, keybinding, safe_disconnect) -from spyder.utils.stylesheet import PANES_TOOLBAR_STYLESHEET class ArrayEditorActions: @@ -716,8 +714,7 @@ def setup_ui(self, title='', readonly=False): # ---- Toolbar and actions - toolbar = SpyderToolbar(parent=self, title='Editor toolbar') - toolbar.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) + toolbar = self.create_toolbar('Editor toolbar', register=False) def do_nothing(): # .create_action() needs a toggled= parameter, but we can only diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 87c7660d711..97c086726f0 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -53,7 +53,6 @@ # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.api.widgets.mixins import SpyderWidgetMixin -from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.base import _ from spyder.py3compat import (is_text_string, is_type_text_string, to_text_string) @@ -64,7 +63,6 @@ from spyder.plugins.variableexplorer.widgets.arrayeditor import get_idx_rect from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import QStylePalette -from spyder.utils.stylesheet import PANES_TOOLBAR_STYLESHEET # Supported real and complex number types @@ -1716,8 +1714,7 @@ def setup_ui(self, title: str) -> None: btn_layout.setContentsMargins(0, 16, 0, 16) self.glayout.addLayout(btn_layout, 4, 0, 1, 2) - self.toolbar = SpyderToolbar(parent=None, title='Editor toolbar') - self.toolbar.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) + self.toolbar = self.create_toolbar('Editor toolbar', register=False) self.layout.addWidget(self.toolbar) self.layout.addLayout(self.glayout) diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 10ef5411186..c63407a90db 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -31,9 +31,7 @@ DEFAULT_ATTR_COLS, DEFAULT_ATTR_DETAILS, ToggleColumnTreeView, TreeItem, TreeModel, TreeProxyModel) from spyder.utils.icon_manager import ima -from spyder.utils.qthelpers import ( - add_actions, create_toolbutton, qapplication, safe_disconnect) -from spyder.utils.stylesheet import PANES_TOOLBAR_STYLESHEET +from spyder.utils.qthelpers import add_actions, qapplication, safe_disconnect from spyder.widgets.simplecodeeditor import SimpleCodeEditor @@ -235,50 +233,59 @@ def _setup_actions(self): def _setup_menu(self, show_callable_attributes=False, show_special_attributes=False): """Sets up the main menu.""" - self.tools_layout = QHBoxLayout() + self.toolbar = self.create_toolbar( + 'Object explorer toolbar', register=False + ) - callable_attributes = create_toolbutton( - self, text=_("Show callable attributes"), + callable_attributes = self.create_toolbutton( + name='Show callable toolbutton', + text=_("Show callable attributes"), icon=ima.icon("class"), - toggled=self._toggle_show_callable_attributes_action) + toggled=self._toggle_show_callable_attributes_action, + register=False + ) callable_attributes.setCheckable(True) callable_attributes.setChecked(show_callable_attributes) - callable_attributes.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) - self.tools_layout.addWidget(callable_attributes) + self.toolbar.add_item(callable_attributes) - special_attributes = create_toolbutton( - self, text=_("Show __special__ attributes"), + special_attributes = self.create_toolbutton( + name='Show special toolbutton', + text=_("Show __special__ attributes"), icon=ima.icon("private2"), - toggled=self._toggle_show_special_attributes_action) + toggled=self._toggle_show_special_attributes_action, + register=False + ) special_attributes.setCheckable(True) special_attributes.setChecked(show_special_attributes) - special_attributes.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) - self.tools_layout.addSpacing(5) - self.tools_layout.addWidget(special_attributes) + self.toolbar.add_item(special_attributes) - self.refresh_button = create_toolbutton( - self, icon=ima.icon('refresh'), + self.refresh_button = self.create_toolbutton( + name='Refresh toolbutton', + icon=ima.icon('refresh'), tip=_('Refresh editor with current value of variable in console'), - triggered=self.refresh_editor + triggered=self.refresh_editor, + register=False ) self.refresh_button.setEnabled(self.data_function is not None) - self.refresh_button.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) - self.tools_layout.addSpacing(5) - self.tools_layout.addWidget(self.refresh_button) + self.toolbar.add_item(self.refresh_button) - self.tools_layout.addStretch() + stretcher = self.create_stretcher('Toolbar stretcher') + self.toolbar.add_item(stretcher) - self.options_button = create_toolbutton( - self, text=_('Options'), icon=ima.icon('tooloptions')) - self.options_button.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) + self.options_button = self.create_toolbutton( + name='Options toolbutton', + text=_('Options'), + icon=ima.icon('tooloptions'), + register=False + ) self.options_button.setPopupMode(QToolButton.InstantPopup) self.show_cols_submenu = self.create_menu( 'Options menu', register=False ) self.options_button.setMenu(self.show_cols_submenu) - self.show_cols_submenu.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) - self.tools_layout.addWidget(self.options_button) + self.toolbar.add_item(self.options_button) + self.toolbar._render() @Slot() def _toggle_show_callable_attributes_action(self): @@ -298,10 +305,8 @@ def _toggle_show_special_attributes_action(self): def _setup_views(self): """Creates the UI widgets.""" layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addLayout(self.tools_layout) + layout.addWidget(self.toolbar) self.central_splitter = QSplitter(self, orientation=Qt.Vertical) layout.addWidget(self.central_splitter) self.setLayout(layout) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index d4288b7554f..2f224e9a23d 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -46,7 +46,6 @@ # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.api.widgets.mixins import SpyderWidgetMixin -from spyder.api.widgets.toolbars import SpyderToolbar from spyder.config.base import _, running_under_pytest from spyder.py3compat import (is_binary_string, to_text_string, is_type_text_string) @@ -61,7 +60,6 @@ from spyder.widgets.helperwidgets import CustomSortFilterProxy from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import SpyderPalette -from spyder.utils.stylesheet import PANES_TOOLBAR_STYLESHEET # Maximum length of a serialized variable to be set in the kernel @@ -1446,7 +1444,7 @@ def set_filter(self, dictfilter=None): self.dictfilter = dictfilter -class CollectionsEditorWidget(QWidget): +class CollectionsEditorWidget(QWidget, SpyderWidgetMixin): """Dictionary Editor Widget""" sig_refresh_requested = Signal() @@ -1463,9 +1461,7 @@ def __init__(self, parent, data, namespacebrowser=None, self, data, namespacebrowser, data_function, readonly, title ) - toolbar = SpyderToolbar(parent=None, title='Editor toolbar') - toolbar.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) - + toolbar = self.create_toolbar('Editor toolbar', register=False) for item in self.editor.menu_actions: if item is not None: toolbar.addAction(item) From 4472b0b1d390ea39c5bc77b0a9ae44d63a09e0f0 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Mon, 18 Dec 2023 22:43:36 +0000 Subject: [PATCH 06/21] Use SpyderWidgetMixin.create_action in Variable Explorer editors Use the function create_action from SpyderWidgetMixin to create actions in the array editor, the collections editor, the dataframe editor and the object explorer. This is to standardize the UX and the code. This does remove the keyboard shortcuts for the actions in the object explorer, but these shortcuts were not shown anywhere so users had little change to discover them. This means that users are unlikely to miss the shortcuts. --- .../variableexplorer/widgets/arrayeditor.py | 17 +- .../widgets/dataframeeditor.py | 160 +++++++++++------- .../widgets/objectexplorer/objectexplorer.py | 39 +++-- spyder/widgets/collectionseditor.py | 160 ++++++++++++------ 4 files changed, 232 insertions(+), 144 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 591a5155a8a..0b7a8baf081 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -44,8 +44,7 @@ from spyder.py3compat import (is_binary_string, is_string, is_text_string, to_binary_string, to_text_string) from spyder.utils.icon_manager import ima -from spyder.utils.qthelpers import ( - add_actions, create_action, keybinding, safe_disconnect) +from spyder.utils.qthelpers import add_actions, keybinding, safe_disconnect class ArrayEditorActions: @@ -536,11 +535,15 @@ def resize_to_contents(self): def setup_menu(self): """Setup context menu""" - self.copy_action = create_action(self, _('Copy'), - shortcut=keybinding('Copy'), - icon=ima.icon('editcopy'), - triggered=self.copy, - context=Qt.WidgetShortcut) + self.copy_action = self.create_action( + name=None, + text=_('Copy'), + icon=ima.icon('editcopy'), + triggered=self.copy, + register_action=False + ) + self.copy_action.setShortcut(keybinding('Copy')) + self.copy_action.setShortcutContext(Qt.WidgetShortcut) menu = self.create_menu('Editor menu', register=False) add_actions(menu, [self.copy_action, ]) return menu diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 97c086726f0..9ef2e5853fd 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -58,8 +58,7 @@ to_text_string) from spyder.utils.icon_manager import ima from spyder.utils.qthelpers import ( - add_actions, create_action, keybinding, MENU_SEPARATOR, qapplication, - safe_disconnect) + add_actions, keybinding, MENU_SEPARATOR, qapplication, safe_disconnect) from spyder.plugins.variableexplorer.widgets.arrayeditor import get_idx_rect from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import QStylePalette @@ -595,7 +594,7 @@ def __init__(self, parent, model, header, hscroll, vscroll, self.remove_col_action = None self.duplicate_row_action = None self.duplicate_col_action = None - self.convert_to_action = None + self.convert_to_menu = None self.refresh_action = None self.setModel(model) @@ -682,10 +681,12 @@ def contextMenuEvent(self, event): def setup_menu_header(self): """Setup context header menu.""" - edit_header_action = create_action( - self, _("Edit"), + edit_header_action = self.create_action( + name=None, + text=_("Edit"), icon=ima.icon('edit'), - triggered=self.edit_header_item + triggered=self.edit_header_item, + register_action=False ) header_menu = [edit_header_action] menu = self.create_menu('DataFrameView header menu', register=False) @@ -705,7 +706,7 @@ def refresh_menu(self): for action in [self.edit_action, self.insert_action_above, self.insert_action_below, self.insert_action_after, self.insert_action_before, self.duplicate_row_action, - self.duplicate_col_action, self.convert_to_action]: + self.duplicate_col_action]: action.setEnabled(condition_edit) # Enable/disable actions for remove col/row and copy @@ -720,78 +721,107 @@ def refresh_menu(self): def setup_menu(self): """Setup context menu.""" - resize_action = create_action( - self, _("Resize rows to contents"), + resize_action = self.create_action( + name=None, + text=_("Resize rows to contents"), icon=ima.icon('collapse_row'), - triggered=lambda: self.resize_to_contents(rows=True) + triggered=lambda: self.resize_to_contents(rows=True), + register_action=False ) - resize_columns_action = create_action( - self, - _("Resize columns to contents"), + resize_columns_action = self.create_action( + name=None, + text=_("Resize columns to contents"), icon=ima.icon('collapse_column'), - triggered=self.resize_to_contents) - self.edit_action = create_action( - self, _("Edit"), + triggered=self.resize_to_contents, + register_action=False + ) + self.edit_action = self.create_action( + name=None, + text=_("Edit"), icon=ima.icon('edit'), - triggered=self.edit_item + triggered=self.edit_item, + register_action=False ) - self.insert_action_above = create_action( - self, _("Insert above"), + self.insert_action_above = self.create_action( + name=None, + text=_("Insert above"), icon=ima.icon('insert_above'), - triggered=lambda: self.insert_item(axis=1, before_above=True) + triggered=lambda: self.insert_item(axis=1, before_above=True), + register_action=False ) - self.insert_action_below = create_action( - self, _("Insert below"), + self.insert_action_below = self.create_action( + name=None, + text=_("Insert below"), icon=ima.icon('insert_below'), - triggered=lambda: self.insert_item(axis=1, before_above=False) + triggered=lambda: self.insert_item(axis=1, before_above=False), + register_action=False ) - self.insert_action_before = create_action( - self, _("Insert before"), + self.insert_action_before = self.create_action( + name=None, + text=_("Insert before"), icon=ima.icon('insert_before'), - triggered=lambda: self.insert_item(axis=0, before_above=True) + triggered=lambda: self.insert_item(axis=0, before_above=True), + register_action=False ) - self.insert_action_after = create_action( - self, _("Insert after"), + self.insert_action_after = self.create_action( + name=None, + text=_("Insert after"), icon=ima.icon('insert_after'), - triggered=lambda: self.insert_item(axis=0, before_above=False) + triggered=lambda: self.insert_item(axis=0, before_above=False), + register_action=False ) - self.remove_row_action = create_action( - self, _("Remove row"), + self.remove_row_action = self.create_action( + name=None, + text=_("Remove row"), icon=ima.icon('delete_row'), - triggered=self.remove_item + triggered=self.remove_item, + register_action=False ) - self.remove_col_action = create_action( - self, _("Remove column"), + self.remove_col_action = self.create_action( + name=None, + text=_("Remove column"), icon=ima.icon('delete_column'), - triggered=lambda: - self.remove_item(axis=1) + triggered=lambda: self.remove_item(axis=1), + register_action=False ) - self.duplicate_row_action = create_action( - self, _("Duplicate row"), + self.duplicate_row_action = self.create_action( + name=None, + text=_("Duplicate row"), icon=ima.icon('duplicate_row'), - triggered=lambda: self.duplicate_row_col(dup_row=True) + triggered=lambda: self.duplicate_row_col(dup_row=True), + register_action=False ) - self.duplicate_col_action = create_action( - self, _("Duplicate column"), + self.duplicate_col_action = self.create_action( + name=None, + text=_("Duplicate column"), icon=ima.icon('duplicate_column'), - triggered=lambda: self.duplicate_row_col(dup_row=False) + triggered=lambda: self.duplicate_row_col(dup_row=False), + register_action=False ) - self.copy_action = create_action( - self, _('Copy'), - shortcut=keybinding('Copy'), + self.copy_action = self.create_action( + name=None, + text=_('Copy'), icon=ima.icon('editcopy'), triggered=self.copy, - context=Qt.WidgetShortcut + register_action=False ) - self.refresh_action = create_action( - self, _('Refresh'), + self.copy_action.setShortcut(keybinding('Copy')) + self.copy_action.setShortcutContext(Qt.WidgetShortcut) + self.refresh_action = self.create_action( + name=None, + text=_('Refresh'), icon=ima.icon('refresh'), tip=_('Refresh editor with current value of variable in console'), - triggered=lambda: self.sig_refresh_requested.emit() + triggered=lambda: self.sig_refresh_requested.emit(), + register_action=False ) self.refresh_action.setEnabled(self.data_function is not None) - self.convert_to_action = create_action(self, _('Convert to')) + self.convert_to_menu = self.create_menu( + menu_id='Convert submenu', + title=_('Convert to'), + register=False + ) menu_actions = [ self.edit_action, self.copy_action, @@ -805,7 +835,7 @@ def setup_menu(self): self.duplicate_row_action, self.duplicate_col_action, MENU_SEPARATOR, - self.convert_to_action, + self.convert_to_menu.menuAction(), MENU_SEPARATOR, resize_action, resize_columns_action, @@ -821,23 +851,22 @@ def setup_menu(self): (_("Float"), float), (_("Str"), to_text_string) ) - convert_to_menu = self.create_menu('Convert submenu', register=False) - self.convert_to_action.setMenu(convert_to_menu) self.convert_to_actions = [] - for name, func in functions: + for text, func in functions: def slot(): self.change_type(func) self.convert_to_actions += [ - create_action( - self, - name, + self.create_action( + name=None, + text=text, triggered=slot, - context=Qt.WidgetShortcut + context=Qt.WidgetShortcut, + register_action=False ) ] menu = self.create_menu('DataFrameView menu', register=False) - add_actions(convert_to_menu, self.convert_to_actions) + add_actions(self.convert_to_menu, self.convert_to_actions) add_actions(menu, menu_actions) return menu @@ -1634,7 +1663,7 @@ def __init__( def setup_and_check(self, data, title='') -> bool: """ Setup editor. - + It returns False if data is not supported, True otherwise. Supported types for data are DataFrame, Series and Index. """ @@ -1772,7 +1801,7 @@ def set_data_and_check(self, data) -> bool: self.toolbar.clear() for item in self.dataTable.menu_actions: if item is not None: - if item.text() != 'Convert to': + if item.text() != _('Convert to'): self.toolbar.addAction(item) return True @@ -1786,11 +1815,12 @@ def save_and_close_enable(self, top_left, bottom_right): def setup_menu_header(self, header): """Setup context header menu.""" - edit_header_action = create_action( - self, - _("Edit"), + edit_header_action = self.create_action( + name=None, + text=_("Edit"), icon=ima.icon('edit'), - triggered=lambda: self.edit_header_item(header=header) + triggered=lambda: self.edit_header_item(header=header), + register_action=False ) header_menu = [edit_header_action] menu = self.create_menu('Context header menu', register=False) diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index c63407a90db..3bad925ed2b 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -15,11 +15,11 @@ # Third-party imports from qtpy.QtCore import Slot, QModelIndex, QPoint, QSize, Qt -from qtpy.QtGui import QKeySequence, QTextOption +from qtpy.QtGui import QTextOption from qtpy.QtWidgets import ( - QAbstractItemView, QAction, QButtonGroup, QGroupBox, QHBoxLayout, - QHeaderView, QMessageBox, QPushButton, QRadioButton, QSplitter, - QToolButton, QVBoxLayout, QWidget) + QAbstractItemView, QButtonGroup, QGroupBox, QHBoxLayout, QHeaderView, + QMessageBox, QPushButton, QRadioButton, QSplitter, QToolButton, + QVBoxLayout, QWidget) # Local imports from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType @@ -212,22 +212,27 @@ def _make_show_column_function(self, column_idx): def _setup_actions(self): """Creates the main window actions.""" + def do_nothing(): + # .create_action() needs a toggled= parameter, but we can only + # set it later in the set_value method, so we use this function as + # a placeholder here. + pass + # Show/hide callable objects - self.toggle_show_callable_action = QAction( - _("Show callable attributes"), - self, - checkable=True, - shortcut=QKeySequence("Alt+C"), - statusTip=_("Shows/hides attributes that are callable " - "(functions, methods, etc)") + self.toggle_show_callable_action = self.create_action( + name=None, + text=_("Show callable attributes"), + toggled=do_nothing, + tip=_("Shows/hides attributes that are callable " + "(functions, methods etc)") ) + # Show/hide special attributes - self.toggle_show_special_attribute_action = QAction( - _("Show __special__ attributes"), - self, - checkable=True, - shortcut=QKeySequence("Alt+S"), - statusTip=_("Shows or hides __special__ attributes") + self.toggle_show_special_attribute_action = self.create_action( + name=None, + text=_("Show __special__ attributes"), + toggled=do_nothing, + tip=_("Shows or hides __special__ attributes") ) def _setup_menu(self, show_callable_attributes=False, diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 2f224e9a23d..a55c522c3d3 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -51,8 +51,7 @@ is_type_text_string) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home -from spyder.utils.qthelpers import ( - add_actions, create_action, MENU_SEPARATOR, mimedata2url) +from spyder.utils.qthelpers import add_actions, MENU_SEPARATOR, mimedata2url from spyder.utils.stringmatching import get_search_scores, get_search_regex from spyder.plugins.variableexplorer.widgets.collectionsdelegate import ( CollectionsDelegate) @@ -642,72 +641,122 @@ def setup_table(self): def setup_menu(self): """Setup context menu""" - resize_action = create_action(self, _("Resize rows to contents"), - icon=ima.icon('collapse_row'), - triggered=self.resizeRowsToContents) - resize_columns_action = create_action( - self, - _("Resize columns to contents"), + resize_action = self.create_action( + name=None, + text=_("Resize rows to contents"), + icon=ima.icon('collapse_row'), + triggered=self.resizeRowsToContents, + register_action=False + ) + resize_columns_action = self.create_action( + name=None, + text=_("Resize columns to contents"), icon=ima.icon('collapse_column'), - triggered=self.resize_column_contents) - self.paste_action = create_action(self, _("Paste"), - icon=ima.icon('editpaste'), - triggered=self.paste) - self.copy_action = create_action(self, _("Copy"), - icon=ima.icon('editcopy'), - triggered=self.copy) - self.edit_action = create_action(self, _("Edit"), - icon=ima.icon('edit'), - triggered=self.edit_item) - self.plot_action = create_action( - self, _("Plot"), + triggered=self.resize_column_contents, + register_action=False + ) + self.paste_action = self.create_action( + name=None, + text=_("Paste"), + icon=ima.icon('editpaste'), + triggered=self.paste, + register_action=False + ) + self.copy_action = self.create_action( + name=None, + text=_("Copy"), + icon=ima.icon('editcopy'), + triggered=self.copy, + register_action=False + ) + self.edit_action = self.create_action( + name=None, + text=_("Edit"), + icon=ima.icon('edit'), + triggered=self.edit_item, + register_action=False + ) + self.plot_action = self.create_action( + name=None, + text=_("Plot"), icon=ima.icon('plot'), - triggered=lambda: self.plot_item('plot') + triggered=lambda: self.plot_item('plot'), + register_action=False ) self.plot_action.setVisible(False) - self.hist_action = create_action( - self, _("Histogram"), + self.hist_action = self.create_action( + name=None, + text=_("Histogram"), icon=ima.icon('hist'), - triggered=lambda: self.plot_item('hist') + triggered=lambda: self.plot_item('hist'), + register_action=False ) self.hist_action.setVisible(False) - self.imshow_action = create_action(self, _("Show image"), - icon=ima.icon('imshow'), - triggered=self.imshow_item) + self.imshow_action = self.create_action( + name=None, + text=_("Show image"), + icon=ima.icon('imshow'), + triggered=self.imshow_item, + register_action=False + ) self.imshow_action.setVisible(False) - self.save_array_action = create_action(self, _("Save array"), - icon=ima.icon('filesave'), - triggered=self.save_array) + self.save_array_action = self.create_action( + name=None, + text=_("Save array"), + icon=ima.icon('filesave'), + triggered=self.save_array, + register_action=False + ) self.save_array_action.setVisible(False) - self.insert_action = create_action( - self, _("Insert"), + self.insert_action = self.create_action( + name=None, + text=_("Insert"), icon=ima.icon('insert'), - triggered=lambda: self.insert_item(below=False) + triggered=lambda: self.insert_item(below=False), + register_action=False ) - self.insert_action_above = create_action( - self, _("Insert above"), + self.insert_action_above = self.create_action( + name=None, + text=_("Insert above"), icon=ima.icon('insert_above'), - triggered=lambda: self.insert_item(below=False) + triggered=lambda: self.insert_item(below=False), + register_action=False ) - self.insert_action_below = create_action( - self, _("Insert below"), + self.insert_action_below = self.create_action( + name=None, + text=_("Insert below"), icon=ima.icon('insert_below'), - triggered=lambda: self.insert_item(below=True) + triggered=lambda: self.insert_item(below=True), + register_action=False ) - self.remove_action = create_action(self, _("Remove"), - icon=ima.icon('editdelete'), - triggered=self.remove_item) - self.rename_action = create_action(self, _("Rename"), - icon=ima.icon('rename'), - triggered=self.rename_item) - self.duplicate_action = create_action(self, _("Duplicate"), - icon=ima.icon('edit_add'), - triggered=self.duplicate_item) - self.view_action = create_action( - self, - _("View with the Object Explorer"), + self.remove_action = self.create_action( + name=None, + text=_("Remove"), + icon=ima.icon('editdelete'), + triggered=self.remove_item, + register_action=False + ) + self.rename_action = self.create_action( + name=None, + text=_("Rename"), + icon=ima.icon('rename'), + triggered=self.rename_item, + register_action=False + ) + self.duplicate_action = self.create_action( + name=None, + text=_("Duplicate"), + icon=ima.icon('edit_add'), + triggered=self.duplicate_item, + register_action=False + ) + self.view_action = self.create_action( + name=None, + text=_("View with the Object Explorer"), icon=ima.icon('outline_explorer'), - triggered=self.view_item) + triggered=self.view_item, + register_action=False + ) menu = self.create_menu('Editor menu', register=False) self.menu_actions = [ @@ -1466,12 +1515,13 @@ def __init__(self, parent, data, namespacebrowser=None, if item is not None: toolbar.addAction(item) - self.refresh_action = create_action( - self, + self.refresh_action = self.create_action( + name=None, text=_('Refresh'), icon=ima.icon('refresh'), tip=_('Refresh editor with current value of variable in console'), - triggered=lambda: self.sig_refresh_requested.emit() + triggered=lambda: self.sig_refresh_requested.emit(), + register_action=None ) toolbar.addAction(self.refresh_action) From 74246769f0acb122037bd5cd24ee8414206225bc Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sat, 23 Dec 2023 18:06:32 +0000 Subject: [PATCH 07/21] Simplify implementation of "Show xxx attributes" in object explorer Simplify "Show callable attributes" and "Show __special__ attributes" by: * adding the actions to the toolbar, instead of first creating a toolbutton and then adding that to the toolbar, * adding a new function to connect the action to and connect only once, instead of disconnecting and re-connecting every time that the value in the editor changes, * using the functions in SpyderWidgetMixin to handle the configuration option. --- .../widgets/objectexplorer/objectexplorer.py | 119 +++++++----------- 1 file changed, 42 insertions(+), 77 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 3bad925ed2b..cae95563895 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -25,13 +25,12 @@ from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.base import _ -from spyder.config.manager import CONF from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.plugins.variableexplorer.widgets.objectexplorer import ( DEFAULT_ATTR_COLS, DEFAULT_ATTR_DETAILS, ToggleColumnTreeView, TreeItem, TreeModel, TreeProxyModel) from spyder.utils.icon_manager import ima -from spyder.utils.qthelpers import add_actions, qapplication, safe_disconnect +from spyder.utils.qthelpers import add_actions, qapplication from spyder.widgets.simplecodeeditor import SimpleCodeEditor @@ -78,10 +77,6 @@ def __init__(self, super().__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) - # Options - show_callable_attributes = self.get_conf('show_callable_attributes') - show_special_attributes = self.get_conf('show_special_attributes') - # Model self.name = name self.expanded = expanded @@ -92,13 +87,12 @@ def __init__(self, self.readonly = readonly self.obj_tree = None + self._proxy_tree_model = None self.btn_save_and_close = None self.btn_close = None # Views - self._setup_actions() - self._setup_menu(show_callable_attributes=show_callable_attributes, - show_special_attributes=show_special_attributes) + self._setup_toolbar() self._setup_views() if self.name: self.setWindowTitle(f'{self.name} - {EDITOR_NAME}') @@ -112,11 +106,6 @@ def __init__(self, self._resize_to_contents = resize_to_contents self._readViewSettings(reset=reset) - # Update views with model - self.toggle_show_special_attribute_action.setChecked( - show_special_attributes) - self.toggle_show_callable_action.setChecked(show_callable_attributes) - def get_value(self): """Get editor current object state.""" return self._tree_model.inspectedItem.obj @@ -150,19 +139,6 @@ def set_value(self, obj): self.obj_tree.setUniformRowHeights(True) self.obj_tree.add_header_context_menu() - # Connect signals - safe_disconnect(self.toggle_show_callable_action.toggled) - self.toggle_show_callable_action.toggled.connect( - self._proxy_tree_model.setShowCallables) - self.toggle_show_callable_action.toggled.connect( - self.obj_tree.resize_columns_to_contents) - - safe_disconnect(self.toggle_show_special_attribute_action.toggled) - self.toggle_show_special_attribute_action.toggled.connect( - self._proxy_tree_model.setShowSpecialAttributes) - self.toggle_show_special_attribute_action.toggled.connect( - self.obj_tree.resize_columns_to_contents) - # Keep a temporary reference of the selection_model to prevent # segfault in PySide. # See http://permalink.gmane.org/gmane.comp.lib.qt.pyside.devel/222 @@ -210,59 +186,45 @@ def _make_show_column_function(self, column_idx): column_idx, not checked) return show_column - def _setup_actions(self): - """Creates the main window actions.""" + def _setup_toolbar(self, show_callable_attributes=False, + show_special_attributes=False): + """ + Sets up the toolbar and the actions in it. + """ def do_nothing(): # .create_action() needs a toggled= parameter, but we can only # set it later in the set_value method, so we use this function as # a placeholder here. pass - # Show/hide callable objects - self.toggle_show_callable_action = self.create_action( - name=None, - text=_("Show callable attributes"), - toggled=do_nothing, - tip=_("Shows/hides attributes that are callable " - "(functions, methods etc)") - ) - - # Show/hide special attributes - self.toggle_show_special_attribute_action = self.create_action( - name=None, - text=_("Show __special__ attributes"), - toggled=do_nothing, - tip=_("Shows or hides __special__ attributes") - ) - - def _setup_menu(self, show_callable_attributes=False, - show_special_attributes=False): - """Sets up the main menu.""" self.toolbar = self.create_toolbar( 'Object explorer toolbar', register=False ) - callable_attributes = self.create_toolbutton( - name='Show callable toolbutton', + # Show/hide callable objects + self.toggle_show_callable_action = self.create_action( + name='Show callable attributes', text=_("Show callable attributes"), icon=ima.icon("class"), - toggled=self._toggle_show_callable_attributes_action, - register=False + tip=_("Shows/hides attributes that are callable " + "(functions, methods etc)"), + toggled=self._show_callable_attributes, + option='show_callable_attributes', + register_action=False ) - callable_attributes.setCheckable(True) - callable_attributes.setChecked(show_callable_attributes) - self.toolbar.add_item(callable_attributes) + self.toolbar.add_item(self.toggle_show_callable_action) - special_attributes = self.create_toolbutton( - name='Show special toolbutton', + # Show/hide special attributes + self.toggle_show_special_attribute_action = self.create_action( + name='Show special attributes', text=_("Show __special__ attributes"), icon=ima.icon("private2"), - toggled=self._toggle_show_special_attributes_action, - register=False + tip=_("Shows or hides __special__ attributes"), + toggled=self._show_special_attributes, + option='show_special_attributes', + register_action=False ) - special_attributes.setCheckable(True) - special_attributes.setChecked(show_special_attributes) - self.toolbar.add_item(special_attributes) + self.toolbar.add_item(self.toggle_show_special_attribute_action) self.refresh_button = self.create_toolbutton( name='Refresh toolbutton', @@ -292,20 +254,23 @@ def _setup_menu(self, show_callable_attributes=False, self.toolbar.add_item(self.options_button) self.toolbar._render() - @Slot() - def _toggle_show_callable_attributes_action(self): - """Toggle show callable atributes action.""" - action_checked = not self.toggle_show_callable_action.isChecked() - self.toggle_show_callable_action.setChecked(action_checked) - self.set_conf('show_callable_attributes', action_checked) + def _show_callable_attributes(self, value: bool): + """ + Called when user toggles "show special attributes" option. + """ + if self._proxy_tree_model: + self._proxy_tree_model.setShowCallables(value) + if self.obj_tree: + self.obj_tree.resize_columns_to_contents() - @Slot() - def _toggle_show_special_attributes_action(self): - """Toggle show special attributes action.""" - action_checked = ( - not self.toggle_show_special_attribute_action.isChecked()) - self.toggle_show_special_attribute_action.setChecked(action_checked) - self.set_conf('show_special_attributes', action_checked) + def _show_special_attributes(self, value: bool): + """ + Called when user toggles "show callable attributes" option. + """ + if self._proxy_tree_model: + self._proxy_tree_model.setShowSpecialAttributes(value) + if self.obj_tree: + self.obj_tree.resize_columns_to_contents() def _setup_views(self): """Creates the UI widgets.""" @@ -480,7 +445,7 @@ def _update_details_for_item(self, tree_item): self.editor.setup_editor( font=self.get_font(SpyderFontType.MonospaceInterface), show_blanks=False, - color_scheme=CONF.get('appearance', 'selected'), + color_scheme=self.get_conf('selected', section='appearance'), scroll_past_end=False, ) self.editor.set_text(data) From 687ee1160de48ee177e84d7d9590881d985b4029 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sun, 24 Dec 2023 17:28:08 +0000 Subject: [PATCH 08/21] Change layout of editor windows in Variable Explorer - Remove verticle space between toolbar and editor - Align right edge of editor and buttons at bottom - Remove extra spacing around buttons at bottom --- .../variableexplorer/widgets/arrayeditor.py | 30 +++++++------ .../widgets/dataframeeditor.py | 36 +++++++++++----- .../widgets/objectexplorer/objectexplorer.py | 42 +++++++++++-------- spyder/widgets/collectionseditor.py | 12 ++++-- 4 files changed, 74 insertions(+), 46 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 0b7a8baf081..7b8545f3677 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -24,10 +24,9 @@ QItemSelectionRange, QModelIndex, Qt, Slot) from qtpy.QtGui import QColor, QCursor, QDoubleValidator, QKeySequence from qtpy.QtWidgets import ( - QAbstractItemDelegate, QApplication, QDialog, QGridLayout, - QHBoxLayout, QInputDialog, QItemDelegate, QLabel, QLineEdit, - QMessageBox, QPushButton, QSpinBox, QStackedWidget, QTableView, - QVBoxLayout, QWidget) + QAbstractItemDelegate, QApplication, QDialog, QHBoxLayout, QInputDialog, + QItemDelegate, QLabel, QLineEdit, QMessageBox, QPushButton, QSpinBox, + QStackedWidget, QStyle, QTableView, QVBoxLayout, QWidget) from spyder_kernels.utils.nsview import value_to_display from spyder_kernels.utils.lazymodules import numpy as np @@ -45,6 +44,7 @@ to_binary_string, to_text_string) from spyder.utils.icon_manager import ima from spyder.utils.qthelpers import add_actions, keybinding, safe_disconnect +from spyder.utils.stylesheet import AppStyle, MAC class ArrayEditorActions: @@ -622,6 +622,7 @@ def __init__(self, parent, data, readonly=False): layout = QVBoxLayout() layout.addWidget(self.view) + layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def accept_changes(self): @@ -688,7 +689,6 @@ def __init__( self.data = None self.arraywidget = None self.stack = None - self.layout = None self.btn_save_and_close = None self.btn_close = None # Values for 3d array editor @@ -712,9 +712,6 @@ def setup_ui(self, title='', readonly=False): interface of the array editor. Some elements need to be hidden depending on the data; this will be done when the data is set. """ - self.layout = QGridLayout() - self.setLayout(self.layout) - # ---- Toolbar and actions toolbar = self.create_toolbar('Editor toolbar', register=False) @@ -772,13 +769,11 @@ def do_nothing(): toolbar.add_item(self.refresh_action) toolbar._render() - self.layout.addWidget(toolbar, 0, 0) # ---- Stack widget (empty) self.stack = QStackedWidget(self) self.stack.currentChanged.connect(self.current_widget_changed) - self.layout.addWidget(self.stack, 1, 0) # ---- Widgets in bottom left for special arrays # @@ -834,9 +829,18 @@ def do_nothing(): # ---- Final layout - # Add bottom row of widgets - self.btn_layout.setContentsMargins(4, 4, 4, 4) - self.layout.addLayout(self.btn_layout, 2, 0) + layout = QVBoxLayout() + layout.addWidget(toolbar) + + # Remove vertical space between toolbar and table containing array + style = self.style() + default_spacing = style.pixelMetric(QStyle.PM_LayoutVerticalSpacing) + layout.addSpacing(-default_spacing) + + layout.addWidget(self.stack) + layout.addSpacing((-1 if MAC else 2) * AppStyle.MarginSize) + layout.addLayout(self.btn_layout) + self.setLayout(layout) # Set title if title: diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 9ef2e5853fd..237de9b8936 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -47,7 +47,7 @@ from qtpy.QtWidgets import ( QApplication, QCheckBox, QDialog, QFrame, QGridLayout, QHBoxLayout, QInputDialog, QItemDelegate, QLabel, QLineEdit, QMessageBox, QPushButton, - QScrollBar, QTableView, QTableWidget, QVBoxLayout, QWidget) + QScrollBar, QStyle, QTableView, QTableWidget, QVBoxLayout, QWidget) from spyder_kernels.utils.lazymodules import numpy as np, pandas as pd # Local imports @@ -62,6 +62,7 @@ from spyder.plugins.variableexplorer.widgets.arrayeditor import get_idx_rect from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import QStylePalette +from spyder.utils.stylesheet import AppStyle, MAC # Supported real and complex number types @@ -1679,12 +1680,15 @@ def setup_ui(self, title: str) -> None: """ Create user interface. """ - self.layout = QVBoxLayout() - self.layout.setSpacing(0) + # ---- Toolbar (to be filled later) + + self.toolbar = self.create_toolbar('Editor toolbar', register=False) + + # ---- Grid layout with tables and scrollbars showing data frame + self.glayout = QGridLayout() self.glayout.setSpacing(0) - self.glayout.setContentsMargins(0, 12, 0, 0) - self.setLayout(self.layout) + self.glayout.setContentsMargins(0, 0, 0, 0) self.hscroll = QScrollBar(Qt.Horizontal) self.vscroll = QScrollBar(Qt.Vertical) @@ -1708,10 +1712,9 @@ def setup_ui(self, title: str) -> None: self.min_trunc = avg_width * 12 # Minimum size for columns self.max_width = avg_width * 64 # Maximum size for columns - # Make the dialog act as a window - self.setWindowFlags(Qt.Window) + # ---- Buttons at bottom + btn_layout = QHBoxLayout() - btn_layout.setSpacing(5) btn_format = QPushButton(_("Format")) btn_layout.addWidget(btn_format) @@ -1740,15 +1743,26 @@ def setup_ui(self, title: str) -> None: self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) - btn_layout.setContentsMargins(0, 16, 0, 16) - self.glayout.addLayout(btn_layout, 4, 0, 1, 2) + # ---- Final layout - self.toolbar = self.create_toolbar('Editor toolbar', register=False) + self.layout = QVBoxLayout() self.layout.addWidget(self.toolbar) + + # Remove vertical space between toolbar and data frame + style = self.style() + default_spacing = style.pixelMetric(QStyle.PM_LayoutVerticalSpacing) + self.layout.addSpacing(-default_spacing) + self.layout.addLayout(self.glayout) + self.layout.addSpacing((-1 if MAC else 2) * AppStyle.MarginSize) + self.layout.addLayout(btn_layout) + self.setLayout(self.layout) self.setWindowTitle(title) + # Make the dialog act as a window + self.setWindowFlags(Qt.Window) + def set_data_and_check(self, data) -> bool: """ Checks whether data is suitable and display it in the editor. diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index cae95563895..7d6dbb07aca 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -18,7 +18,7 @@ from qtpy.QtGui import QTextOption from qtpy.QtWidgets import ( QAbstractItemView, QButtonGroup, QGroupBox, QHBoxLayout, QHeaderView, - QMessageBox, QPushButton, QRadioButton, QSplitter, QToolButton, + QMessageBox, QPushButton, QRadioButton, QSplitter, QStyle, QToolButton, QVBoxLayout, QWidget) # Local imports @@ -31,6 +31,7 @@ TreeItem, TreeModel, TreeProxyModel) from spyder.utils.icon_manager import ima from spyder.utils.qthelpers import add_actions, qapplication +from spyder.utils.stylesheet import AppStyle, MAC from spyder.widgets.simplecodeeditor import SimpleCodeEditor @@ -178,7 +179,9 @@ def set_value(self, obj): old_obj_tree.deleteLater() else: self.central_splitter.insertWidget(0, self.obj_tree) - + self.central_splitter.setCollapsible(0, False) + self.central_splitter.setCollapsible(1, True) + self.central_splitter.setSizes([500, 320]) def _make_show_column_function(self, column_idx): """Creates a function that shows or hides a column.""" @@ -274,29 +277,27 @@ def _show_special_attributes(self, value: bool): def _setup_views(self): """Creates the UI widgets.""" - layout = QVBoxLayout() - layout.addWidget(self.toolbar) self.central_splitter = QSplitter(self, orientation=Qt.Vertical) - layout.addWidget(self.central_splitter) - self.setLayout(layout) # Bottom pane bottom_pane_widget = QWidget() + bottom_pane_widget.setContentsMargins(0, 2*AppStyle.MarginSize, 0, 0) bottom_layout = QHBoxLayout() bottom_layout.setSpacing(0) - bottom_layout.setContentsMargins(5, 5, 5, 5) # left top right bottom + bottom_layout.setContentsMargins(0, 0, 0, 0) bottom_pane_widget.setLayout(bottom_layout) self.central_splitter.addWidget(bottom_pane_widget) group_box = QGroupBox(_("Details")) + group_box.setStyleSheet('QGroupBox ' + '{margin-bottom: 0px; margin-right: -2px;}') bottom_layout.addWidget(group_box) - v_group_layout = QVBoxLayout() h_group_layout = QHBoxLayout() - h_group_layout.setContentsMargins(2, 2, 2, 2) # left top right bottom - group_box.setLayout(v_group_layout) - v_group_layout.addLayout(h_group_layout) + top_margin = self.style().pixelMetric(QStyle.PM_LayoutTopMargin) + h_group_layout.setContentsMargins(0, top_margin, 0, 0) + group_box.setLayout(h_group_layout) # Radio buttons radio_widget = QWidget() @@ -324,8 +325,6 @@ def _setup_views(self): # Save and close buttons btn_layout = QHBoxLayout() - btn_layout.setContentsMargins(4, 8, 8, 16) - btn_layout.setSpacing(5) btn_layout.addStretch() if not self.readonly: @@ -339,12 +338,19 @@ def _setup_views(self): self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) - layout.addLayout(btn_layout) - # Splitter parameters - self.central_splitter.setCollapsible(0, False) - self.central_splitter.setCollapsible(1, True) - self.central_splitter.setSizes([500, 320]) + layout = QVBoxLayout() + layout.addWidget(self.toolbar) + + # Remove vertical space between toolbar and data from object + style = self.style() + default_spacing = style.pixelMetric(QStyle.PM_LayoutVerticalSpacing) + layout.addSpacing(-default_spacing) + + layout.addWidget(self.central_splitter) + layout.addSpacing((-1 if MAC else 2) * AppStyle.MarginSize) + layout.addLayout(btn_layout) + self.setLayout(layout) # End of setup_methods def _readViewSettings(self, reset=False): diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index a55c522c3d3..e70e3563b1f 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -59,6 +59,7 @@ from spyder.widgets.helperwidgets import CustomSortFilterProxy from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import SpyderPalette +from spyder.utils.stylesheet import AppStyle, MAC # Maximum length of a serialized variable to be set in the kernel @@ -1530,6 +1531,8 @@ def __init__(self, parent, data, namespacebrowser=None, self.refresh_action.setEnabled(data_function is not None) layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) layout.addWidget(toolbar) layout.addWidget(self.editor) self.setLayout(layout) @@ -1595,13 +1598,9 @@ def setup(self, data, title='', readonly=False, remote=False, self.widget.sig_refresh_requested.connect(self.refresh_editor) self.widget.editor.source_model.sig_setting_data.connect( self.save_and_close_enable) - layout = QVBoxLayout() - layout.addWidget(self.widget) - self.setLayout(layout) # Buttons configuration btn_layout = QHBoxLayout() - btn_layout.setContentsMargins(4, 4, 4, 4) btn_layout.addStretch() if not readonly: @@ -1616,7 +1615,12 @@ def setup(self, data, title='', readonly=False, remote=False, self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) + # CollectionEditor widget layout + layout = QVBoxLayout() + layout.addWidget(self.widget) + layout.addSpacing((-1 if MAC else 2) * AppStyle.MarginSize) layout.addLayout(btn_layout) + self.setLayout(layout) self.setWindowTitle(self.widget.get_title()) if icon is None: From d0be94a66474daa0416d6f0ac3b4aa48db63cba8 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 14:43:20 +0000 Subject: [PATCH 09/21] Explicitly pass actions from editors to toolbar --- .../widgets/dataframeeditor.py | 33 +++++++++++----- spyder/widgets/collectionseditor.py | 38 ++++++++++++++----- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 237de9b8936..6c8327c23b2 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -582,7 +582,6 @@ def __init__(self, parent, model, header, hscroll, vscroll, self.menu = None self.menu_header_h = None - self.menu_actions = [] self.empty_ws_menu = None self.copy_action = None self.edit_action = None @@ -597,6 +596,8 @@ def __init__(self, parent, model, header, hscroll, vscroll, self.duplicate_col_action = None self.convert_to_menu = None self.refresh_action = None + self.resize_action = None + self.resize_columns_action = None self.setModel(model) self.setHorizontalScrollBar(hscroll) @@ -722,14 +723,14 @@ def refresh_menu(self): def setup_menu(self): """Setup context menu.""" - resize_action = self.create_action( + self.resize_action = self.create_action( name=None, text=_("Resize rows to contents"), icon=ima.icon('collapse_row'), triggered=lambda: self.resize_to_contents(rows=True), register_action=False ) - resize_columns_action = self.create_action( + self.resize_columns_action = self.create_action( name=None, text=_("Resize columns to contents"), icon=ima.icon('collapse_column'), @@ -838,12 +839,11 @@ def setup_menu(self): MENU_SEPARATOR, self.convert_to_menu.menuAction(), MENU_SEPARATOR, - resize_action, - resize_columns_action, + self.resize_action, + self.resize_columns_action, MENU_SEPARATOR, self.refresh_action ] - self.menu_actions = menu_actions.copy() functions = ( (_("Bool"), bool), @@ -1813,10 +1813,23 @@ def set_data_and_check(self, data) -> bool: self.table_header.setRowHeight(0, self.table_header.height()) self.toolbar.clear() - for item in self.dataTable.menu_actions: - if item is not None: - if item.text() != _('Convert to'): - self.toolbar.addAction(item) + actions = [ + self.dataTable.edit_action, + self.dataTable.copy_action, + self.dataTable.remove_row_action, + self.dataTable.remove_col_action, + self.dataTable.insert_action_above, + self.dataTable.insert_action_below, + self.dataTable.insert_action_after, + self.dataTable.insert_action_before, + self.dataTable.duplicate_row_action, + self.dataTable.duplicate_col_action, + self.dataTable.resize_action, + self.dataTable.resize_columns_action, + self.dataTable.refresh_action + ] + for item in actions: + self.toolbar.addAction(item) return True diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index e70e3563b1f..5922f8ba09b 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -600,7 +600,6 @@ def __init__(self, parent): self.array_filename = None self.menu = None - self.menu_actions = [] self.empty_ws_menu = None self.paste_action = None self.copy_action = None @@ -617,6 +616,8 @@ def __init__(self, parent): self.rename_action = None self.duplicate_action = None self.view_action = None + self.resize_action = None + self.resize_columns_action = None self.delegate = None self.proxy_model = None self.source_model = None @@ -642,14 +643,14 @@ def setup_table(self): def setup_menu(self): """Setup context menu""" - resize_action = self.create_action( + self.resize_action = self.create_action( name=None, text=_("Resize rows to contents"), icon=ima.icon('collapse_row'), triggered=self.resizeRowsToContents, register_action=False ) - resize_columns_action = self.create_action( + self.resize_columns_action = self.create_action( name=None, text=_("Resize columns to contents"), icon=ima.icon('collapse_column'), @@ -760,7 +761,7 @@ def setup_menu(self): ) menu = self.create_menu('Editor menu', register=False) - self.menu_actions = [ + menu_actions = [ self.edit_action, self.copy_action, self.paste_action, @@ -778,10 +779,10 @@ def setup_menu(self): self.hist_action, self.imshow_action, MENU_SEPARATOR, - resize_action, - resize_columns_action + self.resize_action, + self.resize_columns_action ] - add_actions(menu, self.menu_actions) + add_actions(menu, menu_actions) self.empty_ws_menu = self.create_menu('Empty ws', register=False) add_actions( @@ -1512,9 +1513,26 @@ def __init__(self, parent, data, namespacebrowser=None, ) toolbar = self.create_toolbar('Editor toolbar', register=False) - for item in self.editor.menu_actions: - if item is not None: - toolbar.addAction(item) + actions = [ + self.editor.edit_action, + self.editor.copy_action, + self.editor.paste_action, + self.editor.rename_action, + self.editor.remove_action, + self.editor.save_array_action, + self.editor.insert_action, + self.editor.insert_action_above, + self.editor.insert_action_below, + self.editor.duplicate_action, + self.editor.view_action, + self.editor.plot_action, + self.editor.hist_action, + self.editor.imshow_action, + self.editor.resize_action, + self.editor.resize_columns_action + ] + for item in actions: + toolbar.addAction(item) self.refresh_action = self.create_action( name=None, From e9412a7ccaf9332d451d57a6098ee2310a4f817f Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 14:46:18 +0000 Subject: [PATCH 10/21] Move Refresh and Resize to front of toolbars This commit re-orders the buttons in the toolbars for the array editor, collections editor, dataframe editor and object explorer. The Refresh button is the only button present for every editor and is probably one of the most common buttons users will click, as well as being something that applies to the whole object and always enabled, so it makes sense to put it first. The Resize buttons (only present for the array and dataframe editors) also apply to the whole object and are always enabled, so they are put next. --- .../variableexplorer/widgets/arrayeditor.py | 18 ++++++------- .../widgets/dataframeeditor.py | 8 +++--- .../widgets/objectexplorer/objectexplorer.py | 20 +++++++------- spyder/widgets/collectionseditor.py | 26 +++++++++---------- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 7b8545f3677..5487e4b2b6d 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -722,6 +722,15 @@ def do_nothing(): # function as a placeholder here. pass + self.refresh_action = self.create_action( + ArrayEditorActions.Refresh, + text=_('Refresh'), + icon=self.create_icon('refresh'), + tip=_('Refresh editor with current value of variable in console'), + triggered=self.refresh) + self.refresh_action.setDisabled(self.data_function is None) + toolbar.add_item(self.refresh_action) + self.copy_action = self.create_action( ArrayEditorActions.Copy, text=_('Copy'), @@ -759,15 +768,6 @@ def do_nothing(): toggled=do_nothing) toolbar.add_item(self.toggle_bgcolor_action) - self.refresh_action = self.create_action( - ArrayEditorActions.Refresh, - text=_('Refresh'), - icon=self.create_icon('refresh'), - tip=_('Refresh editor with current value of variable in console'), - triggered=self.refresh) - self.refresh_action.setDisabled(self.data_function is None) - toolbar.add_item(self.refresh_action) - toolbar._render() # ---- Stack widget (empty) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 6c8327c23b2..634c18483fb 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -1814,6 +1814,9 @@ def set_data_and_check(self, data) -> bool: self.toolbar.clear() actions = [ + self.dataTable.refresh_action, + self.dataTable.resize_action, + self.dataTable.resize_columns_action, self.dataTable.edit_action, self.dataTable.copy_action, self.dataTable.remove_row_action, @@ -1823,10 +1826,7 @@ def set_data_and_check(self, data) -> bool: self.dataTable.insert_action_after, self.dataTable.insert_action_before, self.dataTable.duplicate_row_action, - self.dataTable.duplicate_col_action, - self.dataTable.resize_action, - self.dataTable.resize_columns_action, - self.dataTable.refresh_action + self.dataTable.duplicate_col_action ] for item in actions: self.toolbar.addAction(item) diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 7d6dbb07aca..79f3427b605 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -204,6 +204,16 @@ def do_nothing(): 'Object explorer toolbar', register=False ) + self.refresh_button = self.create_toolbutton( + name='Refresh toolbutton', + icon=ima.icon('refresh'), + tip=_('Refresh editor with current value of variable in console'), + triggered=self.refresh_editor, + register=False + ) + self.refresh_button.setEnabled(self.data_function is not None) + self.toolbar.add_item(self.refresh_button) + # Show/hide callable objects self.toggle_show_callable_action = self.create_action( name='Show callable attributes', @@ -229,16 +239,6 @@ def do_nothing(): ) self.toolbar.add_item(self.toggle_show_special_attribute_action) - self.refresh_button = self.create_toolbutton( - name='Refresh toolbutton', - icon=ima.icon('refresh'), - tip=_('Refresh editor with current value of variable in console'), - triggered=self.refresh_editor, - register=False - ) - self.refresh_button.setEnabled(self.data_function is not None) - self.toolbar.add_item(self.refresh_button) - stretcher = self.create_stretcher('Toolbar stretcher') self.toolbar.add_item(stretcher) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 5922f8ba09b..2379b77b087 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -1512,8 +1512,20 @@ def __init__(self, parent, data, namespacebrowser=None, self, data, namespacebrowser, data_function, readonly, title ) + self.refresh_action = self.create_action( + name=None, + text=_('Refresh'), + icon=ima.icon('refresh'), + tip=_('Refresh editor with current value of variable in console'), + triggered=lambda: self.sig_refresh_requested.emit(), + register_action=None + ) + toolbar = self.create_toolbar('Editor toolbar', register=False) actions = [ + self.refresh_action, + self.editor.resize_action, + self.editor.resize_columns_action, self.editor.edit_action, self.editor.copy_action, self.editor.paste_action, @@ -1527,23 +1539,11 @@ def __init__(self, parent, data, namespacebrowser=None, self.editor.view_action, self.editor.plot_action, self.editor.hist_action, - self.editor.imshow_action, - self.editor.resize_action, - self.editor.resize_columns_action + self.editor.imshow_action ] for item in actions: toolbar.addAction(item) - self.refresh_action = self.create_action( - name=None, - text=_('Refresh'), - icon=ima.icon('refresh'), - tip=_('Refresh editor with current value of variable in console'), - triggered=lambda: self.sig_refresh_requested.emit(), - register_action=None - ) - toolbar.addAction(self.refresh_action) - # Update the toolbar actions state self.editor.refresh_menu() self.refresh_action.setEnabled(data_function is not None) From 8b16c698a13fec9c448de878e6f44082d31df586 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 15:00:18 +0000 Subject: [PATCH 11/21] Remove Refresh and Resize actions from context menus Remove the Refresh item from the context menu of the dataframe editor and the Resize Rows and Resize Column items from the context menus of both the collections and the dataframe editors. The reason is that these are already present in the toolbar and not context specific. --- spyder/plugins/variableexplorer/widgets/dataframeeditor.py | 7 +------ spyder/widgets/collectionseditor.py | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 634c18483fb..ae60349ed2b 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -837,12 +837,7 @@ def setup_menu(self): self.duplicate_row_action, self.duplicate_col_action, MENU_SEPARATOR, - self.convert_to_menu.menuAction(), - MENU_SEPARATOR, - self.resize_action, - self.resize_columns_action, - MENU_SEPARATOR, - self.refresh_action + self.convert_to_menu.menuAction() ] functions = ( diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 2379b77b087..cc3d2e95ea4 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -777,10 +777,7 @@ def setup_menu(self): self.view_action, self.plot_action, self.hist_action, - self.imshow_action, - MENU_SEPARATOR, - self.resize_action, - self.resize_columns_action + self.imshow_action ] add_actions(menu, menu_actions) From c831f81d219570190fe62e4fb028f70b83a72295 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 16:29:43 +0000 Subject: [PATCH 12/21] Move refresh_action from dataframe view to editor widget This is a refactor to simplify the code. --- .../widgets/dataframeeditor.py | 24 +++++++++---------- .../widgets/tests/test_dataframeeditor.py | 12 +++++----- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index ae60349ed2b..625fe4f15d4 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -571,7 +571,6 @@ class DataFrameView(QTableView, SpyderWidgetMixin): sig_sort_by_column = Signal() sig_fetch_more_columns = Signal() sig_fetch_more_rows = Signal() - sig_refresh_requested = Signal() CONF_SECTION = 'variable_explorer' @@ -595,7 +594,6 @@ def __init__(self, parent, model, header, hscroll, vscroll, self.duplicate_row_action = None self.duplicate_col_action = None self.convert_to_menu = None - self.refresh_action = None self.resize_action = None self.resize_columns_action = None @@ -809,15 +807,6 @@ def setup_menu(self): ) self.copy_action.setShortcut(keybinding('Copy')) self.copy_action.setShortcutContext(Qt.WidgetShortcut) - self.refresh_action = self.create_action( - name=None, - text=_('Refresh'), - icon=ima.icon('refresh'), - tip=_('Refresh editor with current value of variable in console'), - triggered=lambda: self.sig_refresh_requested.emit(), - register_action=False - ) - self.refresh_action.setEnabled(self.data_function is not None) self.convert_to_menu = self.create_menu( menu_id='Convert submenu', @@ -1645,6 +1634,16 @@ def __init__( super().__init__(parent) self.data_function = data_function + self.refresh_action = self.create_action( + name=None, + text=_('Refresh'), + icon=ima.icon('refresh'), + tip=_('Refresh editor with current value of variable in console'), + triggered=self.refresh_editor, + register_action=False + ) + self.refresh_action.setEnabled(self.data_function is not None) + # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to @@ -1809,7 +1808,7 @@ def set_data_and_check(self, data) -> bool: self.toolbar.clear() actions = [ - self.dataTable.refresh_action, + self.refresh_action, self.dataTable.resize_action, self.dataTable.resize_columns_action, self.dataTable.edit_action, @@ -1947,7 +1946,6 @@ def create_data_table(self): self.dataTable.sig_sort_by_column.connect(self._sort_update) self.dataTable.sig_fetch_more_columns.connect(self._fetch_more_columns) self.dataTable.sig_fetch_more_rows.connect(self._fetch_more_rows) - self.dataTable.sig_refresh_requested.connect(self.refresh_editor) def sortByIndex(self, index): """Implement a Index sort.""" diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py index c0684ad2a14..1fb996afeb7 100644 --- a/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py @@ -441,7 +441,7 @@ def test_dataframeeditor_refreshaction_disabled(): df = DataFrame([[0]]) editor = DataFrameEditor(None) editor.setup_and_check(df) - assert not editor.dataTable.refresh_action.isEnabled() + assert not editor.refresh_action.isEnabled() def test_dataframeeditor_refresh(): @@ -454,8 +454,8 @@ def test_dataframeeditor_refresh(): editor = DataFrameEditor(data_function=lambda: df_new) editor.setup_and_check(df_zero) assert_frame_equal(editor.get_value(), df_zero) - assert editor.dataTable.refresh_action.isEnabled() - editor.dataTable.refresh_action.trigger() + assert editor.refresh_action.isEnabled() + editor.refresh_action.trigger() assert_frame_equal(editor.get_value(), df_new) @@ -476,7 +476,7 @@ def test_dataframeeditor_refresh_after_edit(result): with patch('spyder.plugins.variableexplorer.widgets.dataframeeditor' '.QMessageBox.question', return_value=result) as mock_question: - editor.dataTable.refresh_action.trigger() + editor.refresh_action.trigger() mock_question.assert_called_once() editor.accept() if result == QMessageBox.Yes: @@ -496,7 +496,7 @@ def test_dataframeeditor_refresh_into_int(qtbot): with patch('spyder.plugins.variableexplorer.widgets.dataframeeditor' '.QMessageBox.critical') as mock_critical, \ qtbot.waitSignal(editor.rejected, timeout=0): - editor.dataTable.refresh_action.trigger() + editor.refresh_action.trigger() mock_critical.assert_called_once() @@ -514,7 +514,7 @@ def datafunc(): with patch('spyder.plugins.variableexplorer.widgets.dataframeeditor' '.QMessageBox.critical') as mock_critical, \ qtbot.waitSignal(editor.rejected, timeout=0): - editor.dataTable.refresh_action.trigger() + editor.refresh_action.trigger() mock_critical.assert_called_once() From 4130a68ea296498f75be23f7769d5d7b19357828 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 15:02:26 +0000 Subject: [PATCH 13/21] Rename "Save array" as "Save" and remove from toolbar * Rename action in order to match the wording of the other editors in the Variable Explorer and avoid confusion in nested arrays. * Remove from toolbar because it is already in the context menu and that is the logical place because it acts on the selected item. --- spyder/widgets/collectionseditor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index cc3d2e95ea4..20cb755576d 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -704,7 +704,7 @@ def setup_menu(self): self.imshow_action.setVisible(False) self.save_array_action = self.create_action( name=None, - text=_("Save array"), + text=_("Save"), icon=ima.icon('filesave'), triggered=self.save_array, register_action=False @@ -1528,7 +1528,6 @@ def __init__(self, parent, data, namespacebrowser=None, self.editor.paste_action, self.editor.rename_action, self.editor.remove_action, - self.editor.save_array_action, self.editor.insert_action, self.editor.insert_action_above, self.editor.insert_action_below, From 38220613452bfb56c0a1fbf059d17256cee7c16b Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 15:21:10 +0000 Subject: [PATCH 14/21] Remove Edit, Copy, Paste, Rename from toolbars Specifically, * Remove Edit and Copy from the toolbar of the array editor, and add Edit to the context menu (for consistency). * Remove Edit and Copy from the toolbar of the dataframe editor. * Remove Edit, Copy, Paste and Rename from the toolbar of the collections editor. These actions do not belong to the toolbars because they are highly context-sensitive, basic and already easy to trigger. --- .../variableexplorer/widgets/arrayeditor.py | 30 ++++++------------- .../widgets/dataframeeditor.py | 2 -- spyder/widgets/collectionseditor.py | 4 --- 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 5487e4b2b6d..e40ee0b4b1d 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -544,8 +544,16 @@ def setup_menu(self): ) self.copy_action.setShortcut(keybinding('Copy')) self.copy_action.setShortcutContext(Qt.WidgetShortcut) + + edit_action = self.create_action( + name=None, + text=_('Edit'), + icon=ima.icon('edit'), + triggered=self.edit_item, + register_action=False + ) menu = self.create_menu('Editor menu', register=False) - add_actions(menu, [self.copy_action, ]) + add_actions(menu, [self.copy_action, edit_action]) return menu def contextMenuEvent(self, event): @@ -731,20 +739,6 @@ def do_nothing(): self.refresh_action.setDisabled(self.data_function is None) toolbar.add_item(self.refresh_action) - self.copy_action = self.create_action( - ArrayEditorActions.Copy, - text=_('Copy'), - icon=self.create_icon('editcopy'), - triggered=do_nothing) - toolbar.add_item(self.copy_action) - - self.edit_action = self.create_action( - ArrayEditorActions.Edit, - text=_('Edit'), - icon=self.create_icon('edit'), - triggered=do_nothing) - toolbar.add_item(self.edit_action) - self.format_action = self.create_action( ArrayEditorActions.Format, text=_('Format'), @@ -938,12 +932,6 @@ def set_data_and_check(self, data, readonly=False): # ---- Actions - safe_disconnect(self.copy_action.triggered) - self.copy_action.triggered.connect(self.arraywidget.view.copy) - - safe_disconnect(self.edit_action.triggered) - self.edit_action.triggered.connect(self.arraywidget.view.edit_item) - safe_disconnect(self.format_action.triggered) self.format_action.triggered.connect(self.arraywidget.change_format) self.format_action.setEnabled(is_float(self.arraywidget.data.dtype)) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 625fe4f15d4..0dec5982223 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -1811,8 +1811,6 @@ def set_data_and_check(self, data) -> bool: self.refresh_action, self.dataTable.resize_action, self.dataTable.resize_columns_action, - self.dataTable.edit_action, - self.dataTable.copy_action, self.dataTable.remove_row_action, self.dataTable.remove_col_action, self.dataTable.insert_action_above, diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 20cb755576d..816bdda745f 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -1523,10 +1523,6 @@ def __init__(self, parent, data, namespacebrowser=None, self.refresh_action, self.editor.resize_action, self.editor.resize_columns_action, - self.editor.edit_action, - self.editor.copy_action, - self.editor.paste_action, - self.editor.rename_action, self.editor.remove_action, self.editor.insert_action, self.editor.insert_action_above, From 8f9f19995ecb452f9a0fab6188c4c7ce2f3be41e Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 15:06:26 +0000 Subject: [PATCH 15/21] Reorder items in context menus and toolbars Specifically, change the context menus and toolbars of the collections and dataframe editors, and put Remove (Row/Column) after Duplicate (Row/Column), where it logically belongs. Also put Copy before Edit in the context menus for consistency. --- .../variableexplorer/widgets/dataframeeditor.py | 12 ++++++------ spyder/widgets/collectionseditor.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 0dec5982223..aea9d0dcbf1 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -814,10 +814,8 @@ def setup_menu(self): register=False ) menu_actions = [ - self.edit_action, self.copy_action, - self.remove_row_action, - self.remove_col_action, + self.edit_action, MENU_SEPARATOR, self.insert_action_above, self.insert_action_below, @@ -825,6 +823,8 @@ def setup_menu(self): self.insert_action_before, self.duplicate_row_action, self.duplicate_col_action, + self.remove_row_action, + self.remove_col_action, MENU_SEPARATOR, self.convert_to_menu.menuAction() ] @@ -1811,14 +1811,14 @@ def set_data_and_check(self, data) -> bool: self.refresh_action, self.dataTable.resize_action, self.dataTable.resize_columns_action, - self.dataTable.remove_row_action, - self.dataTable.remove_col_action, self.dataTable.insert_action_above, self.dataTable.insert_action_below, self.dataTable.insert_action_after, self.dataTable.insert_action_before, self.dataTable.duplicate_row_action, - self.dataTable.duplicate_col_action + self.dataTable.duplicate_col_action, + self.dataTable.remove_row_action, + self.dataTable.remove_col_action ] for item in actions: self.toolbar.addAction(item) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 816bdda745f..06e92f15aef 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -642,7 +642,7 @@ def setup_table(self): self.selectionModel().selectionChanged.connect(self.refresh_menu) def setup_menu(self): - """Setup context menu""" + """Setup actions and context menu""" self.resize_action = self.create_action( name=None, text=_("Resize rows to contents"), @@ -762,17 +762,17 @@ def setup_menu(self): menu = self.create_menu('Editor menu', register=False) menu_actions = [ - self.edit_action, self.copy_action, self.paste_action, self.rename_action, - self.remove_action, + self.edit_action, self.save_array_action, MENU_SEPARATOR, self.insert_action, self.insert_action_above, self.insert_action_below, self.duplicate_action, + self.remove_action, MENU_SEPARATOR, self.view_action, self.plot_action, @@ -1523,11 +1523,11 @@ def __init__(self, parent, data, namespacebrowser=None, self.refresh_action, self.editor.resize_action, self.editor.resize_columns_action, - self.editor.remove_action, self.editor.insert_action, self.editor.insert_action_above, self.editor.insert_action_below, self.editor.duplicate_action, + self.editor.remove_action, self.editor.view_action, self.editor.plot_action, self.editor.hist_action, From 54686b7d4000f220819331a362a8eaf6c4af2f00 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 26 Dec 2023 19:39:08 +0000 Subject: [PATCH 16/21] Remove buttons in lower left of dataframe editor window The functionality from the four buttons is handled as follows: * Add Format button to toolbar * There already is a Resize button in the toolbar * Add Background Color button to toolbar * Add options menu and put Column Min/Max item in there --- .../widgets/dataframeeditor.py | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index aea9d0dcbf1..3af272e12e8 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -45,9 +45,9 @@ Signal, Slot) from qtpy.QtGui import QColor, QCursor from qtpy.QtWidgets import ( - QApplication, QCheckBox, QDialog, QFrame, QGridLayout, QHBoxLayout, - QInputDialog, QItemDelegate, QLabel, QLineEdit, QMessageBox, QPushButton, - QScrollBar, QStyle, QTableView, QTableWidget, QVBoxLayout, QWidget) + QApplication, QDialog, QFrame, QGridLayout, QHBoxLayout, QInputDialog, + QItemDelegate, QLabel, QLineEdit, QMessageBox, QPushButton, QScrollBar, + QStyle, QTableView, QTableWidget, QToolButton, QVBoxLayout, QWidget) from spyder_kernels.utils.lazymodules import numpy as np, pandas as pd # Local imports @@ -58,7 +58,7 @@ to_text_string) from spyder.utils.icon_manager import ima from spyder.utils.qthelpers import ( - add_actions, keybinding, MENU_SEPARATOR, qapplication, safe_disconnect) + add_actions, keybinding, MENU_SEPARATOR, qapplication) from spyder.plugins.variableexplorer.widgets.arrayeditor import get_idx_rect from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import QStylePalette @@ -1643,6 +1643,24 @@ def __init__( register_action=False ) self.refresh_action.setEnabled(self.data_function is not None) + self.format_action = self.create_action( + name=None, + text=_('Format'), + icon=self.create_icon('format_float'), + tip=_('Set format of floating-point numbers'), + triggered=self.change_format + ) + self.bgcolor_action = self.create_action( + name=None, + text=_('Background color'), + icon=self.create_icon('background_color'), + toggled=self.change_bgcolor_enable + ) + self.bgcolor_global_action = self.create_action( + name='Background color global', + text=_('Column min/max'), + toggled=self.toggle_bgcolor_global + ) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread @@ -1709,22 +1727,6 @@ def setup_ui(self, title: str) -> None: # ---- Buttons at bottom btn_layout = QHBoxLayout() - - btn_format = QPushButton(_("Format")) - btn_layout.addWidget(btn_format) - btn_format.clicked.connect(self.change_format) - - btn_resize = QPushButton(_('Resize')) - btn_layout.addWidget(btn_resize) - btn_resize.clicked.connect(self.resize_to_contents) - - self.bgcolor = QCheckBox(_('Background color')) - self.bgcolor.stateChanged.connect(self.change_bgcolor_enable) - btn_layout.addWidget(self.bgcolor) - - self.bgcolor_global = QCheckBox(_('Column min/max')) - btn_layout.addWidget(self.bgcolor_global) - btn_layout.addStretch() self.btn_save_and_close = QPushButton(_('Save and Close')) @@ -1791,14 +1793,13 @@ def set_data_and_check(self, data) -> bool: self.setModel(self.dataModel) self.resizeColumnsToContents() - self.bgcolor.setChecked(self.dataModel.bgcolor_enabled) - self.bgcolor.setEnabled(self.dataModel.bgcolor_enabled) + self.bgcolor_action.setChecked(self.dataModel.bgcolor_enabled) + self.bgcolor_action.setEnabled(self.dataModel.bgcolor_enabled) - safe_disconnect(self.bgcolor_global.stateChanged) - self.bgcolor_global.stateChanged.connect(self.dataModel.colum_avg) - self.bgcolor_global.setChecked(self.dataModel.colum_avg_enabled) - self.bgcolor_global.setEnabled(not self.is_series and - self.dataModel.bgcolor_enabled) + self.bgcolor_global_action.setChecked(self.dataModel.colum_avg_enabled) + self.bgcolor_global_action.setEnabled( + not self.is_series and self.dataModel.bgcolor_enabled + ) self.btn_save_and_close.setDisabled(True) self.dataModel.set_format_spec(self.get_conf('dataframe_format')) @@ -1809,8 +1810,10 @@ def set_data_and_check(self, data) -> bool: self.toolbar.clear() actions = [ self.refresh_action, + self.format_action, self.dataTable.resize_action, self.dataTable.resize_columns_action, + self.bgcolor_action, self.dataTable.insert_action_above, self.dataTable.insert_action_below, self.dataTable.insert_action_after, @@ -1823,6 +1826,21 @@ def set_data_and_check(self, data) -> bool: for item in actions: self.toolbar.addAction(item) + stretcher = self.create_stretcher('Toolbar stretcher') + self.toolbar.addWidget(stretcher) + + options_menu = self.create_menu('Options menu', register=False) + options_menu.add_action(self.bgcolor_global_action) + options_button = self.create_toolbutton( + name='Options toolbutton', + text=_('Options'), + icon=ima.icon('tooloptions'), + register=False + ) + options_button.setPopupMode(QToolButton.InstantPopup) + options_button.setMenu(options_menu) + self.toolbar.addWidget(options_button) + return True @Slot(QModelIndex, QModelIndex) @@ -2142,7 +2160,14 @@ def change_bgcolor_enable(self, state): This is implementet so column min/max is only active when bgcolor is """ self.dataModel.bgcolor(state) - self.bgcolor_global.setEnabled(not self.is_series and state > 0) + self.bgcolor_global_action.setEnabled(not self.is_series and state > 0) + + def toggle_bgcolor_global(self, state: bool) -> None: + """ + Toggle "Column min/max" setting + """ + if self.dataModel: + self.dataModel.colum_avg(state) def refresh_editor(self) -> None: """ From 65944e4c535fb196ab985049dee9599918a9cb77 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 3 Jan 2024 17:52:14 +0000 Subject: [PATCH 17/21] Change default value of new parameter in create_toolbutton() This fixes a mistake in commit 7d76a884. That commit introduced a new parameter `register` in the member function create_toolbutton() in SpyderToolButtonMixin. The default value made this a change in behaviour which broke some tests of the outline explorer. The new default value ensures that the behaviour is not changed if no value for the new parameter is given, making the change backward compatible. --- spyder/api/widgets/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index b65e2f34b6f..7735cb51681 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -48,7 +48,7 @@ class SpyderToolButtonMixin: def create_toolbutton(self, name, text=None, icon=None, tip=None, toggled=None, triggered=None, autoraise=True, text_beside_icon=False, - section=None, option=None, register=False): + section=None, option=None, register=True): """ Create a Spyder toolbutton. """ From c0a1cd1e0ae090317b7213de817bce37fc892c03 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 3 Jan 2024 21:14:08 +0000 Subject: [PATCH 18/21] Eliminate warnings in array editor This fixes the test failure in test_arrayeditor_with_inf_array. --- .../variableexplorer/widgets/arrayeditor.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index e40ee0b4b1d..12e9d911e7a 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -443,6 +443,9 @@ def setEditorData(self, editor, index): #TODO: Implement "Paste" (from clipboard) feature class ArrayView(QTableView, SpyderWidgetMixin): """Array view class""" + + CONF_SECTION = 'variable_explorer' + def __init__(self, parent, model, dtype, shape): QTableView.__init__(self, parent) @@ -735,7 +738,9 @@ def do_nothing(): text=_('Refresh'), icon=self.create_icon('refresh'), tip=_('Refresh editor with current value of variable in console'), - triggered=self.refresh) + triggered=self.refresh, + register_action=False + ) self.refresh_action.setDisabled(self.data_function is None) toolbar.add_item(self.refresh_action) @@ -744,7 +749,9 @@ def do_nothing(): text=_('Format'), icon=self.create_icon('format_float'), tip=_('Set format of floating-point numbers'), - triggered=do_nothing) + triggered=do_nothing, + register_action=False + ) toolbar.add_item(self.format_action) self.resize_action = self.create_action( @@ -752,14 +759,18 @@ def do_nothing(): text=_('Resize'), icon=self.create_icon('collapse_column'), tip=_('Resize columns to contents'), - triggered=do_nothing) + triggered=do_nothing, + register_action=False + ) toolbar.add_item(self.resize_action) self.toggle_bgcolor_action = self.create_action( ArrayEditorActions.ToggleBackgroundColor, text=_('Background color'), icon=self.create_icon('background_color'), - toggled=do_nothing) + toggled=do_nothing, + register_action=False + ) toolbar.add_item(self.toggle_bgcolor_action) toolbar._render() From 2d765eefe51aaa64900c3eca46036801d7f37a62 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sun, 28 Jan 2024 20:04:37 +0000 Subject: [PATCH 19/21] Move toolbar buttons in Variable Explorer and its editors * Move Refresh button to the right in the array and dataframe editors and put it immediately before the options menu. * Move the Search, Filter and Refresh buttons in the main widget of the Variable Editor immediately before the options menu. in the array and dataframe editor * Move the Toggle background and Format actions in the array and dataframe editors to the options menu. * Add separators in the toolbar of dataframe and collections editors. --- spyder/api/widgets/main_widget.py | 2 +- .../variableexplorer/widgets/arrayeditor.py | 49 ++++++++++++------- .../widgets/dataframeeditor.py | 17 ++++--- .../variableexplorer/widgets/main_widget.py | 12 ++++- .../widgets/objectexplorer/objectexplorer.py | 20 ++++---- spyder/widgets/collectionseditor.py | 30 ++++++------ 6 files changed, 74 insertions(+), 56 deletions(-) diff --git a/spyder/api/widgets/main_widget.py b/spyder/api/widgets/main_widget.py index 8fa6448deb8..51d1b987289 100644 --- a/spyder/api/widgets/main_widget.py +++ b/spyder/api/widgets/main_widget.py @@ -279,7 +279,7 @@ def __init__(self, name, plugin, parent=None): parent=self, title=_("Main widget corner toolbar"), ) - self._corner_toolbar.ID = 'corner_toolbar', + self._corner_toolbar.ID = 'corner_toolbar' TOOLBAR_REGISTRY.register_reference( self._corner_toolbar, self._corner_toolbar.ID, diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 12e9d911e7a..3027ed59d92 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -26,7 +26,7 @@ from qtpy.QtWidgets import ( QAbstractItemDelegate, QApplication, QDialog, QHBoxLayout, QInputDialog, QItemDelegate, QLabel, QLineEdit, QMessageBox, QPushButton, QSpinBox, - QStackedWidget, QStyle, QTableView, QVBoxLayout, QWidget) + QStackedWidget, QStyle, QTableView, QToolButton, QVBoxLayout, QWidget) from spyder_kernels.utils.nsview import value_to_display from spyder_kernels.utils.lazymodules import numpy as np @@ -723,9 +723,8 @@ def setup_ui(self, title='', readonly=False): interface of the array editor. Some elements need to be hidden depending on the data; this will be done when the data is set. """ - # ---- Toolbar and actions - toolbar = self.create_toolbar('Editor toolbar', register=False) + # ---- Actions def do_nothing(): # .create_action() needs a toggled= parameter, but we can only @@ -733,6 +732,14 @@ def do_nothing(): # function as a placeholder here. pass + self.format_action = self.create_action( + ArrayEditorActions.Format, + text=_('Format'), + icon=self.create_icon('format_float'), + tip=_('Set format of floating-point numbers'), + triggered=do_nothing, + register_action=False + ) self.refresh_action = self.create_action( ArrayEditorActions.Refresh, text=_('Refresh'), @@ -742,18 +749,6 @@ def do_nothing(): register_action=False ) self.refresh_action.setDisabled(self.data_function is None) - toolbar.add_item(self.refresh_action) - - self.format_action = self.create_action( - ArrayEditorActions.Format, - text=_('Format'), - icon=self.create_icon('format_float'), - tip=_('Set format of floating-point numbers'), - triggered=do_nothing, - register_action=False - ) - toolbar.add_item(self.format_action) - self.resize_action = self.create_action( ArrayEditorActions.Resize, text=_('Resize'), @@ -762,17 +757,33 @@ def do_nothing(): triggered=do_nothing, register_action=False ) - toolbar.add_item(self.resize_action) - self.toggle_bgcolor_action = self.create_action( ArrayEditorActions.ToggleBackgroundColor, text=_('Background color'), - icon=self.create_icon('background_color'), toggled=do_nothing, register_action=False ) - toolbar.add_item(self.toggle_bgcolor_action) + # ---- Toolbar and options menu + + options_menu = self.create_menu('Options menu', register=False) + options_menu.add_action(self.toggle_bgcolor_action) + options_menu.add_action(self.format_action) + + options_button = self.create_toolbutton( + name='Options toolbutton', + text=_('Options'), + icon=ima.icon('tooloptions'), + register=False + ) + options_button.setPopupMode(QToolButton.InstantPopup) + options_button.setMenu(options_menu) + + toolbar = self.create_toolbar('Editor toolbar', register=False) + toolbar.add_item(self.resize_action) + toolbar.add_item(self.create_stretcher(id_='stretcher')) + toolbar.add_item(self.refresh_action) + toolbar.add_item(options_button) toolbar._render() # ---- Stack widget (empty) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 3af272e12e8..9c833fa79e9 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -1644,16 +1644,15 @@ def __init__( ) self.refresh_action.setEnabled(self.data_function is not None) self.format_action = self.create_action( - name=None, + name='Format', text=_('Format'), icon=self.create_icon('format_float'), tip=_('Set format of floating-point numbers'), triggered=self.change_format ) self.bgcolor_action = self.create_action( - name=None, + name='Background color', text=_('Background color'), - icon=self.create_icon('background_color'), toggled=self.change_bgcolor_enable ) self.bgcolor_global_action = self.create_action( @@ -1808,12 +1807,10 @@ def set_data_and_check(self, data) -> bool: self.table_header.setRowHeight(0, self.table_header.height()) self.toolbar.clear() + self.toolbar.addAction(self.dataTable.resize_action) + self.toolbar.addAction(self.dataTable.resize_columns_action) + self.toolbar.addSeparator() actions = [ - self.refresh_action, - self.format_action, - self.dataTable.resize_action, - self.dataTable.resize_columns_action, - self.bgcolor_action, self.dataTable.insert_action_above, self.dataTable.insert_action_below, self.dataTable.insert_action_after, @@ -1829,8 +1826,12 @@ def set_data_and_check(self, data) -> bool: stretcher = self.create_stretcher('Toolbar stretcher') self.toolbar.addWidget(stretcher) + self.toolbar.addAction(self.refresh_action) + options_menu = self.create_menu('Options menu', register=False) + options_menu.add_action(self.bgcolor_action) options_menu.add_action(self.bgcolor_global_action) + options_menu.add_action(self.format_action) options_button = self.create_toolbutton( name='Options toolbutton', text=_('Options'), diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py index 644df9ab687..0bbc3c56610 100644 --- a/spyder/plugins/variableexplorer/widgets/main_widget.py +++ b/spyder/plugins/variableexplorer/widgets/main_widget.py @@ -382,8 +382,7 @@ def setup(self): # Main toolbar main_toolbar = self.get_main_toolbar() for item in [import_data_action, save_action, save_as_action, - reset_namespace_action, search_action, refresh_action, - self.filter_button]: + reset_namespace_action]: self.add_item_to_toolbar( item, toolbar=main_toolbar, @@ -391,6 +390,15 @@ def setup(self): ) save_action.setEnabled(False) + # Corner toolbar (i.e., buttons in front of options menu) + corner_toolbar = self.get_toolbar('corner_toolbar') + for item in [search_action, self.filter_button, refresh_action]: + self.add_item_to_toolbar( + item, + toolbar=corner_toolbar, + section='corner' + ) + # ---- Context menu to show when there are variables present self.context_menu = self.create_menu( VariableExplorerWidgetMenus.PopulatedContextMenu) diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index 79f3427b605..f96356366f6 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -204,16 +204,6 @@ def do_nothing(): 'Object explorer toolbar', register=False ) - self.refresh_button = self.create_toolbutton( - name='Refresh toolbutton', - icon=ima.icon('refresh'), - tip=_('Refresh editor with current value of variable in console'), - triggered=self.refresh_editor, - register=False - ) - self.refresh_button.setEnabled(self.data_function is not None) - self.toolbar.add_item(self.refresh_button) - # Show/hide callable objects self.toggle_show_callable_action = self.create_action( name='Show callable attributes', @@ -242,6 +232,16 @@ def do_nothing(): stretcher = self.create_stretcher('Toolbar stretcher') self.toolbar.add_item(stretcher) + self.refresh_button = self.create_toolbutton( + name='Refresh toolbutton', + icon=ima.icon('refresh'), + tip=_('Refresh editor with current value of variable in console'), + triggered=self.refresh_editor, + register=False + ) + self.refresh_button.setEnabled(self.data_function is not None) + self.toolbar.add_item(self.refresh_button) + self.options_button = self.create_toolbutton( name='Options toolbutton', text=_('Options'), diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 06e92f15aef..602d7978687 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -1519,22 +1519,20 @@ def __init__(self, parent, data, namespacebrowser=None, ) toolbar = self.create_toolbar('Editor toolbar', register=False) - actions = [ - self.refresh_action, - self.editor.resize_action, - self.editor.resize_columns_action, - self.editor.insert_action, - self.editor.insert_action_above, - self.editor.insert_action_below, - self.editor.duplicate_action, - self.editor.remove_action, - self.editor.view_action, - self.editor.plot_action, - self.editor.hist_action, - self.editor.imshow_action - ] - for item in actions: - toolbar.addAction(item) + toolbar.addAction(self.refresh_action) + toolbar.addAction(self.editor.resize_action) + toolbar.addAction(self.editor.resize_columns_action) + toolbar.addSeparator() + toolbar.addAction(self.editor.insert_action) + toolbar.addAction(self.editor.insert_action_above) + toolbar.addAction(self.editor.insert_action_below) + toolbar.addAction(self.editor.duplicate_action) + toolbar.addAction(self.editor.remove_action) + toolbar.addSeparator() + toolbar.addAction(self.editor.view_action) + toolbar.addAction(self.editor.plot_action) + toolbar.addAction(self.editor.hist_action) + toolbar.addAction(self.editor.imshow_action) # Update the toolbar actions state self.editor.refresh_menu() From 2a61b450f3592f8b61f5f8830d40172b308c40ab Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Thu, 8 Feb 2024 13:49:04 +0000 Subject: [PATCH 20/21] Move buttons in Variable Explorer and its editors again Specifically: * Move "Insert before" to be before "Insert after" in the dataframe editor. * Move "Refresh" to the right in the collections editor, just before the options menu. * Move "Search", "Filter" and "Refresh" in the Variable Explorer pane to be between the spinner and the options menu. * Split the row and column actions in the dataframe editor. * Move "Resize rows" and "Resize columns" to the left of "Refresh" in the array, collections and dataframe editors. --- .../variableexplorer/widgets/arrayeditor.py | 2 +- .../widgets/dataframeeditor.py | 31 ++++++++--------- .../variableexplorer/widgets/main_widget.py | 33 +++++++++++++------ spyder/widgets/collectionseditor.py | 8 ++--- 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 3027ed59d92..06879d3e556 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -780,8 +780,8 @@ def do_nothing(): options_button.setMenu(options_menu) toolbar = self.create_toolbar('Editor toolbar', register=False) - toolbar.add_item(self.resize_action) toolbar.add_item(self.create_stretcher(id_='stretcher')) + toolbar.add_item(self.resize_action) toolbar.add_item(self.refresh_action) toolbar.add_item(options_button) toolbar._render() diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index 9c833fa79e9..ef09a23840b 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -819,11 +819,12 @@ def setup_menu(self): MENU_SEPARATOR, self.insert_action_above, self.insert_action_below, - self.insert_action_after, - self.insert_action_before, self.duplicate_row_action, - self.duplicate_col_action, self.remove_row_action, + MENU_SEPARATOR, + self.insert_action_before, + self.insert_action_after, + self.duplicate_col_action, self.remove_col_action, MENU_SEPARATOR, self.convert_to_menu.menuAction() @@ -1807,25 +1808,21 @@ def set_data_and_check(self, data) -> bool: self.table_header.setRowHeight(0, self.table_header.height()) self.toolbar.clear() - self.toolbar.addAction(self.dataTable.resize_action) - self.toolbar.addAction(self.dataTable.resize_columns_action) + self.toolbar.addAction(self.dataTable.insert_action_above) + self.toolbar.addAction(self.dataTable.insert_action_below) + self.toolbar.addAction(self.dataTable.duplicate_row_action) + self.toolbar.addAction(self.dataTable.remove_row_action) self.toolbar.addSeparator() - actions = [ - self.dataTable.insert_action_above, - self.dataTable.insert_action_below, - self.dataTable.insert_action_after, - self.dataTable.insert_action_before, - self.dataTable.duplicate_row_action, - self.dataTable.duplicate_col_action, - self.dataTable.remove_row_action, - self.dataTable.remove_col_action - ] - for item in actions: - self.toolbar.addAction(item) + self.toolbar.addAction(self.dataTable.insert_action_before) + self.toolbar.addAction(self.dataTable.insert_action_after) + self.toolbar.addAction(self.dataTable.duplicate_col_action) + self.toolbar.addAction(self.dataTable.remove_col_action) stretcher = self.create_stretcher('Toolbar stretcher') self.toolbar.addWidget(stretcher) + self.toolbar.addAction(self.dataTable.resize_action) + self.toolbar.addAction(self.dataTable.resize_columns_action) self.toolbar.addAction(self.refresh_action) options_menu = self.create_menu('Options menu', register=False) diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py index 0bbc3c56610..86f8847a06b 100644 --- a/spyder/plugins/variableexplorer/widgets/main_widget.py +++ b/spyder/plugins/variableexplorer/widgets/main_widget.py @@ -218,7 +218,7 @@ def setup(self): triggered=lambda x: self.reset_namespace(), ) - search_action = self.create_action( + self.search_action = self.create_action( VariableExplorerWidgetActions.Search, text=_("Search variable names and types"), icon=self.create_icon('find'), @@ -226,7 +226,7 @@ def setup(self): register_shortcut=True ) - refresh_action = self.create_action( + self.refresh_action = self.create_action( VariableExplorerWidgetActions.Refresh, text=_("Refresh variables"), icon=self.create_icon('refresh'), @@ -390,14 +390,7 @@ def setup(self): ) save_action.setEnabled(False) - # Corner toolbar (i.e., buttons in front of options menu) - corner_toolbar = self.get_toolbar('corner_toolbar') - for item in [search_action, self.filter_button, refresh_action]: - self.add_item_to_toolbar( - item, - toolbar=corner_toolbar, - section='corner' - ) + # Search, Filter and Refresh buttons are added in _setup() # ---- Context menu to show when there are variables present self.context_menu = self.create_menu( @@ -443,6 +436,26 @@ def setup(self): section=VariableExplorerContextMenuSections.Edit, ) + def _setup(self): + """ + Create options menu and adjacent toolbar buttons, etc. + + This calls the parent's method to setup default actions, create the + spinner and the options menu, and connect signals. After that, it adds + the Search, Filter and Refresh buttons between the spinner and the + options menu. + """ + super()._setup() + + corner_widget = self._corner_widget + for action in corner_widget.actions(): + if action.defaultWidget() == self.get_options_menu_button(): + options_menu_action = action + + corner_widget.insertAction(options_menu_action, self.search_action) + corner_widget.insertAction(options_menu_action, self.filter_button) + corner_widget.insertAction(options_menu_action, self.refresh_action) + def update_actions(self): """Update the actions.""" if self.is_current_widget_empty(): diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index 602d7978687..a3699bb7dda 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -1519,10 +1519,6 @@ def __init__(self, parent, data, namespacebrowser=None, ) toolbar = self.create_toolbar('Editor toolbar', register=False) - toolbar.addAction(self.refresh_action) - toolbar.addAction(self.editor.resize_action) - toolbar.addAction(self.editor.resize_columns_action) - toolbar.addSeparator() toolbar.addAction(self.editor.insert_action) toolbar.addAction(self.editor.insert_action_above) toolbar.addAction(self.editor.insert_action_below) @@ -1533,6 +1529,10 @@ def __init__(self, parent, data, namespacebrowser=None, toolbar.addAction(self.editor.plot_action) toolbar.addAction(self.editor.hist_action) toolbar.addAction(self.editor.imshow_action) + toolbar.addWidget(self.create_stretcher('Toolbar stretcher')) + toolbar.addAction(self.editor.resize_action) + toolbar.addAction(self.editor.resize_columns_action) + toolbar.addAction(self.refresh_action) # Update the toolbar actions state self.editor.refresh_menu() From b2cb5c4a6d04c3825a84848f35b9ffd834c4db58 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Sun, 11 Feb 2024 11:32:06 +0000 Subject: [PATCH 21/21] Use SpyderWidgetMixin API to build toolbars and menus * Consistently use add_item_to_menu() and add_item_to_toolbar() to add items to menus and toolbars. * Use sections to add separators to menus and toolbars. * Define constants for the names of actions, widgets, etc in enum-like classes. * A few minor improvements to code style. --- spyder/api/widgets/mixins.py | 10 +- .../variableexplorer/widgets/arrayeditor.py | 52 +++- .../widgets/dataframeeditor.py | 275 ++++++++++++------ .../variableexplorer/widgets/main_widget.py | 6 +- .../widgets/objectexplorer/objectexplorer.py | 79 +++-- .../tests/test_objectexplorer.py | 11 +- .../objectexplorer/toggle_column_mixin.py | 33 ++- .../widgets/tests/test_dataframeeditor.py | 3 + spyder/widgets/collectionseditor.py | 201 +++++++++---- 9 files changed, 450 insertions(+), 220 deletions(-) diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py index 7735cb51681..fa359e0c1e9 100644 --- a/spyder/api/widgets/mixins.py +++ b/spyder/api/widgets/mixins.py @@ -168,8 +168,11 @@ def create_stretcher(self, id_=None): stretcher.ID = id_ return stretcher - def create_toolbar(self, name: str, - register: bool = True) -> SpyderToolbar: + def create_toolbar( + self, + name: str, + register: bool = True + ) -> SpyderToolbar: """ Create a Spyder toolbar. @@ -184,7 +187,8 @@ def create_toolbar(self, name: str, toolbar.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) if register: TOOLBAR_REGISTRY.register_reference( - toolbar, name, self.PLUGIN_NAME, self.CONTEXT_NAME) + toolbar, name, self.PLUGIN_NAME, self.CONTEXT_NAME + ) return toolbar def get_toolbar(self, name: str, context: Optional[str] = None, diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py index 06879d3e556..4d03bdf8390 100644 --- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py +++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py @@ -43,9 +43,12 @@ from spyder.py3compat import (is_binary_string, is_string, is_text_string, to_binary_string, to_text_string) from spyder.utils.icon_manager import ima -from spyder.utils.qthelpers import add_actions, keybinding, safe_disconnect +from spyder.utils.qthelpers import keybinding, safe_disconnect from spyder.utils.stylesheet import AppStyle, MAC +# ============================================================================= +# ---- Constants +# ============================================================================= class ArrayEditorActions: Copy = 'copy_action' @@ -56,6 +59,16 @@ class ArrayEditorActions: ToggleBackgroundColor = 'toggle_background_color_action' +class ArrayEditorMenus: + Options = 'options_menu' + + +class ArrayEditorWidgets: + OptionsToolButton = 'options_button_widget' + Toolbar = 'toolbar' + ToolbarStretcher = 'toolbar_stretcher' + + # Note: string and unicode data types will be formatted with 's' (see below) SUPPORTED_FORMATS = { 'single': '.6g', @@ -105,10 +118,10 @@ class ArrayEditorActions: LARGE_NROWS = 1e5 LARGE_COLS = 60 - #============================================================================== -# Utility functions +# ---- Utility functions #============================================================================== + def is_float(dtype): """Return True if datatype dtype is a float kind""" return ('float' in dtype.name) or dtype.name in ['single', 'double'] @@ -127,8 +140,9 @@ def get_idx_rect(index_list): #============================================================================== -# Main classes +# ---- Main classes #============================================================================== + class ArrayModel(QAbstractTableModel, SpyderFontsMixin): """Array Editor Table Model""" @@ -539,7 +553,7 @@ def resize_to_contents(self): def setup_menu(self): """Setup context menu""" self.copy_action = self.create_action( - name=None, + name=ArrayEditorActions.Copy, text=_('Copy'), icon=ima.icon('editcopy'), triggered=self.copy, @@ -549,14 +563,17 @@ def setup_menu(self): self.copy_action.setShortcutContext(Qt.WidgetShortcut) edit_action = self.create_action( - name=None, + name=ArrayEditorActions.Edit, text=_('Edit'), icon=ima.icon('edit'), triggered=self.edit_item, register_action=False ) + menu = self.create_menu('Editor menu', register=False) - add_actions(menu, [self.copy_action, edit_action]) + for action in [self.copy_action, edit_action]: + self.add_item_to_menu(action, menu) + return menu def contextMenuEvent(self, event): @@ -766,12 +783,15 @@ def do_nothing(): # ---- Toolbar and options menu - options_menu = self.create_menu('Options menu', register=False) + options_menu = self.create_menu( + ArrayEditorMenus.Options, + register=False + ) options_menu.add_action(self.toggle_bgcolor_action) options_menu.add_action(self.format_action) options_button = self.create_toolbutton( - name='Options toolbutton', + name=ArrayEditorWidgets.OptionsToolButton, text=_('Options'), icon=ima.icon('tooloptions'), register=False @@ -779,11 +799,15 @@ def do_nothing(): options_button.setPopupMode(QToolButton.InstantPopup) options_button.setMenu(options_menu) - toolbar = self.create_toolbar('Editor toolbar', register=False) - toolbar.add_item(self.create_stretcher(id_='stretcher')) - toolbar.add_item(self.resize_action) - toolbar.add_item(self.refresh_action) - toolbar.add_item(options_button) + toolbar = self.create_toolbar( + ArrayEditorWidgets.Toolbar, + register=False + ) + stretcher = self.create_stretcher(ArrayEditorWidgets.ToolbarStretcher) + for item in [stretcher, self.resize_action, self.refresh_action, + options_button]: + self.add_item_to_toolbar(item, toolbar) + toolbar._render() # ---- Stack widget (empty) diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py index ef09a23840b..1417d071170 100644 --- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py @@ -54,17 +54,72 @@ from spyder.api.config.fonts import SpyderFontsMixin, SpyderFontType from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.base import _ +from spyder.plugins.variableexplorer.widgets.arrayeditor import get_idx_rect +from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.py3compat import (is_text_string, is_type_text_string, to_text_string) from spyder.utils.icon_manager import ima -from spyder.utils.qthelpers import ( - add_actions, keybinding, MENU_SEPARATOR, qapplication) -from spyder.plugins.variableexplorer.widgets.arrayeditor import get_idx_rect -from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog from spyder.utils.palette import QStylePalette +from spyder.utils.qthelpers import keybinding, qapplication from spyder.utils.stylesheet import AppStyle, MAC +# ============================================================================= +# ---- Constants +# ============================================================================= + +class DataframeEditorActions: + ConvertToBool = 'convert_to_bool_action' + ConvertToComplex = 'convert_to_complex_action' + ConvertToFloat = 'convert_to_float_action' + ConvertToInt = 'convert_to_int_action' + ConvertToStr = 'convert_to_str_action' + Copy = 'copy_action' + DuplicateColumn = 'duplicate_column_action' + DuplicateRow = 'duplicate_row_action' + Edit = 'edit_action' + EditHeader = 'edit_header_action' + EditIndex = 'edit_index_action' + Format = 'format_action' + InsertAbove = 'insert_above_action' + InsertAfter = 'insert_after_action' + InsertBefore = 'insert_before_action' + InsertBelow = 'insert_below_action' + Refresh = 'refresh_action' + RemoveColumn = 'remove_column_action' + RemoveRow = 'remove_row_action' + ResizeColumns = 'resize_columns_action' + ResizeRows = 'resize_rows_action' + ToggleBackgroundColor = 'toggle_background_color_action' + ToggleBackgroundColorGlobal = 'toggle_background_color_global_action' + + +class DataframeEditorMenus: + Context = 'context_menu' + ConvertTo = 'convert_to_submenu' + Header = 'header_context_menu' + Index = 'index_context_menu' + Options = 'options_menu' + + +class DataframeEditorWidgets: + OptionsToolButton = 'options_button_widget' + Toolbar = 'toolbar' + ToolbarStretcher = 'toolbar_stretcher' + + +class DataframeEditorContextMenuSections: + Edit = 'edit_section' + Row = 'row_section' + Column = 'column_section' + Convert = 'convert_section' + + +class DataframeEditorToolbarSections: + Row = 'row_section' + ColumnAndRest = 'column_section' + + # Supported real and complex number types REAL_NUMBER_TYPES = (float, int, np.int64, np.int32) COMPLEX_NUMBER_TYPES = (complex, np.complex64, np.complex128) @@ -92,6 +147,9 @@ BACKGROUND_STRING_ALPHA = 0.05 BACKGROUND_MISC_ALPHA = 0.3 +# ============================================================================= +# ---- Utility functions +# ============================================================================= def is_any_real_numeric_dtype(dtype) -> bool: """ @@ -122,6 +180,9 @@ def global_max(col_vals, index): max_col, min_col = zip(*col_vals_without_None) return max(max_col), min(min_col) +# ============================================================================= +# ---- Main classes +# ============================================================================= class DataFrameModel(QAbstractTableModel, SpyderFontsMixin): """ @@ -682,15 +743,14 @@ def contextMenuEvent(self, event): def setup_menu_header(self): """Setup context header menu.""" edit_header_action = self.create_action( - name=None, + name=DataframeEditorActions.EditHeader, text=_("Edit"), icon=ima.icon('edit'), triggered=self.edit_header_item, register_action=False ) - header_menu = [edit_header_action] - menu = self.create_menu('DataFrameView header menu', register=False) - add_actions(menu, header_menu) + menu = self.create_menu(DataframeEditorMenus.Header, register=False) + self.add_item_to_menu(edit_header_action, menu) return menu def refresh_menu(self): @@ -721,85 +781,87 @@ def refresh_menu(self): def setup_menu(self): """Setup context menu.""" + # ---- Create actions + self.resize_action = self.create_action( - name=None, + name=DataframeEditorActions.ResizeRows, text=_("Resize rows to contents"), icon=ima.icon('collapse_row'), triggered=lambda: self.resize_to_contents(rows=True), register_action=False ) self.resize_columns_action = self.create_action( - name=None, + name=DataframeEditorActions.ResizeColumns, text=_("Resize columns to contents"), icon=ima.icon('collapse_column'), triggered=self.resize_to_contents, register_action=False ) self.edit_action = self.create_action( - name=None, + name=DataframeEditorActions.Edit, text=_("Edit"), icon=ima.icon('edit'), triggered=self.edit_item, register_action=False ) self.insert_action_above = self.create_action( - name=None, + name=DataframeEditorActions.InsertAbove, text=_("Insert above"), icon=ima.icon('insert_above'), triggered=lambda: self.insert_item(axis=1, before_above=True), register_action=False ) self.insert_action_below = self.create_action( - name=None, + name=DataframeEditorActions.InsertBelow, text=_("Insert below"), icon=ima.icon('insert_below'), triggered=lambda: self.insert_item(axis=1, before_above=False), register_action=False ) self.insert_action_before = self.create_action( - name=None, + name=DataframeEditorActions.InsertBefore, text=_("Insert before"), icon=ima.icon('insert_before'), triggered=lambda: self.insert_item(axis=0, before_above=True), register_action=False ) self.insert_action_after = self.create_action( - name=None, + name=DataframeEditorActions.InsertAfter, text=_("Insert after"), icon=ima.icon('insert_after'), triggered=lambda: self.insert_item(axis=0, before_above=False), register_action=False ) self.remove_row_action = self.create_action( - name=None, + name=DataframeEditorActions.RemoveRow, text=_("Remove row"), icon=ima.icon('delete_row'), triggered=self.remove_item, register_action=False ) self.remove_col_action = self.create_action( - name=None, + name=DataframeEditorActions.RemoveColumn, text=_("Remove column"), icon=ima.icon('delete_column'), triggered=lambda: self.remove_item(axis=1), register_action=False ) self.duplicate_row_action = self.create_action( - name=None, + name=DataframeEditorActions.DuplicateRow, text=_("Duplicate row"), icon=ima.icon('duplicate_row'), triggered=lambda: self.duplicate_row_col(dup_row=True), register_action=False ) self.duplicate_col_action = self.create_action( - name=None, + name=DataframeEditorActions.DuplicateColumn, text=_("Duplicate column"), icon=ima.icon('duplicate_column'), triggered=lambda: self.duplicate_row_col(dup_row=False), register_action=False ) self.copy_action = self.create_action( - name=None, + name=DataframeEditorActions.Copy, text=_('Copy'), icon=ima.icon('editcopy'), triggered=self.copy, @@ -808,52 +870,60 @@ def setup_menu(self): self.copy_action.setShortcut(keybinding('Copy')) self.copy_action.setShortcutContext(Qt.WidgetShortcut) + # ---- Create "Convert to" submenu and actions + self.convert_to_menu = self.create_menu( - menu_id='Convert submenu', + menu_id=DataframeEditorMenus.ConvertTo, title=_('Convert to'), register=False ) - menu_actions = [ - self.copy_action, - self.edit_action, - MENU_SEPARATOR, - self.insert_action_above, - self.insert_action_below, - self.duplicate_row_action, - self.remove_row_action, - MENU_SEPARATOR, - self.insert_action_before, - self.insert_action_after, - self.duplicate_col_action, - self.remove_col_action, - MENU_SEPARATOR, - self.convert_to_menu.menuAction() - ] - functions = ( - (_("Bool"), bool), - (_("Complex"), complex), - (_("Int"), int), - (_("Float"), float), - (_("Str"), to_text_string) + (_("Bool"), bool, DataframeEditorActions.ConvertToBool), + (_("Complex"), complex, DataframeEditorActions.ConvertToComplex), + (_("Int"), int, DataframeEditorActions.ConvertToInt), + (_("Float"), float, DataframeEditorActions.ConvertToFloat), + (_("Str"), to_text_string, DataframeEditorActions.ConvertToStr) ) - self.convert_to_actions = [] - for text, func in functions: + for text, func, name in functions: def slot(): self.change_type(func) - self.convert_to_actions += [ - self.create_action( - name=None, - text=text, - triggered=slot, - context=Qt.WidgetShortcut, - register_action=False - ) - ] + action = self.create_action( + name=name, + text=text, + triggered=slot, + context=Qt.WidgetShortcut, + register_action=False + ) + self.add_item_to_menu(action, self.convert_to_menu) - menu = self.create_menu('DataFrameView menu', register=False) - add_actions(self.convert_to_menu, self.convert_to_actions) - add_actions(menu, menu_actions) + # ---- Create context menu and fill it + + menu = self.create_menu(DataframeEditorMenus.Context, register=False) + for action in [self.copy_action, self.edit_action]: + self.add_item_to_menu( + action, + menu, + section=DataframeEditorContextMenuSections.Edit + ) + for action in [self.insert_action_above, self.insert_action_below, + self.duplicate_row_action, self.remove_row_action]: + self.add_item_to_menu( + action, + menu, + section=DataframeEditorContextMenuSections.Row + ) + for action in [self.insert_action_before, self.insert_action_after, + self.duplicate_col_action, self.remove_col_action]: + self.add_item_to_menu( + action, + menu, + section=DataframeEditorContextMenuSections.Column + ) + self.add_item_to_menu( + self.convert_to_menu, + menu, + section=DataframeEditorContextMenuSections.Convert + ) return menu @@ -1636,7 +1706,7 @@ def __init__( self.data_function = data_function self.refresh_action = self.create_action( - name=None, + name=DataframeEditorActions.Refresh, text=_('Refresh'), icon=ima.icon('refresh'), tip=_('Refresh editor with current value of variable in console'), @@ -1645,19 +1715,19 @@ def __init__( ) self.refresh_action.setEnabled(self.data_function is not None) self.format_action = self.create_action( - name='Format', + name=DataframeEditorActions.Format, text=_('Format'), icon=self.create_icon('format_float'), tip=_('Set format of floating-point numbers'), triggered=self.change_format ) self.bgcolor_action = self.create_action( - name='Background color', + name=DataframeEditorActions.ToggleBackgroundColor, text=_('Background color'), toggled=self.change_bgcolor_enable ) self.bgcolor_global_action = self.create_action( - name='Background color global', + name=DataframeEditorActions.ToggleBackgroundColorGlobal, text=_('Column min/max'), toggled=self.toggle_bgcolor_global ) @@ -1694,7 +1764,10 @@ def setup_ui(self, title: str) -> None: """ # ---- Toolbar (to be filled later) - self.toolbar = self.create_toolbar('Editor toolbar', register=False) + self.toolbar = self.create_toolbar( + DataframeEditorWidgets.Toolbar, + register=False + ) # ---- Grid layout with tables and scrollbars showing data frame @@ -1807,37 +1880,60 @@ def set_data_and_check(self, data) -> bool: if self.table_header.rowHeight(0) == 0: self.table_header.setRowHeight(0, self.table_header.height()) - self.toolbar.clear() - self.toolbar.addAction(self.dataTable.insert_action_above) - self.toolbar.addAction(self.dataTable.insert_action_below) - self.toolbar.addAction(self.dataTable.duplicate_row_action) - self.toolbar.addAction(self.dataTable.remove_row_action) - self.toolbar.addSeparator() - self.toolbar.addAction(self.dataTable.insert_action_before) - self.toolbar.addAction(self.dataTable.insert_action_after) - self.toolbar.addAction(self.dataTable.duplicate_col_action) - self.toolbar.addAction(self.dataTable.remove_col_action) - - stretcher = self.create_stretcher('Toolbar stretcher') - self.toolbar.addWidget(stretcher) - - self.toolbar.addAction(self.dataTable.resize_action) - self.toolbar.addAction(self.dataTable.resize_columns_action) - self.toolbar.addAction(self.refresh_action) - - options_menu = self.create_menu('Options menu', register=False) - options_menu.add_action(self.bgcolor_action) - options_menu.add_action(self.bgcolor_global_action) - options_menu.add_action(self.format_action) + stretcher = self.create_stretcher( + DataframeEditorWidgets.ToolbarStretcher + ) + + options_menu = self.create_menu( + DataframeEditorMenus.Options, + register=False) + for action in [self.bgcolor_action, self.bgcolor_global_action, + self.format_action]: + self.add_item_to_menu(action, options_menu) + options_button = self.create_toolbutton( - name='Options toolbutton', + name=DataframeEditorWidgets.OptionsToolButton, text=_('Options'), icon=ima.icon('tooloptions'), register=False ) options_button.setPopupMode(QToolButton.InstantPopup) options_button.setMenu(options_menu) - self.toolbar.addWidget(options_button) + + self.toolbar.clear() + self.toolbar._section_items.clear() + self.toolbar._item_map.clear() + + for item in [ + self.dataTable.insert_action_above, + self.dataTable.insert_action_below, + self.dataTable.duplicate_row_action, + self.dataTable.remove_row_action + ]: + self.add_item_to_toolbar( + item, + self.toolbar, + section=DataframeEditorToolbarSections.Row + ) + + for item in [ + self.dataTable.insert_action_before, + self.dataTable.insert_action_after, + self.dataTable.duplicate_col_action, + self.dataTable.remove_col_action, + stretcher, + self.dataTable.resize_action, + self.dataTable.resize_columns_action, + self.refresh_action, + options_button + ]: + self.add_item_to_toolbar( + item, + self.toolbar, + section=DataframeEditorToolbarSections.ColumnAndRest + ) + + self.toolbar._render() return True @@ -1851,15 +1947,14 @@ def save_and_close_enable(self, top_left, bottom_right): def setup_menu_header(self, header): """Setup context header menu.""" edit_header_action = self.create_action( - name=None, + name=DataframeEditorActions.EditIndex, text=_("Edit"), icon=ima.icon('edit'), triggered=lambda: self.edit_header_item(header=header), register_action=False ) - header_menu = [edit_header_action] - menu = self.create_menu('Context header menu', register=False) - add_actions(menu, header_menu) + menu = self.create_menu(DataframeEditorMenus.Index, register=False) + self.add_item_to_menu(edit_header_action, menu) return menu @Slot() diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py index 86f8847a06b..a43bd3b5fd3 100644 --- a/spyder/plugins/variableexplorer/widgets/main_widget.py +++ b/spyder/plugins/variableexplorer/widgets/main_widget.py @@ -452,9 +452,9 @@ def _setup(self): if action.defaultWidget() == self.get_options_menu_button(): options_menu_action = action - corner_widget.insertAction(options_menu_action, self.search_action) - corner_widget.insertAction(options_menu_action, self.filter_button) - corner_widget.insertAction(options_menu_action, self.refresh_action) + for action in [self.search_action, self.filter_button, + self.refresh_action]: + corner_widget.insertAction(options_menu_action, action) def update_actions(self): """Update the actions.""" diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py index f96356366f6..f68e32d45d5 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py @@ -30,11 +30,27 @@ DEFAULT_ATTR_COLS, DEFAULT_ATTR_DETAILS, ToggleColumnTreeView, TreeItem, TreeModel, TreeProxyModel) from spyder.utils.icon_manager import ima -from spyder.utils.qthelpers import add_actions, qapplication +from spyder.utils.qthelpers import qapplication from spyder.utils.stylesheet import AppStyle, MAC from spyder.widgets.simplecodeeditor import SimpleCodeEditor +class ObjectExplorerActions: + Refresh = 'refresh_action' + ShowCallable = 'show_callable_action' + ShowSpecialAttributes = 'show_special_attributes_action' + + +class ObjectExplorerMenus: + Options = 'options_menu' + + +class ObjectExplorerWidgets: + OptionsToolButton = 'options_button_widget' + Toolbar = 'toolbar' + ToolbarStretcher = 'toolbar_stretcher' + + logger = logging.getLogger(__name__) @@ -166,8 +182,8 @@ def set_value(self, obj): obj_tree_header.setStretchLastSection(False) # Add menu item for toggling columns to the Options menu - add_actions(self.show_cols_submenu, - self.obj_tree.toggle_column_actions_group.actions()) + for action in self.obj_tree.toggle_column_actions_group.actions(): + self.add_item_to_menu(action, self.show_cols_submenu) column_visible = [col.col_visible for col in self._attr_cols] for idx, visible in enumerate(column_visible): elem = self.obj_tree.toggle_column_actions_group.actions()[idx] @@ -200,13 +216,8 @@ def do_nothing(): # a placeholder here. pass - self.toolbar = self.create_toolbar( - 'Object explorer toolbar', register=False - ) - - # Show/hide callable objects self.toggle_show_callable_action = self.create_action( - name='Show callable attributes', + name=ObjectExplorerActions.ShowCallable, text=_("Show callable attributes"), icon=ima.icon("class"), tip=_("Shows/hides attributes that are callable " @@ -215,11 +226,9 @@ def do_nothing(): option='show_callable_attributes', register_action=False ) - self.toolbar.add_item(self.toggle_show_callable_action) - # Show/hide special attributes self.toggle_show_special_attribute_action = self.create_action( - name='Show special attributes', + name=ObjectExplorerActions.ShowSpecialAttributes, text=_("Show __special__ attributes"), icon=ima.icon("private2"), tip=_("Shows or hides __special__ attributes"), @@ -227,34 +236,47 @@ def do_nothing(): option='show_special_attributes', register_action=False ) - self.toolbar.add_item(self.toggle_show_special_attribute_action) - stretcher = self.create_stretcher('Toolbar stretcher') - self.toolbar.add_item(stretcher) + stretcher = self.create_stretcher( + ObjectExplorerWidgets.ToolbarStretcher + ) - self.refresh_button = self.create_toolbutton( - name='Refresh toolbutton', + self.refresh_action = self.create_action( + name=ObjectExplorerActions.Refresh, + text=_('Refresh editor with current value of variable in console'), icon=ima.icon('refresh'), - tip=_('Refresh editor with current value of variable in console'), triggered=self.refresh_editor, - register=False + register_action=False ) - self.refresh_button.setEnabled(self.data_function is not None) - self.toolbar.add_item(self.refresh_button) + self.refresh_action.setEnabled(self.data_function is not None) + self.show_cols_submenu = self.create_menu( + ObjectExplorerMenus.Options, + register=False + ) self.options_button = self.create_toolbutton( - name='Options toolbutton', + name=ObjectExplorerWidgets.OptionsToolButton, text=_('Options'), icon=ima.icon('tooloptions'), register=False ) self.options_button.setPopupMode(QToolButton.InstantPopup) + self.options_button.setMenu(self.show_cols_submenu) - self.show_cols_submenu = self.create_menu( - 'Options menu', register=False + self.toolbar = self.create_toolbar( + ObjectExplorerWidgets.Toolbar, + register=False ) - self.options_button.setMenu(self.show_cols_submenu) - self.toolbar.add_item(self.options_button) + + for item in [ + self.toggle_show_callable_action, + self.toggle_show_special_attribute_action, + stretcher, + self.refresh_action, + self.options_button + ]: + self.add_item_to_toolbar(item, self.toolbar) + self.toolbar._render() def _show_callable_attributes(self, value: bool): @@ -290,8 +312,9 @@ def _setup_views(self): self.central_splitter.addWidget(bottom_pane_widget) group_box = QGroupBox(_("Details")) - group_box.setStyleSheet('QGroupBox ' - '{margin-bottom: 0px; margin-right: -2px;}') + group_box.setStyleSheet( + 'QGroupBox {margin-bottom: 0px; margin-right: -2px;}' + ) bottom_layout.addWidget(group_box) h_group_layout = QHBoxLayout() diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py index 05e6e931c99..f8784bc3d2f 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/tests/test_objectexplorer.py @@ -19,7 +19,6 @@ import numpy as np import pytest from qtpy.QtCore import Qt -from qtpy.QtWidgets import QMessageBox # Local imports from spyder.config.manager import CONF @@ -182,13 +181,13 @@ class DataclassForTesting: quantity: int -def test_objectexplorer_refreshbutton_disabled(): +def test_objectexplorer_refreshaction_disabled(): """ - Test that the refresh button is disabled by default. + Test that the refresh action is disabled by default. """ data = DataclassForTesting('lemon', 0.15, 5) editor = ObjectExplorer(data, name='data') - assert not editor.refresh_button.isEnabled() + assert not editor.refresh_action.isEnabled() def test_objectexplorer_refresh(): @@ -204,7 +203,7 @@ def test_objectexplorer_refresh(): root = model.index(0, 0) assert model.data(model.index(0, 0, root), Qt.DisplayRole) == 'name' assert model.data(model.index(0, 3, root), Qt.DisplayRole) == 'lemon' - assert editor.refresh_button.isEnabled() + assert editor.refresh_action.isEnabled() editor.refresh_editor() model = editor.obj_tree.model() root = model.index(0, 0) @@ -226,7 +225,7 @@ def datafunc(): with patch('spyder.plugins.variableexplorer.widgets.objectexplorer' '.objectexplorer.QMessageBox.critical') as mock_critical: with qtbot.waitSignal(editor.rejected, timeout=0): - editor.refresh_button.click() + editor.refresh_action.trigger() mock_critical.assert_called_once() diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py index 8d6362d22d7..8f764938c6e 100644 --- a/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py +++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/toggle_column_mixin.py @@ -14,17 +14,19 @@ # Third-party imports from qtpy.QtCore import Qt, Slot -from qtpy.QtWidgets import (QAbstractItemView, QAction, QActionGroup, - QHeaderView, QTableWidget, QTreeView, QTreeWidget) +from qtpy.QtWidgets import ( + QAbstractItemView, QActionGroup, QHeaderView, QTableWidget, QTreeView, + QTreeWidget) # Local imports +from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.config.base import _ logger = logging.getLogger(__name__) # Toggle mixin -class ToggleColumnMixIn(object): +class ToggleColumnMixIn(SpyderWidgetMixin): """ Adds actions to a QTableView that can show/hide columns by right clicking on the header @@ -59,21 +61,24 @@ def add_header_context_menu(self, checked=None, checkable=None, column_label = self.model().headerData(col, Qt.Horizontal, Qt.DisplayRole) logger.debug("Adding: col {}: {}".format(col, column_label)) - action = QAction(str(column_label), - self.toggle_column_actions_group, - checkable=checkable.get(column_label, True), - enabled=enabled.get(column_label, True), - toolTip=_("Shows or hides " - "the {} column").format(column_label)) func = self.__make_show_column_function(col) - self.__toggle_functions.append(func) # keep reference - horizontal_header.addAction(action) is_checked = checked.get( column_label, - not horizontal_header.isSectionHidden(col)) + not horizontal_header.isSectionHidden(col) + ) + action = self.create_action( + name=f'show_{column_label}_column', + text=str(column_label), + tip=_("Shows or hides the {} column").format(column_label), + toggled=func, + initial=is_checked, + parent=self.toggle_column_actions_group + ) + action.setCheckable(checkable.get(column_label, True)) + action.setEnabled(enabled.get(column_label, True)) + self.__toggle_functions.append(func) # keep reference + horizontal_header.addAction(action) horizontal_header.setSectionHidden(col, not is_checked) - action.setChecked(is_checked) - action.toggled.connect(func) def get_header_context_menu_actions(self): """Returns the actions of the context menu of the header.""" diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py index 1fb996afeb7..608d18d3d56 100644 --- a/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py +++ b/spyder/plugins/variableexplorer/widgets/tests/test_dataframeeditor.py @@ -130,6 +130,7 @@ def test_dataframe_to_type(qtbot): view.setCurrentIndex(view.model().index(0, 0)) # Show context menu, go down until `Convert to`, and open submenu + view.menu.render() view.menu.show() for _ in range(100): activeAction = view.menu.activeAction() @@ -657,6 +658,7 @@ def create_view(qtbot, value): model_index = view.header_class.model().index(0, 2) view.header_class.setCurrentIndex(model_index) qtbot.wait(200) + view.menu_header_h.render() view.menu_header_h.show() qtbot.keyPress(view.menu_header_h, Qt.Key_Down) qtbot.keyPress(view.menu_header_h, Qt.Key_Return) @@ -671,6 +673,7 @@ def create_view(qtbot, value): index = editor.table_index.model() model_index = editor.table_index.model().index(5, 0) editor.table_index.setCurrentIndex(model_index) + editor.menu_header_v.render() editor.menu_header_v.show() qtbot.wait(200) qtbot.keyPress(editor.menu_header_v, Qt.Key_Down) diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py index a3699bb7dda..d6544e61eb1 100644 --- a/spyder/widgets/collectionseditor.py +++ b/spyder/widgets/collectionseditor.py @@ -51,7 +51,7 @@ is_type_text_string) from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home -from spyder.utils.qthelpers import add_actions, MENU_SEPARATOR, mimedata2url +from spyder.utils.qthelpers import mimedata2url from spyder.utils.stringmatching import get_search_scores, get_search_regex from spyder.plugins.variableexplorer.widgets.collectionsdelegate import ( CollectionsDelegate) @@ -62,6 +62,51 @@ from spyder.utils.stylesheet import AppStyle, MAC +class CollectionsEditorActions: + Copy = 'copy_action' + Duplicate = 'duplicate_action' + Edit = 'edit_action' + Histogram = 'histogram_action' + Insert = 'insert_action' + InsertAbove = 'insert_above_action' + InsertBelow = 'insert_below_action' + Paste = 'paste_action' + Plot = 'plot_action' + Refresh = 'refresh_action' + Remove = 'remove_action' + Rename = 'rename_action' + ResizeColumns = 'resize_columns_action' + ResizeRows = 'resize_rows_action' + Save = 'save_action' + ShowImage = 'show_image_action' + ViewObject = 'view_object_action' + + +class CollectionsEditorMenus: + Context = 'context_menu' + ContextIfEmpty = 'context_menu_if_empty' + ConvertTo = 'convert_to_submenu' + Header = 'header_context_menu' + Index = 'index_context_menu' + Options = 'options_menu' + + +class CollectionsEditorWidgets: + Toolbar = 'toolbar' + ToolbarStretcher = 'toolbar_stretcher' + + +class CollectionsEditorContextMenuSections: + Edit = 'edit_section' + AddRemove = 'add_remove_section' + View = 'view_section' + + +class CollectionsEditorToolbarSections: + AddDelete = 'add_delete_section' + ViewAndRest = 'view_section' + + # Maximum length of a serialized variable to be set in the kernel MAX_SERIALIZED_LENGHT = 1e6 @@ -644,42 +689,42 @@ def setup_table(self): def setup_menu(self): """Setup actions and context menu""" self.resize_action = self.create_action( - name=None, + name=CollectionsEditorActions.ResizeRows, text=_("Resize rows to contents"), icon=ima.icon('collapse_row'), triggered=self.resizeRowsToContents, register_action=False ) self.resize_columns_action = self.create_action( - name=None, + name=CollectionsEditorActions.ResizeColumns, text=_("Resize columns to contents"), icon=ima.icon('collapse_column'), triggered=self.resize_column_contents, register_action=False ) self.paste_action = self.create_action( - name=None, + name=CollectionsEditorActions.ResizeRows, text=_("Paste"), icon=ima.icon('editpaste'), triggered=self.paste, register_action=False ) self.copy_action = self.create_action( - name=None, + name=CollectionsEditorActions.Copy, text=_("Copy"), icon=ima.icon('editcopy'), triggered=self.copy, register_action=False ) self.edit_action = self.create_action( - name=None, + name=CollectionsEditorActions.Edit, text=_("Edit"), icon=ima.icon('edit'), triggered=self.edit_item, register_action=False ) self.plot_action = self.create_action( - name=None, + name=CollectionsEditorActions.Plot, text=_("Plot"), icon=ima.icon('plot'), triggered=lambda: self.plot_item('plot'), @@ -687,7 +732,7 @@ def setup_menu(self): ) self.plot_action.setVisible(False) self.hist_action = self.create_action( - name=None, + name=CollectionsEditorActions.Histogram, text=_("Histogram"), icon=ima.icon('hist'), triggered=lambda: self.plot_item('hist'), @@ -695,7 +740,7 @@ def setup_menu(self): ) self.hist_action.setVisible(False) self.imshow_action = self.create_action( - name=None, + name=CollectionsEditorActions.ShowImage, text=_("Show image"), icon=ima.icon('imshow'), triggered=self.imshow_item, @@ -703,7 +748,7 @@ def setup_menu(self): ) self.imshow_action.setVisible(False) self.save_array_action = self.create_action( - name=None, + name=CollectionsEditorActions.Save, text=_("Save"), icon=ima.icon('filesave'), triggered=self.save_array, @@ -711,82 +756,91 @@ def setup_menu(self): ) self.save_array_action.setVisible(False) self.insert_action = self.create_action( - name=None, + name=CollectionsEditorActions.Insert, text=_("Insert"), icon=ima.icon('insert'), triggered=lambda: self.insert_item(below=False), register_action=False ) self.insert_action_above = self.create_action( - name=None, + name=CollectionsEditorActions.InsertAbove, text=_("Insert above"), icon=ima.icon('insert_above'), triggered=lambda: self.insert_item(below=False), register_action=False ) self.insert_action_below = self.create_action( - name=None, + name=CollectionsEditorActions.InsertBelow, text=_("Insert below"), icon=ima.icon('insert_below'), triggered=lambda: self.insert_item(below=True), register_action=False ) self.remove_action = self.create_action( - name=None, + name=CollectionsEditorActions.Remove, text=_("Remove"), icon=ima.icon('editdelete'), triggered=self.remove_item, register_action=False ) self.rename_action = self.create_action( - name=None, + name=CollectionsEditorActions.Rename, text=_("Rename"), icon=ima.icon('rename'), triggered=self.rename_item, register_action=False ) self.duplicate_action = self.create_action( - name=None, + name=CollectionsEditorActions.Duplicate, text=_("Duplicate"), icon=ima.icon('edit_add'), triggered=self.duplicate_item, register_action=False ) self.view_action = self.create_action( - name=None, + name=CollectionsEditorActions.ViewObject, text=_("View with the Object Explorer"), icon=ima.icon('outline_explorer'), triggered=self.view_item, register_action=False ) - menu = self.create_menu('Editor menu', register=False) - menu_actions = [ - self.copy_action, - self.paste_action, - self.rename_action, - self.edit_action, - self.save_array_action, - MENU_SEPARATOR, - self.insert_action, - self.insert_action_above, - self.insert_action_below, - self.duplicate_action, - self.remove_action, - MENU_SEPARATOR, - self.view_action, - self.plot_action, - self.hist_action, - self.imshow_action - ] - add_actions(menu, menu_actions) - - self.empty_ws_menu = self.create_menu('Empty ws', register=False) - add_actions( - self.empty_ws_menu, - [self.insert_action, self.paste_action] + menu = self.create_menu( + CollectionsEditorMenus.Context, + register=False) + + for action in [self.copy_action, self.paste_action, self.rename_action, + self.edit_action, self.save_array_action]: + self.add_item_to_menu( + action, + menu, + section=CollectionsEditorContextMenuSections.Edit ) + for action in [self.insert_action, self.insert_action_above, + self.insert_action_below, self.duplicate_action, + self.remove_action]: + self.add_item_to_menu( + action, + menu, + section=CollectionsEditorContextMenuSections.AddRemove + ) + + for action in [self.view_action, self.plot_action, + self.hist_action, self.imshow_action]: + self.add_item_to_menu( + action, + menu, + section=CollectionsEditorContextMenuSections.View + ) + + self.empty_ws_menu = self.create_menu( + CollectionsEditorMenus.ContextIfEmpty, + register=False) + + for action in [self.insert_action, self.paste_action]: + self.add_item_to_menu(action, self.empty_ws_menu) + return menu # ------ Remote/local API ------------------------------------------------- @@ -1503,14 +1557,14 @@ def __init__(self, parent, data, namespacebrowser=None, QWidget.__init__(self, parent) if remote: self.editor = RemoteCollectionsEditorTableView( - self, data, readonly) + self, data, readonly, create_menu=True) else: self.editor = CollectionsEditorTableView( self, data, namespacebrowser, data_function, readonly, title ) self.refresh_action = self.create_action( - name=None, + name=CollectionsEditorActions.Refresh, text=_('Refresh'), icon=ima.icon('refresh'), tip=_('Refresh editor with current value of variable in console'), @@ -1518,21 +1572,45 @@ def __init__(self, parent, data, namespacebrowser=None, register_action=None ) - toolbar = self.create_toolbar('Editor toolbar', register=False) - toolbar.addAction(self.editor.insert_action) - toolbar.addAction(self.editor.insert_action_above) - toolbar.addAction(self.editor.insert_action_below) - toolbar.addAction(self.editor.duplicate_action) - toolbar.addAction(self.editor.remove_action) - toolbar.addSeparator() - toolbar.addAction(self.editor.view_action) - toolbar.addAction(self.editor.plot_action) - toolbar.addAction(self.editor.hist_action) - toolbar.addAction(self.editor.imshow_action) - toolbar.addWidget(self.create_stretcher('Toolbar stretcher')) - toolbar.addAction(self.editor.resize_action) - toolbar.addAction(self.editor.resize_columns_action) - toolbar.addAction(self.refresh_action) + toolbar = self.create_toolbar( + CollectionsEditorWidgets.Toolbar, + register=False + ) + + stretcher = self.create_stretcher( + CollectionsEditorWidgets.ToolbarStretcher + ) + + for item in [ + self.editor.insert_action, + self.editor.insert_action_above, + self.editor.insert_action_below, + self.editor.duplicate_action, + self.editor.remove_action + ]: + self.add_item_to_toolbar( + item, + toolbar, + section=CollectionsEditorToolbarSections.AddDelete + ) + + for item in [ + self.editor.view_action, + self.editor.plot_action, + self.editor.hist_action, + self.editor.imshow_action, + stretcher, + self.editor.resize_action, + self.editor.resize_columns_action, + self.refresh_action + ]: + self.add_item_to_toolbar( + item, + toolbar, + section=CollectionsEditorToolbarSections.ViewAndRest + ) + + toolbar._render() # Update the toolbar actions state self.editor.refresh_menu() @@ -2082,7 +2160,7 @@ def editor_test(): """Test Collections editor.""" dialog = CollectionsEditor() dialog.setup(get_test_data()) - dialog.show() + dialog.exec_() def remote_editor_test(): @@ -2098,7 +2176,7 @@ def remote_editor_test(): remote = make_remote_view(get_test_data(), settings) dialog = CollectionsEditor() dialog.setup(remote, remote=True) - dialog.show() + dialog.exec_() if __name__ == "__main__": @@ -2107,4 +2185,3 @@ def remote_editor_test(): app = qapplication() # analysis:ignore editor_test() remote_editor_test() - app.exec_()