vibefpmgui.hover_popup

The editor's hover card: a fortitude issue's or a word's (with its fixes), or fortls's hover.

  1"""The editor's hover card: a fortitude issue's or a word's (with its fixes), or fortls's hover."""
  2
  3from __future__ import annotations
  4
  5from typing import TYPE_CHECKING
  6
  7from qtpy.QtCore import QPoint, Qt, QTimer
  8from qtpy.QtGui import QColor, QCursor, QPalette
  9from qtpy.QtWidgets import QApplication, QFrame, QTextBrowser, QToolTip, QVBoxLayout
 10
 11if TYPE_CHECKING:
 12    from .code_editor import CodeEditor
 13
 14
 15class HoverPopup(QFrame):
 16    """A tooltip-like popup for hover information that can scroll: fortls's
 17    answer for a long declaration or doc comment can be taller than the
 18    screen, and QToolTip can't scroll. It stays open while the mouse is on
 19    the name or inside the popup (so it can be scrolled), and closes when
 20    the mouse leaves both, on a key press, or when the editor scrolls."""
 21
 22    MAX_WIDTH, MAX_HEIGHT = 720, 420  # also at most ~60% / 45% of the screen
 23
 24    def __init__(self, editor: CodeEditor):
 25        super().__init__(editor, Qt.ToolTip | Qt.FramelessWindowHint)
 26        self.editor = editor
 27        self.anchor = None  # what the popup is about (see CodeEditor.hover_key)
 28        # A rounded card: the window is transparent, the card inside is drawn.
 29        self.setAttribute(Qt.WA_TranslucentBackground)
 30        self.card = QFrame()
 31        self.card.setObjectName("HoverCard")
 32        self.browser = QTextBrowser()
 33        self.browser.setFrameShape(QFrame.NoFrame)
 34        self.browser.setOpenLinks(False)
 35        self.browser.anchorClicked.connect(self._link_clicked)
 36        self.browser.setFont(QToolTip.font())
 37        self.browser.document().setDocumentMargin(9)
 38        self.browser.viewport().setAutoFillBackground(False)
 39        inner = QVBoxLayout(self.card)
 40        inner.setContentsMargins(1, 3, 1, 3)
 41        inner.addWidget(self.browser)
 42        layout = QVBoxLayout(self)
 43        layout.setContentsMargins(0, 0, 0, 0)
 44        layout.addWidget(self.card)
 45        self._hide_timer = QTimer(self)
 46        self._hide_timer.setSingleShot(True)
 47        self._hide_timer.setInterval(300)  # time to move the mouse into the popup
 48        self._hide_timer.timeout.connect(self._hide_unless_hovered)
 49
 50    def show_at(self, global_pos: QPoint, text_html: str, anchor):
 51        self.anchor = anchor
 52        self._hide_timer.stop()
 53        # The editor's colors (Qt's tooltip colors don't follow dark mode on macOS).
 54        pal = self.editor.palette()
 55        base, text = pal.color(QPalette.Base), pal.color(QPalette.Text)
 56        bg = base.lighter(115) if base.lightness() < 128 else base.darker(103)
 57        border = QColor(text.red(), text.green(), text.blue(), 70).name(QColor.HexArgb)
 58        self.setStyleSheet(f"#HoverCard {{ background: {bg.name()}; border: 1px solid {border}; "
 59                           f"border-radius: 9px; }} QTextBrowser {{ background: transparent; "
 60                           f"color: {text.name()}; }}")
 61        self.browser.setHtml(text_html)
 62        screen = (QApplication.screenAt(global_pos) or QApplication.primaryScreen()).availableGeometry()
 63        max_w = min(self.MAX_WIDTH, int(screen.width() * 0.6))
 64        max_h = min(self.MAX_HEIGHT, int(screen.height() * 0.45))
 65        doc = self.browser.document()
 66        doc.setTextWidth(-1)
 67        bar = self.browser.verticalScrollBar().sizeHint().width()
 68        width = min(int(doc.idealWidth()) + 4, max_w)
 69        doc.setTextWidth(width)
 70        height = int(doc.size().height()) + 10
 71        if height > max_h:  # it will scroll: leave room for the scroll bar
 72            width = min(width + bar, max_w)
 73            height = max_h
 74        self.resize(width + 4, height + 8)  # (the card's border and margins)
 75        # Below the mouse; above it if there's no room; always on screen.
 76        x = min(max(global_pos.x(), screen.left()), screen.right() - self.width())
 77        y = global_pos.y() + 18
 78        if y + self.height() > screen.bottom():
 79            y = max(screen.top(), global_pos.y() - 8 - self.height())
 80        self.move(x, y)
 81        self.browser.verticalScrollBar().setValue(0)
 82        self.show()
 83
 84    def _link_clicked(self, url):
 85        """The card's links: "Apply fix" (fix:N = the editor's Nth lint
 86        issue), a spelling suggestion (spell:N:word) or "Add to Dictionary"
 87        (addword:N); a web page (opened in the browser: a git blame card's
 88        commit, issue, file), or copy:TEXT (to the clipboard)."""
 89        if url.scheme() in ("http", "https"):
 90            from .git_blame import open_web_page
 91
 92            self.hide()
 93            open_web_page(url.toString())
 94            return
 95        if url.scheme() == "copy":
 96            QApplication.clipboard().setText(url.path())
 97            self.hide()
 98            return
 99        if url.scheme() in ("spell", "addword"):
