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