Skip to content

Instantly share code, notes, and snippets.

@jimbob88
Last active September 4, 2022 15:58
Show Gist options
  • Select an option

  • Save jimbob88/0499c0fb8b7ec2dabc91e3d801f1a63f to your computer and use it in GitHub Desktop.

Select an option

Save jimbob88/0499c0fb8b7ec2dabc91e3d801f1a63f to your computer and use it in GitHub Desktop.
A scrollable tooltip in PyQt (PySide6)
import sys
from typing import cast
from lorem_text import lorem
from PySide6 import QtCore
from PySide6.QtCore import QSize, Qt, QPoint, QEvent, QPropertyAnimation, QEasingCurve, QObject
from PySide6.QtGui import QHelpEvent, QPalette, QKeyEvent, QMouseEvent, QScreen, QGuiApplication
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QGraphicsOpacityEffect, QToolTip, \
QFrame
def get_widget_screen(pos: QPoint, widget: QWidget) -> QScreen:
guess = widget.screen() if widget else QGuiApplication.primaryScreen()
exact = guess.virtualSiblingAt(pos)
return exact or guess
class ScrollableToolTip(QScrollArea):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.animations = None
self.label_eff = None
self.setWidgetResizable(True)
self.label = QLabel(self)
self.label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.label.setWordWrap(True)
self.setWidget(self.label)
self.setWindowFlags(Qt.ToolTip | Qt.BypassGraphicsProxyWidget)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setFixedHeight(100)
self.setMouseTracking(True)
self.setForegroundRole(QPalette.ToolTipText)
self.setBackgroundRole(QPalette.ToolTipBase)
self.setPalette(QToolTip.palette())
self.ensurePolished()
self.setFrameStyle(QFrame.NoFrame)
self.setAlignment(Qt.AlignLeft)
def event(self, event: QEvent) -> bool:
if event.type() in [QEvent.KeyPress, QEvent.KeyRelease]:
key = cast(QKeyEvent, event).key()
if key < Qt.Key_Shift or key > Qt.Key_ScrollLock:
self.hide()
elif event.type() == QEvent.Leave:
self.fade_out()
elif event.type() == QEvent.MouseMove:
if not self.rect().isNull() and not self.rect().contains(cast(QMouseEvent, event).globalPos().toPoint()):
self.fade_out()
return super().event(event)
def set_text(self, text: str):
self.label.setText(text)
def show_at(self, pos: QPoint):
self.show()
self.move(pos)
def fade_out(self):
"""Fades out both the label and the window for 500ms"""
self.label_eff = QGraphicsOpacityEffect(self)
self.label.setGraphicsEffect(self.label_eff)
self.animations = [
QPropertyAnimation(self.label_eff, b"opacity"),
QPropertyAnimation(self, b"windowOpacity")
]
for animation in self.animations:
animation.setDuration(500)
animation.setStartValue(1)
animation.setEndValue(0)
animation.setEasingCurve(QEasingCurve.OutBack)
animation.start(QPropertyAnimation.DeleteWhenStopped)
animation.finished.connect(self.hide) # type: ignore
def place_tooltip(self, pos: QPoint, widget: QWidget):
"""A pyside6 compatible implementation of QTipLabel::placeTip
Skips the steps related to QPlatformScreen because that isn't exposed
in the PyQt/PySide implementation of Qt
"""
self.show()
screen = get_widget_screen(pos, widget)
screen_rect = screen.geometry()
if pos.x() + self.width() > screen_rect.x() + screen_rect.width():
pos.setX(
pos.x() - (4 + self.width()) # rx() -=
)
if pos.y() + self.height() > screen_rect.y() + screen_rect.width():
pos.setY(
pos.y() - (24 + self.height()) # ry() -=
)
if pos.y() < screen_rect.y():
pos.setY(screen_rect.y())
if pos.x() + self.width() > screen_rect.x() + screen_rect.width():
pos.setX(
screen_rect.x() + screen_rect.width() - self.width()
)
if pos.x() < screen_rect.x():
pos.setX(
screen_rect.x()
)
if pos.y() + self.height() > screen_rect.y() + screen_rect.height():
pos.setY(
screen_rect.y() + screen_rect.height() - self.height()
)
self.move(pos)
class CustomButton(QPushButton):
def event(self, e: QtCore.QEvent) -> bool:
if QtCore.QEvent.ToolTip == e.type():
e = cast(QHelpEvent, e)
tooltip = ScrollableToolTip(self)
tooltip.set_text(self.toolTip())
tooltip.place_tooltip(e.globalPos(), self)
return False
return super().event(e)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("My App")
button = CustomButton("Hover over Me!", parent=self)
button.setToolTip(lorem.paragraphs(30))
self.setFixedSize(QSize(400, 300))
self.setCentralWidget(button)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment