src/eric7/EricWidgets/EricIconBar.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/EricWidgets/EricIconBar.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,526 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a bar widget showing just icons.
+"""
+
+import contextlib
+
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QCoreApplication
+from PyQt6.QtGui import QColor, QIcon, QCursor, QPalette
+from PyQt6.QtWidgets import QWidget, QBoxLayout, QWIDGETSIZE_MAX, QMenu
+
+from EricWidgets.EricApplication import ericApp
+
+from .EricClickableLabel import EricClickableLabel
+
+import UI.PixmapCache
+
+
+class EricIconBar(QWidget):
+    """
+    Class implementing a bar widget showing just icons.
+    
+    @signal currentChanged(index) emitted to indicate a change of the current
+        index
+    @signal currentClicked(index) emitted to indicate, that the current icon
+        was clicked
+    """
+    BarSizes = {
+        # tuples with (icon size, border size, translated size string)
+        "xs": (
+            16, 1,
+            QCoreApplication.translate("EricIconBar", "extra small")
+        ),
+        "sm": (
+            22, 1,
+            QCoreApplication.translate("EricIconBar", "small")
+        ),
+        "md": (
+            32, 2,
+            QCoreApplication.translate("EricIconBar", "medium")
+        ),
+        "lg": (
+            48, 2,
+            QCoreApplication.translate("EricIconBar", "large")
+        ),
+        "xl": (
+            64, 3,
+            QCoreApplication.translate("EricIconBar", "extra large")
+        ),
+        "xxl": (
+            96, 3,
+            QCoreApplication.translate("EricIconBar", "very large")
+        ),
+    }
+    DefaultBarSize = "md"
+    
+    MoreLabelAspect = 36 / 96
+    
+    MenuStyleSheetTemplate = (
+        "QMenu {{ background-color: {0}; "
+        "selection-background-color: {1}; "
+        "border: 1px solid; }}"
+    )
+    WidgetStyleSheetTemplate = "QWidget {{ background-color: {0}; }}"
+    LabelStyleSheetTemplate = "QLabel {{ background-color: {0}; }}"
+    
+    currentChanged = pyqtSignal(int)
+    currentClicked = pyqtSignal(int)
+    
+    def __init__(self, orientation=Qt.Orientation.Horizontal,
+                 barSize=DefaultBarSize, parent=None):
+        """
+        Constructor
+        
+        @param orientation orientation for the widget
+        @type Qt.Orientation
+        @param barSize size category for the bar (one of 'xs', 'sm', 'md',
+            'lg', 'xl', 'xxl')
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        
+        try:
+            self.__barSize, self.__borderSize = (
+                EricIconBar.BarSizes[barSize][:2])
+            self.__barSizeKey = barSize
+        except KeyError:
+            self.__barSize, self.__borderSize = (
+                EricIconBar.BarSizes[EricIconBar.DefaultBarSize][:2])
+        self.__fixedHeightWidth = (
+            self.__barSize + 2 * self.__borderSize
+        )
+        self.__minimumHeightWidth = int(
+            self.__barSize * self.MoreLabelAspect) + 2 * self.__borderSize
+        
+        # set initial values
+        self.__color = QColor("#008800")
+        self.__orientation = Qt.Orientation.Horizontal
+        self.__currentIndex = -1
+        self.__icons = []
+        
+        # initialize with horizontal layout and change later if needed
+        self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
+        self.setFixedHeight(self.__fixedHeightWidth)
+        self.setMinimumWidth(self.__minimumHeightWidth)
+        
+        self.__layout = QBoxLayout(QBoxLayout.Direction.LeftToRight)
+        self.__layout.setContentsMargins(
+            self.__borderSize, self.__borderSize,
+            self.__borderSize, self.__borderSize)
+        
+        self.__layout.addStretch()
+        
+        self.setLayout(self.__layout)
+        
+        if orientation != self.__orientation:
+            self.setOrientation(orientation)
+        
+        self.setColor(self.__color)
+        
+        self.__createAndAddMoreLabel()
+        
+        self.__adjustIconLabels()
+    
+    def setOrientation(self, orientation):
+        """
+        Public method to set the widget orientation.
+        
+        @param orientation orientation to be set
+        @type Qt.Orientation
+        """
+        # reset list widget size constraints
+        self.setFixedSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
+        
+        # remove the 'More' icon
+        itm = self.__layout.takeAt(self.__layout.count() - 1)
+        itm.widget().deleteLater()
+        del itm
+        
+        if orientation == Qt.Orientation.Horizontal:
+            self.setFixedHeight(self.__fixedHeightWidth)
+            self.setMinimumWidth(self.__minimumHeightWidth)
+            self.__layout.setDirection(QBoxLayout.Direction.LeftToRight)
+        elif orientation == Qt.Orientation.Vertical:
+            self.setFixedWidth(self.__fixedHeightWidth)
+            self.setMinimumHeight(self.__minimumHeightWidth)
+            self.__layout.setDirection(QBoxLayout.Direction.TopToBottom)
+        
+        self.__orientation = orientation
+        
+        self.__createAndAddMoreLabel()
+        
+        self.__adjustIconLabels()
+    
+    def orientation(self):
+        """
+        Public method to get the orientation of the widget.
+        
+        @return orientation of the widget
+        @rtype Qt.Orientation
+        """
+        return self.__orientation
+    
+    def setBarSize(self, barSize):
+        """
+        Public method to set the icon bar size.
+        
+        @param barSize size category for the bar (one of 'xs', 'sm', 'md',
+            'lg', 'xl', 'xxl')
+        @type str
+        """
+        # remove the 'More' icon
+        itm = self.__layout.takeAt(self.__layout.count() - 1)
+        itm.widget().deleteLater()
+        del itm
+        
+        self.__barSize, self.__borderSize = (
+            EricIconBar.BarSizes[barSize][:2])
+        self.__barSizeKey = barSize
+        self.__fixedHeightWidth = (
+            self.__barSize + 2 * self.__borderSize
+        )
+        self.__minimumHeightWidth = int(
+            self.__barSize * self.MoreLabelAspect) + 2 * self.__borderSize
+        
+        if self.__orientation == Qt.Orientation.Horizontal:
+            self.setFixedHeight(self.__fixedHeightWidth)
+            self.setMinimumWidth(self.__minimumHeightWidth)
+        elif self.__orientation == Qt.Orientation.Vertical:
+            self.setFixedWidth(self.__fixedHeightWidth)
+            self.setMinimumHeight(self.__minimumHeightWidth)
+        
+        self.__layout.setContentsMargins(
+            self.__borderSize, self.__borderSize,
+            self.__borderSize, self.__borderSize)
+        
+        for index, icon in enumerate(self.__icons):
+            iconLabel = self.__layout.itemAt(index)
+            if iconLabel:
+                widget = iconLabel.widget()
+                widget.setFixedSize(self.__barSize, self.__barSize)
+                widget.setPixmap(
+                    icon.pixmap(self.__barSize, self.__barSize))
+        
+        self.__createAndAddMoreLabel()
+        
+        self.__adjustIconLabels()
+    
+    def barSize(self):
+        """
+        Public method to get the icon bar size.
+        
+        @return barSize size category for the bar (one of 'xs', 'sm', 'md',
+            'lg', 'xl', 'xxl')
+        @rtype str
+        """
+        return self.__barSizeKey
+    
+    def setColor(self, color):
+        """
+        Public method to set the color of the widget.
+        
+        @param color color of the widget
+        @type QColor
+        """
+        self.__color = color
+        self.__highlightColor = color.darker()
+        
+        self.setStyleSheet(
+            EricIconBar.WidgetStyleSheetTemplate.format(color.name()))
+        
+        label = self.__layout.itemAt(self.__currentIndex)
+        if label:
+            label.widget().setStyleSheet(
+                EricIconBar.LabelStyleSheetTemplate
+                .format(self.__highlightColor.name())
+            )
+    
+    def color(self):
+        """
+        Public method to return the current color.
+        
+        @return current color
+        @rtype QColor
+        """
+        return self.__color
+    
+    def __createIcon(self, icon, label=""):
+        """
+        Private method to creat an icon label.
+        
+        @param icon reference to the icon
+        @type QIcon
+        @param label label text to be shown as a tooltip (defaults to "")
+        @type str (optional)
+        @return created and connected label
+        @rtype EricClickableLabel
+        """
+        iconLabel = EricClickableLabel(self)
+        iconLabel.setFixedSize(self.__barSize, self.__barSize)
+        iconLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        iconLabel.setPixmap(icon.pixmap(self.__barSize, self.__barSize))
+        if label:
+            iconLabel.setToolTip(label)
+        
+        iconLabel.clicked.connect(lambda: self.__iconClicked(iconLabel))
+        
+        return iconLabel
+    
+    def __createAndAddMoreLabel(self):
+        """
+        Private method to create the label to be shown for too many icons.
+        """
+        self.__moreLabel = EricClickableLabel(self)
+        self.__moreLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        if self.__orientation == Qt.Orientation.Horizontal:
+            self.__moreLabel.setFixedSize(
+                int(self.__barSize * self.MoreLabelAspect), self.__barSize)
+            self.__moreLabel.setPixmap(
+                UI.PixmapCache.getIcon("sbDotsH96").pixmap(
+                    int(self.__barSize * self.MoreLabelAspect), self.__barSize
+                )
+            )
+        else:
+            self.__moreLabel.setFixedSize(
+                self.__barSize, int(self.__barSize * self.MoreLabelAspect))
+            self.__moreLabel.setPixmap(
+                UI.PixmapCache.getIcon("sbDotsV96").pixmap(
+                    self.__barSize, int(self.__barSize * self.MoreLabelAspect)
+                )
+            )
+        
+        self.__layout.addWidget(self.__moreLabel)
+        
+        self.__moreLabel.clicked.connect(self.__moreLabelClicked)
+    
+    def addIcon(self, icon, label=""):
+        """
+        Public method to add an icon to the bar.
+        
+        @param icon reference to the icon
+        @type QIcon
+        @param label label text to be shown as a tooltip (defaults to "")
+        @type str (optional)
+        """
+        # the stretch item is always the last one
+        self.insertIcon(self.count(), icon, label=label)
+    
+    def insertIcon(self, index, icon, label=""):
+        """
+        Public method to insert an icon into the bar.
+        
+        @param index position to insert the icon at
+        @type int
+        @param icon reference to the icon
+        @type QIcon
+        @param label label text to be shown as a tooltip (defaults to "")
+        @type str (optional)
+        """
+        iconLabel = self.__createIcon(icon, label=label)
+        self.__layout.insertWidget(index, iconLabel)
+        self.__icons.insert(index, QIcon(icon))
+        
+        if self.__currentIndex < 0:
+            self.setCurrentIndex(index)
+        elif index <= self.__currentIndex:
+            self.setCurrentIndex(self.__currentIndex + 1)
+        
+        self.__adjustIconLabels()
+    
+    def removeIcon(self, index):
+        """
+        Public method to remove an icon from the bar.
+        
+        @param index index of the icon to be removed
+        @type int
+        """
+        label = self.__layout.itemAt(index)
+        if label:
+            with contextlib.suppress(IndexError):
+                del self.__icons[index]
+            itm = self.__layout.takeAt(index)
+            itm.widget().deleteLater()
+            del itm
+            
+            if index == self.__currentIndex:
+                self.setCurrentIndex(index)
+            elif index < self.__currentIndex:
+                self.setCurrentIndex(self.__currentIndex - 1)
+            
+            self.__adjustIconLabels()
+    
+    @pyqtSlot()
+    def __iconClicked(self, label):
+        """
+        Private slot to handle an icon been clicked.
+        
+        @param label reference to the clicked label
+        @type EricClickableLabel
+        """
+        index = self.__layout.indexOf(label)
+        if index >= 0:
+            if index == self.__currentIndex:
+                self.currentClicked.emit(self.__currentIndex)
+            else:
+                self.setCurrentIndex(index)
+    
+    def setCurrentIndex(self, index):
+        """
+        Public method to set the current index.
+        
+        @param index current index to be set
+        @type int
+        """
+        if index >= self.count():
+            index = -1
+        
+        if index != self.__currentIndex and index >= 0:
+            # reset style of previous current icon
+            oldLabel = self.__layout.itemAt(self.__currentIndex)
+            if oldLabel:
+                widget = oldLabel.widget()
+                if widget is not None:
+                    widget.setStyleSheet("")
+            
+            # set style of new current icon
+            newLabel = self.__layout.itemAt(index)
+            if newLabel:
+                newLabel.widget().setStyleSheet(
+                    EricIconBar.LabelStyleSheetTemplate
+                    .format(self.__highlightColor.name())
+                )
+            
+            self.__currentIndex = index
+            self.currentChanged.emit(self.__currentIndex)
+    
+    def currentIndex(self):
+        """
+        Public method to get the current index.
+        
+        @return current index
+        @rtype int
+        """
+        return self.__currentIndex
+    
+    def count(self):
+        """
+        Public method to get the number of icon labels.
+        
+        @return number of icon labels
+        @rtype int
+        """
+        return len(self.__icons)
+    
+    def wheelEvent(self, evt):
+        """
+        Protected method to handle a wheel event.
+        
+        @param evt reference to the wheel event
+        @type QWheelEvent
+        """
+        delta = evt.angleDelta().y()
+        if delta > 0:
+            self.previousIcon()
+        else:
+            self.nextIcon()
+    
+    @pyqtSlot()
+    def previousIcon(self):
+        """
+        Public slot to set the icon before the current one.
+        """
+        index = self.__currentIndex - 1
+        if index < 0:
+            # wrap around
+            index = self.count() - 1
+        
+        self.setCurrentIndex(index)
+    
+    @pyqtSlot()
+    def nextIcon(self):
+        """
+        Public slot to set the icon after the current one.
+        """
+        index = self.__currentIndex + 1
+        if index == self.count():
+            # wrap around
+            index = 0
+        
+        self.setCurrentIndex(index)
+    
+    @pyqtSlot()
+    def __moreLabelClicked(self):
+        """
+        Private slot to handle a click onto the 'More' label.
+        """
+        menu = QMenu(self)
+        baseColor = ericApp().palette().color(
+            QPalette.ColorRole.Base)
+        highlightColor = ericApp().palette().color(
+            QPalette.ColorRole.Highlight)
+        menu.setStyleSheet(
+            EricIconBar.MenuStyleSheetTemplate.format(
+                baseColor.name(), highlightColor.name()))
+        
+        for index in range(self.count()):
+            iconLabel = self.__layout.itemAt(index)
+            if iconLabel:
+                widget = iconLabel.widget()
+                if not widget.isVisible():
+                    act = menu.addAction(widget.toolTip())
+                    act.setData(index)
+        
+        selectedAction = menu.exec(QCursor.pos())
+        if selectedAction is not None:
+            index = selectedAction.data()
+            if index >= 0:
+                if index == self.__currentIndex:
+                    self.currentClicked.emit(self.__currentIndex)
+                else:
+                    self.setCurrentIndex(index)
+    
+    def resizeEvent(self, evt):
+        """
+        Protected method to handle resizing of the icon bar.
+        
+        @param evt reference to the event object
+        @type QResizeEvent
+        """
+        self.__adjustIconLabels()
+    
+    def __adjustIconLabels(self):
+        """
+        Private method to adjust the visibility of the icon labels.
+        """
+        size = (
+            self.width()
+            if self.orientation() == Qt.Orientation.Horizontal else
+            self.height()
+        ) - 2 * self.__borderSize
+        
+        iconsSize = self.count() * self.__barSize
+        
+        if size < iconsSize:
+            self.__moreLabel.show()
+            iconsSize += int(self.__barSize * self.MoreLabelAspect)
+            for index in range(self.count() - 1, -1, -1):
+                iconLabel = self.__layout.itemAt(index)
+                if iconLabel:
+                    if size < iconsSize:
+                        iconLabel.widget().hide()
+                        iconsSize -= self.__barSize
+                    else:
+                        iconLabel.widget().show()
+        else:
+            self.__moreLabel.hide()
+            for index in range(self.count()):
+                iconLabel = self.__layout.itemAt(index)
+                if iconLabel:
+                    iconLabel.widget().show()

eric ide

mercurial