100            number, _, word = url.path().partition(":")
101            try:
102                issue = self.editor.lint_issues[int(number)]
103            except (ValueError, IndexError):
104                return
105            self.hide()
106            if url.scheme() == "spell":
107                self.editor.replace_word(issue, word)
108            else:
109                self.editor.word_added.emit(issue.word)
110            return
111        if url.scheme() == "fix":
112            try:
113                issue = self.editor.lint_issues[int(url.path())]
114            except (ValueError, IndexError):
115                return
116            self.hide()
117            self.editor.lint_fix_requested.emit(issue)
118
119    def schedule_hide(self):
120        if self.isVisible() and not self._hide_timer.isActive():
121            self._hide_timer.start()
122
123    def keep(self):
124        self._hide_timer.stop()
125
126    def _hide_unless_hovered(self):
127        if not self.geometry().contains(QCursor.pos()):
128            self.hide()
129
130    def leaveEvent(self, event):
131        self.schedule_hide()
132        super().leaveEvent(event)
133
134    def enterEvent(self, event):
135        self.keep()
136        super().enterEvent(event)
class HoverPopup(PySide6.QtWidgets.QFrame):
 16class HoverPopup(QFrame):
 17    """A tooltip-like popup for hover information that can scroll: fortls's
 18    answer for a long declaration or doc comment can be taller than the
 19    screen, and QToolTip can't scroll. It stays open while the mouse is on
 20    the name or inside the popup (so it can be scrolled), and closes when
 21    the mouse leaves both, on a key press, or when the editor scrolls."""
 22
 23    MAX_WIDTH, MAX_HEIGHT = 720, 420  # also at most ~60% / 45% of the screen
 24
 25    def __init__(self, editor: CodeEditor):
 26        super().__init__(editor, Qt.ToolTip | Qt.FramelessWindowHint)
 27        self.editor = editor
 28        self.anchor = None  # what the popup is about (see CodeEditor.hover_key)
 29        # A rounded card: the window is transparent, the card inside is drawn.
 30        self.setAttribute(Qt.WA_TranslucentBackground)
 31        self.card = QFrame()
 32        self.card.setObjectName("HoverCard")
 33        self.browser = QTextBrowser()
 34        self.browser.setFrameShape(QFrame.NoFrame)
 35        self.browser.setOpenLinks(False)
 36        self.browser.anchorClicked.connect(self._link_clicked)
 37        self.browser.setFont(QToolTip.font())
 38        self.browser.document().setDocumentMargin(9)
 39        self.browser.viewport().setAutoFillBackground(False)
 40        inner = QVBoxLayout(self.card)
 41        inner.setContentsMargins(1, 3, 1, 3)
 42        inner.addWidget(self.browser)
 43        layout = QVBoxLayout(self)
 44        layout.setContentsMargins(0, 0, 0, 0)
 45        layout.addWidget(self.card)
 46        self._hide_timer = QTimer(self)
 47        self._hide_timer.setSingleShot(True)
 48        self._hide_timer.setInterval(300)  # time to move the mouse into the popup
 49        self._hide_timer.timeout.connect(self._hide_unless_hovered)
 50
 51    def show_at(self, global_pos: QPoint, text_html: str, anchor):
 52        self.anchor = anchor
 53        self._hide_timer.stop()
 54        # The editor's colors (Qt's tooltip colors don't follow dark mode on macOS).
 55        pal = self.editor.palette()
 56        base, text = pal.color(QPalette.Base), pal.color(QPalette.Text)
 57        bg = base.lighter(115) if base.lightness() < 128 else base.darker(103)
 58        border = QColor(text.red(), text.green(), text.blue(), 70).name(QColor.HexArgb)
 59        self.setStyleSheet(f"#HoverCard {{ background: {bg.name()}; border: 1px solid {border}; "
 60                           f"border-radius: 9px; }} QTextBrowser {{ background: transparent; "
 61                           f"color: {text.name()}; }}")
 62        self.browser.setHtml(text_html)
 63        screen = (QApplication.screenAt(global_pos) or QApplication.primaryScreen()).availableGeometry()
 64        max_w = min(self.MAX_WIDTH, int(screen.width() * 0.6))
 65        max_h = min(self.MAX_HEIGHT, int(screen.height() * 0.45))
 66        doc = self.browser.document()
 67        doc.setTextWidth(-1)
 68        bar = self.browser.verticalScrollBar().sizeHint().width()
 69        width = min(int(doc.idealWidth()) + 4, max_w)
 70        doc.setTextWidth(width)
 71        height = int(doc.size().height()) + 10
 72        if height > max_h:  # it will scroll: leave room for the scroll bar
 73            width = min(width + bar, max_w)
 74            height = max_h
 75        self.resize(width + 4, height + 8)  # (the card's border and margins)
 76        # Below the mouse; above it if there's no room; always on screen.
 77        x = min(max(global_pos.x(), screen.left()), screen.right() - self.width())
 78        y = global_pos.y() + 18
 79        if y + self.height() > screen.bottom():
 80            y = max(screen.top(), global_pos.y() - 8 - self.height())
 81        self.move(x, y)
 82        self.browser.verticalScrollBar().setValue(0)
 83        self.show()
 84
 85    def _link_clicked(self, url):
 86        """The card's links: "Apply fix" (fix:N = the editor's Nth lint
 87        issue), a spelling suggestion (spell:N:word) or "Add to Dictionary"
 88        (addword:N); a web page (opened in the browser: a git blame card's
 89        commit, issue, file), or copy:TEXT (to the clipboard)."""
 90        if url.scheme() in ("http", "https"):
 91            from .git_blame import open_web_page
 92
 93            self.hide()
 94            open_web_page(url.toString())
 95            return
 96        if url.scheme() == "copy":
 97            QApplication.clipboard().setText(url.path())
 98            self.hide()
 99            return
