Files
fluent_widgets_pyside6/qfluentwidgets/components/widgets/tool_tip.py

461 lines
14 KiB
Python
Raw Normal View History

2025-08-14 18:45:16 +08:00
# coding:utf-8
from enum import Enum
from PySide6.QtCore import QEvent, QObject, QPoint, QTimer, Qt, QPropertyAnimation, QModelIndex, QRect
from PySide6.QtGui import QColor, QHelpEvent
from PySide6.QtWidgets import (QApplication, QFrame, QGraphicsDropShadowEffect,
QHBoxLayout, QLabel, QWidget, QAbstractItemView, QStyleOptionViewItem,
QTableView)
from ...common import FluentStyleSheet
from ...common.screen import getCurrentScreenGeometry
class ToolTipPosition(Enum):
""" Info bar position """
TOP = 0
BOTTOM = 1
LEFT = 2
RIGHT = 3
TOP_LEFT = 4
TOP_RIGHT = 5
BOTTOM_LEFT = 6
BOTTOM_RIGHT = 7
class ItemViewToolTipType(Enum):
""" Info bar position """
LIST = 0
TABLE = 1
class ToolTip(QFrame):
""" Tool tip """
def __init__(self, text='', parent=None):
"""
Parameters
----------
text: str
the text of tool tip
parent: QWidget
parent widget
"""
super().__init__(parent=parent)
self.__text = text
self.__duration = 1000
self.container = self._createContainer()
self.timer = QTimer(self)
self.setLayout(QHBoxLayout())
self.containerLayout = QHBoxLayout(self.container)
self.label = QLabel(text, self)
# set layout
self.layout().setContentsMargins(12, 8, 12, 12)
self.layout().addWidget(self.container)
self.containerLayout.addWidget(self.label)
self.containerLayout.setContentsMargins(8, 6, 8, 6)
# add opacity effect
self.opacityAni = QPropertyAnimation(self, b'windowOpacity', self)
self.opacityAni.setDuration(150)
# add shadow
self.shadowEffect = QGraphicsDropShadowEffect(self)
self.shadowEffect.setBlurRadius(25)
self.shadowEffect.setColor(QColor(0, 0, 0, 50))
self.shadowEffect.setOffset(0, 5)
self.container.setGraphicsEffect(self.shadowEffect)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.hide)
# set style
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint)
self.__setQss()
def text(self):
return self.__text
def setText(self, text):
""" set text on tooltip """
self.__text = text
self.label.setText(text)
self.container.adjustSize()
self.adjustSize()
def duration(self):
return self.__duration
def setDuration(self, duration: int):
""" set tooltip duration in milliseconds
Parameters
----------
duration: int
display duration in milliseconds, if `duration <= 0`, tooltip won't disappear automatically
"""
self.__duration = duration
def __setQss(self):
""" set style sheet """
self.container.setObjectName("container")
self.label.setObjectName("contentLabel")
FluentStyleSheet.TOOL_TIP.apply(self)
self.label.adjustSize()
self.adjustSize()
def _createContainer(self):
return QFrame(self)
def showEvent(self, e):
self.opacityAni.setStartValue(0)
self.opacityAni.setEndValue(1)
self.opacityAni.start()
self.timer.stop()
if self.duration() > 0:
self.timer.start(self.__duration + self.opacityAni.duration())
super().showEvent(e)
def hideEvent(self, e):
self.timer.stop()
super().hideEvent(e)
def adjustPos(self, widget, position: ToolTipPosition):
""" adjust the position of tooltip relative to widget """
manager = ToolTipPositionManager.make(position)
self.move(manager.position(self, widget))
class ToolTipPositionManager:
""" Tooltip position manager """
def position(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = self._pos(tooltip, parent)
x, y = pos.x(), pos.y()
rect = getCurrentScreenGeometry()
x = max(rect.left(), min(pos.x(), rect.right() - tooltip.width() - 4))
y = max(rect.top(), min(pos.y(), rect.bottom() - tooltip.height() - 4))
return QPoint(x, y)
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
raise NotImplementedError
@staticmethod
def make(position: ToolTipPosition):
""" mask info bar manager according to the display position """
managers = {
ToolTipPosition.TOP: TopToolTipManager,
ToolTipPosition.BOTTOM: BottomToolTipManager,
ToolTipPosition.LEFT: LeftToolTipManager,
ToolTipPosition.RIGHT: RightToolTipManager,
ToolTipPosition.TOP_RIGHT: TopRightToolTipManager,
ToolTipPosition.BOTTOM_RIGHT: BottomRightToolTipManager,
ToolTipPosition.TOP_LEFT: TopLeftToolTipManager,
ToolTipPosition.BOTTOM_LEFT: BottomLeftToolTipManager,
}
if position not in managers:
raise ValueError(f'`{position}` is an invalid info bar position.')
return managers[position]()
class TopToolTipManager(ToolTipPositionManager):
""" Top tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget):
pos = parent.mapToGlobal(QPoint())
x = pos.x() + parent.width()//2 - tooltip.width()//2
y = pos.y() - tooltip.height()
return QPoint(x, y)
class BottomToolTipManager(ToolTipPositionManager):
""" Bottom tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() + parent.width()//2 - tooltip.width()//2
y = pos.y() + parent.height()
return QPoint(x, y)
class LeftToolTipManager(ToolTipPositionManager):
""" Left tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() - tooltip.width()
y = pos.y() + (parent.height() - tooltip.height()) // 2
return QPoint(x, y)
class RightToolTipManager(ToolTipPositionManager):
""" Right tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() + parent.width()
y = pos.y() + (parent.height() - tooltip.height()) // 2
return QPoint(x, y)
class TopRightToolTipManager(ToolTipPositionManager):
""" Top right tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() + parent.width() - tooltip.width() + \
tooltip.layout().contentsMargins().right()
y = pos.y() - tooltip.height()
return QPoint(x, y)
class TopLeftToolTipManager(ToolTipPositionManager):
""" Top left tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() - tooltip.layout().contentsMargins().left()
y = pos.y() - tooltip.height()
return QPoint(x, y)
class BottomRightToolTipManager(ToolTipPositionManager):
""" Bottom right tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() + parent.width() - tooltip.width() + \
tooltip.layout().contentsMargins().right()
y = pos.y() + parent.height()
return QPoint(x, y)
class BottomLeftToolTipManager(ToolTipPositionManager):
""" Bottom left tooltip position manager """
def _pos(self, tooltip: ToolTip, parent: QWidget) -> QPoint:
pos = parent.mapToGlobal(QPoint())
x = pos.x() - tooltip.layout().contentsMargins().left()
y = pos.y() + parent.height()
return QPoint(x, y)
class ItemViewToolTipManager(ToolTipPositionManager):
""" Item view tooltip position manager """
def __init__(self, itemRect=QRect()):
super().__init__()
self.itemRect = itemRect
def _pos(self, tooltip: ToolTip, view: QAbstractItemView) -> QPoint:
pos = view.mapToGlobal(self.itemRect.topLeft())
x = pos.x()
y = pos.y() - tooltip.height() + 10
return QPoint(x, y)
@staticmethod
def make(tipType: ItemViewToolTipType, itemRect: QRect):
""" mask info bar manager according to the display tipType """
managers = {
ItemViewToolTipType.LIST: ItemViewToolTipManager,
ItemViewToolTipType.TABLE: TableItemToolTipManager,
}
if tipType not in managers:
raise ValueError(f'`{tipType}` is an invalid info bar tipType.')
return managers[tipType](itemRect)
class TableItemToolTipManager(ItemViewToolTipManager):
""" Table item view tooltip position manager """
def _pos(self, tooltip: ToolTip, view: QTableView) -> QPoint:
pos = view.mapToGlobal(self.itemRect.topLeft())
x = pos.x() + view.verticalHeader().isVisible() * view.verticalHeader().width()
y = pos.y() - tooltip.height() + view.horizontalHeader().isVisible() * view.horizontalHeader().height() + 10
return QPoint(x, y)
class ToolTipFilter(QObject):
""" Tool button with a tool tip """
def __init__(self, parent: QWidget, showDelay=300, position=ToolTipPosition.TOP):
"""
Parameters
----------
parent: QWidget
the widget to install tool tip
showDelay: int
show tool tip after how long the mouse hovers in milliseconds
position: TooltipPosition
where to show the tooltip
"""
super().__init__(parent=parent)
self.isEnter = False
self._tooltip = None
self._tooltipDelay = showDelay
self.position = position
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.showToolTip)
def eventFilter(self, obj: QObject, e: QEvent) -> bool:
if e.type() == QEvent.ToolTip:
return True
elif e.type() in [QEvent.Hide, QEvent.Leave]:
self.hideToolTip()
elif e.type() == QEvent.Enter:
self.isEnter = True
parent = self.parent() # type: QWidget
if self._canShowToolTip():
if self._tooltip is None:
self._tooltip = self._createToolTip()
t = parent.toolTipDuration() if parent.toolTipDuration() > 0 else -1
self._tooltip.setDuration(t)
# show the tool tip after delay
self.timer.start(self._tooltipDelay)
elif e.type() == QEvent.MouseButtonPress:
self.hideToolTip()
return super().eventFilter(obj, e)
def _createToolTip(self):
return ToolTip(self.parent().toolTip(), self.parent().window())
def hideToolTip(self):
""" hide tool tip """
self.isEnter = False
self.timer.stop()
if self._tooltip:
self._tooltip.hide()
def showToolTip(self):
""" show tool tip """
if not self.isEnter:
return
parent = self.parent() # type: QWidget
self._tooltip.setText(parent.toolTip())
self._tooltip.adjustPos(parent, self.position)
self._tooltip.show()
def setToolTipDelay(self, delay: int):
""" set the delay of tool tip """
self._tooltipDelay = delay
def _canShowToolTip(self) -> bool:
parent = self.parent() # type: QWidget
return parent.isWidgetType() and parent.toolTip() and parent.isEnabled()
class ItemViewToolTip(ToolTip):
""" Item view tool tip """
def adjustPos(self, view: QAbstractItemView, itemRect: QRect, tooltipType: ItemViewToolTipType):
manager = ItemViewToolTipManager.make(tooltipType, itemRect)
self.move(manager.position(self, view))
class ItemViewToolTipDelegate(ToolTipFilter):
""" Item view tool tip """
def __init__(self, parent: QAbstractItemView, showDelay=300, tooltipType=ItemViewToolTipType.TABLE):
super().__init__(parent, showDelay, ToolTipPosition.TOP)
self.text = ""
self.currentIndex = None
self.tooltipDuration = -1
self.tooltipType = tooltipType
self.viewport = parent.viewport()
parent.installEventFilter(self)
parent.viewport().installEventFilter(self)
parent.horizontalScrollBar().valueChanged.connect(self.hideToolTip)
parent.verticalScrollBar().valueChanged.connect(self.hideToolTip)
def eventFilter(self, obj: QObject, e: QEvent) -> bool:
if obj is self.parent():
if e.type() in [QEvent.Type.Hide, QEvent.Type.Leave]:
self.hideToolTip()
elif e.type() == QEvent.Type.Enter:
self.isEnter = True
elif obj is self.viewport:
if e.type() == QEvent.Type.MouseButtonPress:
self.hideToolTip()
return QObject.eventFilter(self, obj, e)
def _createToolTip(self):
return ItemViewToolTip(self.text, self.parent().window())
def showToolTip(self):
""" show tool tip """
if not self._tooltip:
self._tooltip = self._createToolTip()
view = self.parent() # type: QAbstractItemView
self._tooltip.setText(self.text)
if self.currentIndex:
rect = view.visualRect(self.currentIndex)
else:
rect = QRect()
self._tooltip.adjustPos(view, rect, self.tooltipType)
self._tooltip.show()
def _canShowToolTip(self) -> bool:
return True
def setText(self, text: str):
self.text = text
if self._tooltip:
self._tooltip.setText(text)
def setToolTipDuration(self, duration):
self.tooltipDuration = duration
if self._tooltip:
self._tooltip.setDuration(duration)
def helpEvent(self, event: QHelpEvent, view: QAbstractItemView, option: QStyleOptionViewItem, index: QModelIndex) -> bool:
if not event or not view:
return False
if event.type() == QEvent.Type.ToolTip:
text = index.data(Qt.ItemDataRole.ToolTipRole)
if not text:
self.hideToolTip()
return False
self.text = text
self.currentIndex = index
if not self._tooltip:
self._tooltip = self._createToolTip()
self._tooltip.setDuration(self.tooltipDuration)
# show the tool tip after delay
self.timer.start(self._tooltipDelay)
return True