--- /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()