100        if url.scheme() in ("spell", "addword"):
101            number, _, word = url.path().partition(":")
102            try:
103                issue = self.editor.lint_issues[int(number)]
104            except (ValueError, IndexError):
105                return
106            self.hide()
107            if url.scheme() == "spell":
108                self.editor.replace_word(issue, word)
109            else:
110                self.editor.word_added.emit(issue.word)
111            return
112        if url.scheme() == "fix":
113            try:
114                issue = self.editor.lint_issues[int(url.path())]
115            except (ValueError, IndexError):
116                return
117            self.hide()
118            self.editor.lint_fix_requested.emit(issue)
119
120    def schedule_hide(self):
121        if self.isVisible() and not self._hide_timer.isActive():
122            self._hide_timer.start()
123
124    def keep(self):
125        self._hide_timer.stop()
126
127    def _hide_unless_hovered(self):
128        if not self.geometry().contains(QCursor.pos()):
129            self.hide()
130
131    def leaveEvent(self, event):
132        self.schedule_hide()
133        super().leaveEvent(event)
134
135    def enterEvent(self, event):
136        self.keep()
137        super().enterEvent(event)

A tooltip-like popup for hover information that can scroll: fortls's answer for a long declaration or doc comment can be taller than the screen, and QToolTip can't scroll. It stays open while the mouse is on the name or inside the popup (so it can be scrolled), and closes when the mouse leaves both, on a key press, or when the editor scrolls.

