Coverage for vibefpmgui/widgets.py: 100%
70 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""Small widgets shared by the window's parts."""
3from qtpy.QtCore import QSize, Qt, Signal
4from qtpy.QtWidgets import QDialog, QLabel, QLineEdit, QMenu, QPushButton, QSizePolicy
7class StatusLabel(QLabel):
8 """A label for a line of status whose text keeps changing (an editor's
9 status line, a panel's progress): it asks for its text's width when
10 there's room, but needs only a little. A plain label's minimum width is
11 its whole text, so a longer status ("154 AI review notes", "… · context
12 ~14k of 32k") made the window wider, past the screen's edge for a window
13 filling it. When narrow, the text is cut off at the right."""
15 MINIMUM_WIDTH = 40
17 def minimumSizeHint(self) -> QSize:
18 hint = super().minimumSizeHint()
19 return QSize(min(hint.width(), self.MINIMUM_WIDTH), hint.height())
22class HistoryLineEdit(QLineEdit):
23 """A field for commands, as a shell's: Enter sends its text
24 (``submitted``), remembers it, and clears the field; Up and Down go
25 through what was sent (Down past the last one: what was being typed).
26 The history is this field's own, for as long as it lives (not saved)."""
28 submitted = Signal(str)
30 def __init__(self, parent=None):
31 super().__init__(parent)
32 self.history: list[str] = []
33 self._index = 0 # (in history; len(history): the text being typed, not sent yet)
34 self._draft = ""
35 self.returnPressed.connect(self._submit)
37 def _submit(self):
38 text = self.text().strip()
39 if not text:
40 return
41 if not self.history or self.history[-1] != text: # (the same twice in a row: kept once)
42 self.history.append(text)
43 self._index, self._draft = len(self.history), ""
44 self.clear()
45 self.submitted.emit(text)
47 def keyPressEvent(self, event):
48 key = event.key()
49 if key == Qt.Key_Up and self._index > 0:
50 if self._index == len(self.history):
51 self._draft = self.text()
52 self._index -= 1
53 self.setText(self.history[self._index])
54 elif key == Qt.Key_Down and self._index < len(self.history):
55 self._index += 1
56 self.setText(self._draft if self._index == len(self.history) else self.history[self._index])
57 elif key in (Qt.Key_Up, Qt.Key_Down):
58 pass # (no earlier, or later, entry)
59 else:
60 super().keyPressEvent(event)
63def run_dialog(dialog: QDialog) -> int:
64 """Show a dialog until it's closed, then let it go (a dialog that's a
65 child of the window would otherwise stay, hidden, until the app quits)."""
66 try:
67 return dialog.exec()
68 finally:
69 dialog.deleteLater()
72class StayOpenMenu(QMenu):
73 """A menu that stays open when a checkable item is clicked (to tick several)."""
75 def mouseReleaseEvent(self, event):
76 action = self.activeAction()
77 if action is not None and action.isCheckable() and action.isEnabled():
78 action.trigger()
79 return
80 super().mouseReleaseEvent(event)
83class MenuButton(QPushButton):
84 """A button that opens a menu, its text cut short with "…" to fit (it
85 doesn't make the panel it's in wider); the whole text is its tooltip."""
87 def __init__(self, parent=None):
88 super().__init__(parent)
89 self._full_text = ""
90 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
92 def set_full_text(self, text: str):
93 self._full_text = text
94 self._elide()
96 def full_text(self) -> str:
97 return self._full_text
99 def minimumSizeHint(self):
100 hint = super().minimumSizeHint()
101 return QSize(60, hint.height())
103 def sizeHint(self):
104 hint = super().sizeHint()
105 return QSize(60, hint.height())
107 def resizeEvent(self, event):
108 super().resizeEvent(event)
109 self._elide()
111 def _elide(self):
112 room = max(10, self.width() - 40) # (the menu arrow and the margins)
113 self.setText(self.fontMetrics().elidedText(self._full_text, Qt.ElideRight, room))