Source code for spyder.api.widgets.toolbars

# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Spyder API toolbar widgets.
"""

# Standard library imports
from collections import OrderedDict
import os
import sys
from typing import Dict, List, Optional, Tuple, Union
import uuid

# Third part imports
from qtpy.QtCore import QEvent, QObject, QSize, Qt, Signal
from qtpy.QtWidgets import (
    QAction, QProxyStyle, QStyle, QToolBar, QToolButton, QWidget)

# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.translations import _
from spyder.api.widgets.menus import SpyderMenu, SpyderMenuProxyStyle
from spyder.utils.icon_manager import ima
from spyder.utils.qthelpers import SpyderAction
from spyder.utils.stylesheet import (
    APP_TOOLBAR_STYLESHEET, PANES_TOOLBAR_STYLESHEET)


# Generic type annotations
ToolbarItem = Union[SpyderAction, QWidget]
ToolbarItemEntry = Tuple[ToolbarItem, Optional[str], Optional[str],
                         Optional[str]]

# ---- Constants
# ----------------------------------------------------------------------------
class ToolbarLocation:
    Top = Qt.TopToolBarArea
    Bottom = Qt.BottomToolBarArea


# ---- Event filters
# ----------------------------------------------------------------------------
[docs] class ToolTipFilter(QObject): """ Filter tool tip events on toolbuttons. """
[docs] def eventFilter(self, obj, event): event_type = event.type() action = obj.defaultAction() if isinstance(obj, QToolButton) else None if event_type == QEvent.ToolTip and action is not None: if action.tip is None: return action.text_beside_icon return QObject.eventFilter(self, obj, event)
# ---- Styles # ----------------------------------------------------------------------------
[docs] class ToolbarStyle(QProxyStyle): # The toolbar type. This can be 'Application' or 'MainWidget' TYPE = None
[docs] def pixelMetric(self, pm, option, widget): """ Adjust size of toolbar extension button (in pixels). From https://stackoverflow.com/a/27042352/438386 """ # Important: These values need to be updated in case we change the size # of our toolbar buttons in utils/stylesheet.py. That's because Qt only # allow to set them in pixels here, not em's. if pm == QStyle.PM_ToolBarExtensionExtent: if self.TYPE == 'Application': if os.name == 'nt': return 40 elif sys.platform == 'darwin': return 54 else: return 57 elif self.TYPE == 'MainWidget': if os.name == 'nt': return 36 elif sys.platform == 'darwin': return 42 else: return 44 else: print("Unknown toolbar style type") # spyder: test-skip return super().pixelMetric(pm, option, widget)
# ---- Toolbars # ----------------------------------------------------------------------------
[docs] class SpyderToolbar(QToolBar): """ Spyder Toolbar. This class provides toolbars with some predefined functionality. """ sig_is_rendered = Signal() """ This signal is emitted to let other objects know that the toolbar is now rendered. """ def __init__(self, parent, title): super().__init__(parent=parent) # Attributes self._title = title self._section_items = OrderedDict() self._item_map: Dict[str, ToolbarItem] = {} self._pending_items: Dict[str, List[ToolbarItemEntry]] = {} self._default_section = "default_section" self._filter = None self.setWindowTitle(title) # Set attributes for extension button. # From https://stackoverflow.com/a/55412455/438386 ext_button = self.findChild(QToolButton, "qt_toolbar_ext_button") ext_button.setIcon(ima.icon('toolbar_ext_button')) ext_button.setToolTip(_("More")) # Set style for extension button menu (not all extension buttons have # it). if ext_button.menu(): ext_button.menu().setStyleSheet( SpyderMenu._generate_stylesheet().toString() ) ext_button_menu_style = SpyderMenuProxyStyle(None) ext_button_menu_style.setParent(self) ext_button.menu().setStyle(ext_button_menu_style)
[docs] def add_item( self, action_or_widget: ToolbarItem, section: Optional[str] = None, before: Optional[str] = None, before_section: Optional[str] = None, omit_id: bool = False ): """ Add action or widget item to given toolbar `section`. Parameters ---------- item: SpyderAction or QWidget The item to add to the `toolbar`. toolbar_id: str or None The application toolbar unique string identifier. section: str or None The section id in which to insert the `item` on the `toolbar`. before: str or None Make the item appear before another given item. before_section: str or None Make the item defined section appear before another given section (must be already defined). omit_id: bool If True, then the toolbar will check if the item to add declares an id, False otherwise. This flag exists only for items added on Spyder 4 plugins. Default: False """ item_id = None if ( isinstance(action_or_widget, SpyderAction) or hasattr(action_or_widget, 'action_id') ): item_id = action_or_widget.action_id elif hasattr(action_or_widget, 'ID'): item_id = action_or_widget.ID if not omit_id and item_id is None and action_or_widget is not None: raise SpyderAPIError( f'Item {action_or_widget} must declare an ID attribute.' ) if before is not None: if before not in self._item_map: before_pending_items = self._pending_items.get(before, []) before_pending_items.append( (action_or_widget, section, before, before_section)) self._pending_items[before] = before_pending_items return else: before = self._item_map[before] if section is None: section = self._default_section action_or_widget._section = section if before is not None: if section == self._default_section: action_or_widget._section = before._section section = before._section if section not in self._section_items: self._section_items[section] = [action_or_widget] else: if before is not None: new_actions_or_widgets = [] for act_or_wid in self._section_items[section]: if act_or_wid == before: new_actions_or_widgets.append(action_or_widget) new_actions_or_widgets.append(act_or_wid) self._section_items[section] = new_actions_or_widgets else: self._section_items[section].append(action_or_widget) if (before_section is not None and before_section in self._section_items): new_sections_keys = [] for sec in self._section_items.keys(): if sec == before_section: new_sections_keys.append(section) if sec != section: new_sections_keys.append(sec) self._section_items = OrderedDict( (section_key, self._section_items[section_key]) for section_key in new_sections_keys) if item_id is not None: self._item_map[item_id] = action_or_widget if item_id in self._pending_items: item_pending = self._pending_items.pop(item_id) for item, section, before, before_section in item_pending: self.add_item(item, section=section, before=before, before_section=before_section)
[docs] def remove_item(self, item_id: str): """Remove action or widget from toolbar by id.""" try: item = self._item_map.pop(item_id) for section in list(self._section_items.keys()): section_items = self._section_items[section] if item in section_items: section_items.remove(item) if len(section_items) == 0: self._section_items.pop(section) self.clear() self.render() except KeyError: pass
[docs] def render(self): """Create the toolbar taking into account sections and locations.""" sec_items = [] for sec, items in self._section_items.items(): for item in items: sec_items.append([sec, item]) sep = QAction(self) sep.setSeparator(True) sec_items.append((None, sep)) if sec_items: sec_items.pop() for (sec, item) in sec_items: if isinstance(item, QAction): add_method = super().addAction else: add_method = super().addWidget add_method(item) if isinstance(item, QAction): widget = self.widgetForAction(item) if self._filter is not None: widget.installEventFilter(self._filter) text_beside_icon = getattr(item, 'text_beside_icon', False) if text_beside_icon: widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) if item.isCheckable(): widget.setCheckable(True) self.sig_is_rendered.emit()
[docs] class ApplicationToolbar(SpyderToolbar): """ Spyder Main application Toolbar. """ ID = None """ Unique string toolbar identifier. This is used by Qt to be able to save and restore the state of widgets. """ def __init__(self, parent, toolbar_id, title): super().__init__(parent=parent, title=title) self.ID = toolbar_id self._style = ToolbarStyle(None) self._style.TYPE = 'Application' self._style.setParent(self) self.setStyle(self._style) self.setStyleSheet(str(APP_TOOLBAR_STYLESHEET)) def __str__(self): return f"ApplicationToolbar('{self.ID}')" def __repr__(self): return f"ApplicationToolbar('{self.ID}')"
[docs] class MainWidgetToolbar(SpyderToolbar): """ Spyder Widget toolbar class. A toolbar used in Spyder dockable plugins to add internal toolbars to their interface. """ ID = None """ Unique string toolbar identifier. """ def __init__(self, parent=None, title=None): super().__init__(parent, title=title or '') self._icon_size = QSize(16, 16) # Setup self.setObjectName("main_widget_toolbar_{}".format( str(uuid.uuid4())[:8])) self.setFloatable(False) self.setMovable(False) self.setContextMenuPolicy(Qt.PreventContextMenu) self.setIconSize(self._icon_size) self._style = ToolbarStyle(None) self._style.TYPE = 'MainWidget' self._style.setParent(self) self.setStyle(self._style) self.setStyleSheet(str(PANES_TOOLBAR_STYLESHEET)) self._filter = ToolTipFilter() def set_icon_size(self, icon_size): self._icon_size = icon_size self.setIconSize(icon_size)