HoverPopup(editor: vibefpmgui.code_editor.CodeEditor)
25    def __init__(self, editor: CodeEditor):
26        super().__init__(editor, Qt.ToolTip | Qt.FramelessWindowHint)
27        self.editor = editor
28        self.anchor = None  # what the popup is about (see CodeEditor.hover_key)
29        # A rounded card: the window is transparent, the card inside is drawn.
30        self.setAttribute(Qt.WA_TranslucentBackground)
31        self.card = QFrame()
32        self.card.setObjectName("HoverCard")
33        self.browser = QTextBrowser()
34        self.browser.setFrameShape(QFrame.NoFrame)
35        self.browser.setOpenLinks(False)
36        self.browser.anchorClicked.connect(self._link_clicked)
37        self.browser.setFont(QToolTip.font())
38        self.browser.document().setDocumentMargin(9)
39        self.browser.viewport().setAutoFillBackground(False)
40        inner = QVBoxLayout(self.card)
41        inner.setContentsMargins(1, 3, 1, 3)
42        inner.addWidget(self.browser)
43        layout = QVBoxLayout(self)
44        layout.setContentsMargins(0, 0, 0, 0)
45        layout.addWidget(self.card)
46        self._hide_timer = QTimer(self)
47        self._hide_timer.setSingleShot(True)
48        self._hide_timer.setInterval(300)  # time to move the mouse into the popup
49        self._hide_timer.timeout.connect(self._hide_unless_hovered)

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, frameShape: PySide6.QtWidgets.QFrame.Shape | None = None, frameShadow: PySide6.QtWidgets.QFrame.Shadow | None = None, lineWidth: int | None = None, midLineWidth: int | None = None, frameWidth: int | None = None, frameRect: PySide6.QtCore.QRect | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

editor
anchor
card
browser
def show_at(self, global_pos: PySide6.QtCore.QPoint, text_html: str, anchor):
51    def show_at(self, global_pos: QPoint, text_html: str, anchor):
52        self.anchor = anchor
53        self._hide_timer.stop()
54        # The editor's colors (Qt's tooltip colors don't follow dark mode on macOS).
55        pal = self.editor.palette()
56        base, text = pal.color(QPalette.Base), pal.color(QPalette.Text)
57        bg = base.lighter(115) if base.lightness() < 128 else base.darker(103)
58        border = QColor(text.red(), text.green(), text.blue(), 70).name(QColor.HexArgb)
59        self.setStyleSheet(f"#HoverCard {{ background: {bg.name()}; border: 1px solid {border}; "
60                           f"border-radius: 9px; }} QTextBrowser {{ background: transparent; "
61                           f"color: {text.name()}; }}")
62        self.browser.setHtml(text_html)
63        screen = (QApplication.screenAt(global_pos) or QApplication.primaryScreen()).availableGeometry()
64        max_w = min(self.MAX_WIDTH, int(screen.width() * 0.6))
65        max_h = min(self.MAX_HEIGHT, int(screen.height() * 0.45))
66        doc = self.browser.document()
67        doc.setTextWidth(-1)
68        bar = self.browser.verticalScrollBar().sizeHint().width()
69        width = min(int(doc.idealWidth()) + 4, max_w)
70        doc.setTextWidth(width)
71        height = int(doc.size().height()) + 10
72        if height > max_h:  # it will scroll: leave room for the scroll bar
73            width = min(width + bar, max_w)
74            height = max_h
75        self.resize(width + 4, height + 8)  # (the card's border and margins)
76        # Below the mouse; above it if there's no room; always on screen.
77        x = min(max(global_pos.x(), screen.left()), screen.right() - self.width())
78        y = global_pos.y() + 18
79        if y + self.height() > screen.bottom():
80            y = max(screen.top(), global_pos.y() - 8 - self.height())
81        self.move(x, y)
82        self.browser.verticalScrollBar().setValue(0)
83        self.show()
def schedule_hide(self):
120    def schedule_hide(self):
121        if self.isVisible() and not self._hide_timer.isActive():
122            self._hide_timer.start()
def keep(self):
124    def keep(self):
125        self._hide_timer.stop()
def leaveEvent(self, event):
131    def leaveEvent(self, event):
132        self.schedule_hide()
133        super().leaveEvent(event)

leaveEvent(self, event: PySide6.QtCore.QEvent, /) -> None

def enterEvent(self, event):
135    def enterEvent(self, event):
136        self.keep()
137        super().enterEvent(event)

enterEvent(self, event: PySide6.QtGui.QEnterEvent, /) -> None

MAX_WIDTH = 720
MAX_HEIGHT = 420
staticMetaObject = PySide6.QtCore.QMetaObject("HoverPopup" inherits "QFrame": )