Source code for spyder.api.widgets.status

# -----------------------------------------------------------------------------
# Copyright (c) 2020- Spyder Project Contributors
#
# Released under the terms of the MIT License
# (see LICENSE.txt in the project root directory for details)
# -----------------------------------------------------------------------------

"""Status bar widgets API."""

from __future__ import annotations

# Standard library import
from typing import TYPE_CHECKING

# Third party imports
import qstylizer.parser
from qtpy import PYSIDE2
from qtpy.QtCore import Qt, QSize, QTimer, Signal
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget

# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.utils.palette import SpyderPalette
from spyder.utils.qthelpers import create_waitspinner
from spyder.utils.stylesheet import MAC

if TYPE_CHECKING:
    import qstylizer.style.StyleSheet
    from qtpy.QtGui import QCloseEvent, QEnterEvent, QEvent, QMouseEvent


[docs] class StatusBarWidget(QWidget, SpyderWidgetMixin): """ Base class for status bar widgets. These widgets consist by default of an icon, a label and a spinner, which are organized from left to right in that order. You can also add any other :class:`QWidget` to this layout by setting the :attr:`CUSTOM_WIDGET_CLASS` class attribute. It'll be put between the label and the spinner. """ ID: str | None = None """ Unique string identifier name for this widget. """ CUSTOM_WIDGET_CLASS: type[QWidget] | None = None """ A custom widget class object to add to the default layout, or ``None`` to not add one. """ INTERACT_ON_CLICK: bool = False """ If ``True``, the user can interact with widget when clicking it (for example, to show a menu). If ``False``, the default, the widget is not interactive. """ sig_clicked: Signal = Signal() """ Signal emitted when the widget is clicked. """
[docs] def __init__( self, parent: QWidget | None = None, show_icon: bool = True, show_label: bool = True, show_spinner: bool = False, ) -> None: """ Base class for status bar widgets. These are composed of the following widgets, which are arranged in a :class:`QHBoxLayout` from left to right: * Icon * Label * Custom :class:`QWidget` * Spinner Parameters ---------- parent: QWidget | None, optional The parent widget of this one, or ``None`` (default). show_icon: bool, optional If ``True`` (default), show an icon in the widget. If ``False``, don't show an icon. show_label: bool, optional If ``True`` (default), show a label in the widget. If ``False``, don't show a label. show_spinner: bool, optional If ``True``, show a progress spinner in the widget. If ``False`` (default), don't show a spinner. Notes ----- * To use an icon, you need to redefine the :meth:`get_icon` method. * To use a label, you need to call :meth:`set_value`. """ if not PYSIDE2: super().__init__(parent, class_parent=parent) else: QWidget.__init__(self, parent) SpyderWidgetMixin.__init__(self, class_parent=parent) self._parent = parent self.show_icon = show_icon self.show_label = show_label self.show_spinner = show_spinner self.value = None self.label_icon = None self.label_value = None self.spinner = None self.custom_widget = None # In case the widget has an associated menu self.menu = None self._set_layout() self._css = self._generate_stylesheet() self.setStyleSheet(self._css.toString())
# ---- Private API # ------------------------------------------------------------------------- def _set_layout(self) -> None: """Set layout for default widgets.""" # Icon if self.show_icon: self._icon = self.get_icon() self._pixmap = None self._icon_size = QSize(16, 16) # Should this be adjustable? self.label_icon = QLabel(self) self.set_icon() # Label if self.show_label: self.label_value = QLabel(self) self.set_value("") self.label_value.setAlignment(Qt.AlignRight | Qt.AlignVCenter) # Custom widget if self.CUSTOM_WIDGET_CLASS: if not issubclass(self.CUSTOM_WIDGET_CLASS, QWidget): raise SpyderAPIError( "Any custom status widget must subclass QWidget!" ) self.custom_widget = self.CUSTOM_WIDGET_CLASS(self._parent) # Spinner if self.show_spinner: self.spinner = create_waitspinner(size=14, parent=self) self.spinner.hide() # Layout setup layout = QHBoxLayout(self) layout.setSpacing(0) # Reduce space between icon and label if self.show_icon: layout.addWidget(self.label_icon) if self.show_label: layout.addWidget(self.label_value) if self.custom_widget: layout.addWidget(self.custom_widget) if self.show_spinner: layout.addWidget(self.spinner) layout.addSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.setAlignment(Qt.AlignVCenter) # Setup self.update_tooltip() def _generate_stylesheet(self) -> qstylizer.style.StyleSheet: """Generate the widget's stylesheet.""" # Remove opacity that comes from QDarkstyle. # This work around is necessary because qstylizer doesn't have support # for the opacity property. initial_css = "QToolTip {opacity: 255;}" css = qstylizer.parser.parse(initial_css) # Make style match the one set for other tooltips in the app css.QToolTip.setValues( color=SpyderPalette.COLOR_TEXT_1, backgroundColor=SpyderPalette.COLOR_ACCENT_2, border="none", padding="1px 2px", ) return css # ---- Public API # -------------------------------------------------------------------------
[docs] def get_icon(self) -> QIcon | None: """ Get the widget's icon. Returns ------- QIcon | None The widget's icon object, or ``None`` if it doesn't have one. """ return None
[docs] def set_icon(self) -> None: """ Set the icon for the status bar widget. Returns ------- None """ if self.label_icon: icon = self._icon self.label_icon.setVisible(icon is not None) if icon is not None and isinstance(icon, QIcon): self._pixmap = icon.pixmap(self._icon_size) self.label_icon.setPixmap(self._pixmap)
[docs] def set_value(self, value: str) -> None: """ Set formatted text value for the widget. Parameters ---------- value : str The formatted text value to set. Returns ------- None """ if self.label_value: self.value = value self.label_value.setText(value)
[docs] def get_tooltip(self) -> str: """ Get the widget's tooltip text. Returns ------- str The widget's tooltip text. """ return ""
[docs] def update_tooltip(self) -> str: """ Update the tooltip for the widget. Returns ------- str The updated tooltip text. """ tooltip = self.get_tooltip() if tooltip: if self.label_value: self.label_value.setToolTip(tooltip) if self.label_icon: self.label_icon.setToolTip(tooltip) self.setToolTip(tooltip)
# ---- Qt methods # -------------------------------------------------------------------------
[docs] def mousePressEvent(self, event: QMouseEvent) -> None: """ Change the widget's background color when it's clicked. Parameters ---------- event : QMouseEvent The mouse event that was triggered. Returns ------- None """ if self.INTERACT_ON_CLICK: self._css.QWidget.setValues( backgroundColor=SpyderPalette.COLOR_BACKGROUND_6 ) self.setStyleSheet(self._css.toString()) super().mousePressEvent(event)
[docs] def mouseReleaseEvent(self, event: QMouseEvent) -> None: """ Change the widget's background color & emit a signal when it's clicked. Parameters ---------- event : QMouseEvent The mouse release event. Returns ------- None """ super().mouseReleaseEvent(event) if self.INTERACT_ON_CLICK: self._css.QWidget.setValues( # Mac doesn't correctly restore the background color after # clicking on a widget that shows a menu backgroundColor=( SpyderPalette.COLOR_BACKGROUND_4 if MAC and self.menu else SpyderPalette.COLOR_BACKGROUND_5 ) ) self.setStyleSheet(self._css.toString()) self.sig_clicked.emit()
[docs] def enterEvent(self, event: QEnterEvent) -> None: """ Change the widget's background color and cursor shape on hover. Parameters ---------- event : QEnterEvent The mouse hover event. Returns ------- None """ if self.INTERACT_ON_CLICK: self._css.QWidget.setValues( backgroundColor=SpyderPalette.COLOR_BACKGROUND_5 ) self.setStyleSheet(self._css.toString()) self.setCursor(Qt.PointingHandCursor) self.update_tooltip() super().enterEvent(event)
[docs] def leaveEvent(self, event: QEvent) -> None: """ Restore the widget's background color when not hovering. Parameters ---------- event : QEvent The mouse leave event. Returns ------- None """ if self.INTERACT_ON_CLICK: self._css.QWidget.setValues( backgroundColor=SpyderPalette.COLOR_BACKGROUND_4 ) self.setStyleSheet(self._css.toString()) super().leaveEvent(event)
[docs] class BaseTimerStatus(StatusBarWidget): """ Base class for status bar widgets that update based on timers. """
[docs] def __init__(self, parent: QWidget | None = None) -> None: """ Base class for status bar widgets that update based on timers. Parameters ---------- parent : QWidget | None, optional The parent widget of this one, or ``None`` (default). Returns ------- None """ self.timer = None # Needs to come before parent call super().__init__(parent) self._interval = 2000 # Widget setup fm = self.label_value.fontMetrics() self.label_value.setMinimumWidth(fm.width("000%")) # Setup self.timer = QTimer(self) self.timer.timeout.connect(self.update_status) self.timer.start(self._interval)
# ---- Qt methods # -------------------------------------------------------------------------
[docs] def closeEvent(self, event: QCloseEvent) -> None: """ Handle the widget close event by stopping the timer. Parameters ---------- event : QCloseEvent The widget close event. Returns ------- None """ self.timer.stop() super().closeEvent(event)
[docs] def setVisible(self, value: bool) -> None: """ Stop the timer if the widget is not visible. Parameters ---------- value : bool ``True`` if the timer should be started, ``False`` to stop it. Returns ------- None """ if self.timer is not None: if value: self.timer.start(self._interval) else: self.timer.stop() super().setVisible(value)
# ---- Public API # -------------------------------------------------------------------------
[docs] def update_status(self) -> None: """ Update the status label widget, if the widget is visible. Returns ------- None """ if self.isVisible(): self.label_value.setText(self.get_value())
[docs] def set_interval(self, interval: int) -> None: """ Set the timer interval. Parameters ---------- interval : int The timer interval, in integer milliseconds. Returns ------- None """ self._interval = interval if self.timer is not None: self.timer.setInterval(interval)
[docs] def get_value(self) -> str: """ Return the formatted text value shown in the widget. Returns ------- str The formatted text value. Raises ------ NotImplementedError Must be implemented by subclasses to work. """ raise NotImplementedError