Last active
September 4, 2022 15:58
-
-
Save jimbob88/0499c0fb8b7ec2dabc91e3d801f1a63f to your computer and use it in GitHub Desktop.
A scrollable tooltip in PyQt (PySide6)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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