feat(ui): close the last gaps against the audit design (31/31)
check_design_parity.py reads its checklist from the audit page's own
proposals; it now reports every one of the 31 as implemented, with no
deliberate divergences left.
* Schedule: the Kanban/Calendar drop-list became a pair of tabs, and the
Running lane is outlined while it holds anything — dropping a card there
starts the task for real, so it should not look like the other six.
* Cowork: agent / routing / usage / folder moved out of the typing box into
their own status strip beneath it, styled as status rather than a second
toolbar. All of them stay interactive; the design's read-only strip would
have cost features.
* Folder: the path is written as the screen's title instead of sitting in a
read-only text box that looked editable and cost a row.
* GraphRAG: the second toolbar row is gone (Export joined the first), and
the one button that relabelled itself became Đồ thị | Tin nhắn tabs, so
the view you are NOT in is named too.
* Settings gained the theme picker, so language / provider / theme are all
reachable there as well as on the rail's account row.
* Task editor: the five group boxes are grouped into three step tabs
(Nội dung → Lịch chạy → Liên kết). All 22 fields verified present after
the move; only the old section index is gone, replaced by the tabs.
* The assistant dot now clears a screen's own bottom bar (Cowork's
composer), measured from the composer's top edge in window coordinates.
Also adds .gitattributes: without it a Windows checkout records CRLF and
every file reads as fully rewritten to a Linux CI runner.
Verification: 7 check_*.py suites green, no screen clipped at 1920/1366/1280,
and no dialog scrolls sideways at 9/11/14pt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -460,11 +460,18 @@ class Composer(QWidget):
|
||||
|
||||
# bottom row: left slot (e.g. Cowork's output-folder picker) — stretch —
|
||||
# right slot (e.g. Plan/Act toggle, the per-tab Agent combo on Code/Cowork)
|
||||
# Its own strip UNDER the typing box, styled as a status line rather
|
||||
# than a second toolbar: the design asks for the typing area to be just
|
||||
# input · attach · send, with agent / routing / usage / folder reading
|
||||
# as status underneath. They stay interactive — only quieter.
|
||||
self._bottom_left_count = 0
|
||||
self.extra_row = QHBoxLayout()
|
||||
self.extra_row.setContentsMargins(0, 0, 0, 0)
|
||||
self.extra_bar = QWidget()
|
||||
self.extra_bar.setObjectName("composerStatus")
|
||||
self.extra_row = QHBoxLayout(self.extra_bar)
|
||||
self.extra_row.setContentsMargins(2, 2, 2, 0)
|
||||
self.extra_row.setSpacing(6)
|
||||
self.extra_row.addStretch(1)
|
||||
root.addLayout(self.extra_row)
|
||||
root.addWidget(self.extra_bar)
|
||||
|
||||
on_language_changed(self._retranslate)
|
||||
|
||||
|
||||
+14
-5
@@ -261,14 +261,20 @@ class FolderTab(QWidget):
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
|
||||
# The path IS the title of this screen, so it is written as one rather
|
||||
# than shown in a read-only text box that looks editable and costs a
|
||||
# whole row of its own. Full path on hover; the button still opens the
|
||||
# folder picker.
|
||||
bar = QHBoxLayout()
|
||||
self.path_edit = QLineEdit(self._root)
|
||||
self.path_edit.setReadOnly(True)
|
||||
self.path_lbl = QLabel(self._root)
|
||||
self.path_lbl.setObjectName("folderTitle")
|
||||
self.path_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
self.path_lbl.setToolTip(self._root)
|
||||
self._open_btn = QPushButton()
|
||||
self._open_btn.setIcon(icon("folder"))
|
||||
self._open_btn.setObjectName("primary")
|
||||
self._open_btn.clicked.connect(self._pick_root)
|
||||
bar.addWidget(self.path_edit, 1)
|
||||
bar.addWidget(self.path_lbl, 1)
|
||||
bar.addWidget(self._open_btn)
|
||||
root.addLayout(bar)
|
||||
|
||||
@@ -383,7 +389,8 @@ class FolderTab(QWidget):
|
||||
if not p or not os.path.isdir(p):
|
||||
return
|
||||
self._root = p
|
||||
self.path_edit.setText(p)
|
||||
self.path_lbl.setText(p)
|
||||
self.path_lbl.setToolTip(p)
|
||||
self.model.setRootPath(p)
|
||||
self.tree.setRootIndex(self.model.index(p))
|
||||
if getattr(self, "terminal", None) is not None:
|
||||
@@ -1492,7 +1499,9 @@ class FolderTab(QWidget):
|
||||
else tr("folder.preview"))
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
self.path_edit.setPlaceholderText(tr("folder.path_placeholder"))
|
||||
# The label always shows a real path, so the placeholder became a
|
||||
# tooltip hint on the button that changes it.
|
||||
self._open_btn.setToolTip(tr("folder.path_placeholder"))
|
||||
self._open_btn.setText(tr("folder.open_folder"))
|
||||
self.save_btn.setText(tr("folder.save"))
|
||||
self.ext_btn.setText(tr("folder.open_external"))
|
||||
|
||||
+13
-1
@@ -334,13 +334,25 @@ class HelpAgentWidget(QWidget):
|
||||
self.reposition()
|
||||
self.raise_()
|
||||
|
||||
# A screen whose bottom edge is an input row (Cowork's composer) must not
|
||||
# have the dock sitting on top of it — set by MainWindow when the page
|
||||
# changes, in window coordinates.
|
||||
_bottom_guard = 0
|
||||
|
||||
def set_bottom_guard(self, height: int) -> None:
|
||||
"""Reserve `height` px at the foot of the window for the page's own
|
||||
controls; the dock floats above it instead of over the Send button."""
|
||||
if height != self._bottom_guard:
|
||||
self._bottom_guard = max(0, height)
|
||||
self.reposition()
|
||||
|
||||
def reposition(self) -> None:
|
||||
"""Pin to the parent's bottom-right corner (called on parent resize)."""
|
||||
p = self.parentWidget()
|
||||
if p is None:
|
||||
return
|
||||
x = max(0, p.width() - self.width() - _MARGIN)
|
||||
y = max(0, p.height() - self.height() - _MARGIN)
|
||||
y = max(0, p.height() - self.height() - _MARGIN - self._bottom_guard)
|
||||
self.move(x, y)
|
||||
|
||||
# ---- rendering --------------------------------------------------------
|
||||
|
||||
+26
-9
@@ -16,8 +16,8 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
|
||||
QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox,
|
||||
QPlainTextEdit, QPushButton, QScrollArea, QStackedWidget, QTableWidget,
|
||||
QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
QPlainTextEdit, QPushButton, QScrollArea, QStackedWidget, QTabBar,
|
||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core import tasks as taskrepo
|
||||
@@ -92,13 +92,18 @@ class ScheduleTaskTab(QWidget):
|
||||
self.ai_btn = QPushButton()
|
||||
self.ai_btn.setIcon(icon("sparkle"))
|
||||
self.ai_btn.clicked.connect(self._ai_create)
|
||||
self.view_combo = QComboBox()
|
||||
for v in _VIEWS:
|
||||
self.view_combo.addItem("", v)
|
||||
self.view_combo.currentIndexChanged.connect(self._on_view_changed)
|
||||
# Two views of the same tasks, so they read as a pair of tabs rather
|
||||
# than a drop-list you have to open to discover the Calendar exists.
|
||||
self.view_tabs = QTabBar()
|
||||
self.view_tabs.setObjectName("viewTabs")
|
||||
self.view_tabs.setDrawBase(False)
|
||||
self.view_tabs.setExpanding(False)
|
||||
for _v in _VIEWS:
|
||||
self.view_tabs.addTab("")
|
||||
self.view_tabs.currentChanged.connect(self._on_view_changed)
|
||||
header.addWidget(self._title)
|
||||
header.addWidget(self.counts_lbl, 1)
|
||||
header.addWidget(self.view_combo)
|
||||
header.addWidget(self.view_tabs)
|
||||
header.addWidget(self.add_btn)
|
||||
header.addWidget(self.ai_btn)
|
||||
root.addLayout(header)
|
||||
@@ -162,14 +167,14 @@ class ScheduleTaskTab(QWidget):
|
||||
self.ai_btn.setText(tr("schedtask.ai_btn"))
|
||||
self.ai_btn.setToolTip(tr("schedtask.ai_tooltip"))
|
||||
for i, v in enumerate(_VIEWS):
|
||||
self.view_combo.setItemText(i, tr(f"schedtask.view.{v}"))
|
||||
self.view_tabs.setTabText(i, tr(f"schedtask.view.{v}"))
|
||||
for status, col in self.columns.items():
|
||||
col.setToolTip(tr(f"schedtask.col_tip.{status}"))
|
||||
self.refresh()
|
||||
|
||||
# ---- Kanban / Calendar view switch --------------------------------
|
||||
def _on_view_changed(self) -> None:
|
||||
self._view_stack.setCurrentIndex(self.view_combo.currentIndex())
|
||||
self._view_stack.setCurrentIndex(self.view_tabs.currentIndex())
|
||||
|
||||
def _add_task_on_date(self, date_str: str) -> None:
|
||||
"""Create a task pre-filled with the clicked calendar date (default
|
||||
@@ -213,9 +218,21 @@ class ScheduleTaskTab(QWidget):
|
||||
item = QListWidgetItem(self._card_text(t))
|
||||
item.setData(Qt.UserRole, t["task_id"])
|
||||
self.columns[status].addItem(item)
|
||||
pal = current_palette()
|
||||
for status, col in self.columns.items():
|
||||
self.column_headers[status].setText(
|
||||
f"{tr(f'schedtask.status.{status}')} ({counts[status]})")
|
||||
# Dropping a card into Running STARTS the task for real, so that
|
||||
# lane is outlined while it holds anything — the one column here
|
||||
# with a side effect should not look like the other six.
|
||||
if status == "running" and counts[status]:
|
||||
col.setStyleSheet(
|
||||
f"border: 1px solid {pal.warning}; border-radius: {pal.radius}px;")
|
||||
self.column_headers[status].setStyleSheet(
|
||||
f"font-weight:600; color: {pal.warning};")
|
||||
else:
|
||||
col.setStyleSheet("")
|
||||
self.column_headers[status].setStyleSheet("font-weight:600;")
|
||||
if col.count() == 0:
|
||||
empty = QListWidgetItem(tr("schedtask.no_tasks"))
|
||||
empty.setFlags(Qt.NoItemFlags)
|
||||
|
||||
@@ -57,6 +57,15 @@ class SettingsDialog(QDialog):
|
||||
self._select_combo(self.language_combo, ctx.config.language)
|
||||
top.addRow(tr("settings.language"), self.language_combo)
|
||||
|
||||
# Theme belongs with the other per-account settings. It is also on the
|
||||
# rail's account row (one click for the common flip); this is the same
|
||||
# value, named and explained, for people who come looking in Settings.
|
||||
self.theme_combo = QComboBox()
|
||||
for key in ("system", "dark", "light"):
|
||||
self.theme_combo.addItem(tr(f"settings.theme_{key}"), key)
|
||||
self._select_combo(self.theme_combo, getattr(ctx.config, "theme", "system"))
|
||||
top.addRow(tr("settings.theme"), self.theme_combo)
|
||||
|
||||
self.tray_chk = QCheckBox(tr("settings.tray_keep"))
|
||||
self.tray_chk.setChecked(bool(data.get("tray", {}).get("minimize_on_close", True)))
|
||||
top.addRow("", self.tray_chk)
|
||||
@@ -642,6 +651,9 @@ class SettingsDialog(QDialog):
|
||||
data = self.ctx.config.data
|
||||
data["active_provider"] = self.provider_combo.currentData()
|
||||
data["language"] = self.language_combo.currentData()
|
||||
# MainWindow._open_settings re-applies the theme after this returns, so
|
||||
# writing the value here is enough to make it take effect.
|
||||
data["theme"] = self.theme_combo.currentData()
|
||||
|
||||
self._stash_provider_fields()
|
||||
for key, staged in self._prov_staging.items():
|
||||
|
||||
+41
-29
@@ -18,7 +18,7 @@ from PySide6.QtGui import QBrush, QColor, QFont, QPen
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QFileDialog, QGraphicsEllipseItem, QGraphicsLineItem,
|
||||
QGraphicsScene, QGraphicsSimpleTextItem, QGraphicsView, QHBoxLayout,
|
||||
QLabel, QLineEdit, QPushButton, QSplitter, QStackedWidget,
|
||||
QLabel, QLineEdit, QPushButton, QSplitter, QStackedWidget, QTabBar,
|
||||
QTextBrowser, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -229,29 +229,36 @@ class StructureGraphView(QWidget):
|
||||
self._scan_btn.setIcon(icon("search"))
|
||||
self._scan_btn.setObjectName("primary")
|
||||
self._scan_btn.clicked.connect(self._scan)
|
||||
bar.addWidget(self.path_edit, 1)
|
||||
bar.addWidget(self._pick_btn)
|
||||
bar.addWidget(self.project_combo)
|
||||
bar.addWidget(self._scan_btn)
|
||||
root.addLayout(bar)
|
||||
self._refresh_project_combo()
|
||||
|
||||
# Toolbar: messages toggle + export
|
||||
bar2 = QHBoxLayout()
|
||||
bar2.addStretch(1)
|
||||
self._msgs_toggle_btn = QPushButton()
|
||||
self._msgs_toggle_btn.setIcon(icon("message"))
|
||||
self._msgs_toggle_btn.setToolTip(tr("structure.msgs_tooltip"))
|
||||
self._msgs_toggle_btn.clicked.connect(self._toggle_messages)
|
||||
bar2.addWidget(self._msgs_toggle_btn)
|
||||
|
||||
# ONE toolbar row. There used to be a second row holding just the
|
||||
# messages toggle and Export, which cost a whole row of height to carry
|
||||
# two buttons.
|
||||
self._export_btn = QPushButton()
|
||||
self._export_btn.setIcon(icon("upload"))
|
||||
self._export_btn.setObjectName("primary")
|
||||
self._export_btn.clicked.connect(self._export)
|
||||
bar2.addWidget(self._export_btn)
|
||||
bar.addWidget(self.path_edit, 1)
|
||||
bar.addWidget(self._pick_btn)
|
||||
bar.addWidget(self.project_combo)
|
||||
bar.addWidget(self._scan_btn)
|
||||
bar.addWidget(self._export_btn)
|
||||
root.addLayout(bar)
|
||||
self._refresh_project_combo()
|
||||
|
||||
root.addLayout(bar2)
|
||||
# Đồ thị | Tin nhắn as a real pair of tabs: the old single button
|
||||
# relabelled itself, so the view you were NOT looking at was the only
|
||||
# one named on screen.
|
||||
self.view_tabs = QTabBar()
|
||||
self.view_tabs.setObjectName("viewTabs")
|
||||
self.view_tabs.setDrawBase(False)
|
||||
self.view_tabs.setExpanding(False)
|
||||
self.view_tabs.addTab(icon("graph"), "")
|
||||
self.view_tabs.addTab(icon("message"), "")
|
||||
self.view_tabs.currentChanged.connect(self._on_view_tab)
|
||||
tab_row = QHBoxLayout()
|
||||
tab_row.setContentsMargins(0, 0, 0, 0)
|
||||
tab_row.addWidget(self.view_tabs)
|
||||
tab_row.addStretch(1)
|
||||
root.addLayout(tab_row)
|
||||
|
||||
split = QSplitter(Qt.Horizontal)
|
||||
self.scene = QGraphicsScene()
|
||||
@@ -337,8 +344,10 @@ class StructureGraphView(QWidget):
|
||||
self._pick_btn.setText(tr("structure.browse"))
|
||||
self._scan_btn.setText(tr("structure.scan"))
|
||||
self._export_btn.setText(tr("structure.export_png"))
|
||||
showing = self._stack.currentWidget() is getattr(self, "_msgs_view", None)
|
||||
self._msgs_toggle_btn.setText(tr("structure.graph_btn") if showing else tr("structure.msgs_btn"))
|
||||
# Both views are named at once now, so neither label depends on state.
|
||||
self.view_tabs.setTabText(0, tr("structure.graph_btn"))
|
||||
self.view_tabs.setTabText(1, tr("structure.msgs_btn"))
|
||||
self.view_tabs.setTabToolTip(1, tr("structure.msgs_tooltip"))
|
||||
self._ag_collapse.setToolTip(tr("structure.collapse_agent_tooltip"))
|
||||
self._ag_label.setText(tr("structure.agent_header"))
|
||||
self.ask_edit.setPlaceholderText(tr("structure.ask_placeholder"))
|
||||
@@ -413,16 +422,19 @@ class StructureGraphView(QWidget):
|
||||
self._rescan_timer.start()
|
||||
|
||||
# ---- Messages (by day, as JSON) --------------------------------------
|
||||
def _toggle_messages(self) -> None:
|
||||
"""Switch between the knowledge graph and the Messages-by-day view."""
|
||||
showing = self._stack.currentWidget() is self._msgs_view
|
||||
if showing:
|
||||
self._stack.setCurrentWidget(self.web if self.web is not None else self.view)
|
||||
else:
|
||||
def _on_view_tab(self, index: int) -> None:
|
||||
"""Tab 0 = graph, tab 1 = messages. Same two views as before, now named
|
||||
on screen instead of hidden behind one button's changing label."""
|
||||
if index == 1:
|
||||
self._reload_messages()
|
||||
self._stack.setCurrentWidget(self._msgs_view)
|
||||
self._msgs_toggle_btn.setText(
|
||||
tr("structure.graph_btn") if not showing else tr("structure.msgs_btn"))
|
||||
else:
|
||||
self._stack.setCurrentWidget(self.web if self.web is not None else self.view)
|
||||
|
||||
def _toggle_messages(self) -> None:
|
||||
"""Kept for callers that still ask for a flip (e.g. keyboard paths)."""
|
||||
showing = self._stack.currentWidget() is self._msgs_view
|
||||
self.view_tabs.setCurrentIndex(0 if showing else 1)
|
||||
|
||||
def _reload_messages(self) -> None:
|
||||
"""Build the tree: day → conversation. Click a conversation to see its
|
||||
|
||||
+43
-26
@@ -19,7 +19,7 @@ from PySide6.QtWidgets import (
|
||||
QCheckBox, QComboBox, QDateTimeEdit, QDialog, QDialogButtonBox, QFileDialog,
|
||||
QFormLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit,
|
||||
QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton,
|
||||
QScrollArea, QSpinBox, QVBoxLayout, QWidget,
|
||||
QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..config import PROVIDER_LABELS
|
||||
@@ -93,12 +93,11 @@ class TaskEditorDialog(QDialog):
|
||||
root = QVBoxLayout(content)
|
||||
|
||||
# ---- basics ----------------------------------------------------
|
||||
# Zero-height anchor: this block is a bare form, so the index needs
|
||||
# something to scroll to.
|
||||
self._anchor_basic = QWidget()
|
||||
self._anchor_basic.setFixedHeight(0)
|
||||
root.addWidget(self._anchor_basic)
|
||||
form = QFormLayout()
|
||||
# Held in its own widget so the whole block can be moved into a step
|
||||
# tab below; it is a bare form, not a group box.
|
||||
self._basic_box = QWidget()
|
||||
form = QFormLayout(self._basic_box)
|
||||
form.setContentsMargins(0, 0, 0, 0)
|
||||
self.title_edit = QLineEdit(self.task.get("title", ""))
|
||||
# Description is the source of truth. Its ✨ button GENERATES the Prompt
|
||||
# (Input) FROM the description — the title is just the task's label and
|
||||
@@ -211,7 +210,7 @@ class TaskEditorDialog(QDialog):
|
||||
form.addRow(tr("schedtask.f_skill"), self.skill_combo)
|
||||
form.addRow(tr("schedtask.f_priority"), self.priority_combo)
|
||||
form.addRow(tr("schedtask.f_status"), self.status_combo)
|
||||
root.addLayout(form)
|
||||
root.addWidget(self._basic_box)
|
||||
self._main_form = form
|
||||
self._model_box = model_box
|
||||
self._on_run_kind_changed() # apply agent/flow row visibility
|
||||
@@ -433,25 +432,38 @@ class TaskEditorDialog(QDialog):
|
||||
eform.addRow("", self.approval_chk)
|
||||
root.addWidget(eg)
|
||||
|
||||
# Five groups in one long scroll — same problem, same fix as Settings.
|
||||
from .widgets import section_index
|
||||
self.section_list = section_index(scroll, [
|
||||
(tr("schedtask.g_basic"), self._anchor_basic),
|
||||
(tr("schedtask.g_schedule"), sg),
|
||||
(tr("schedtask.g_input"), ig),
|
||||
(tr("schedtask.g_dependency"), dg),
|
||||
(tr("schedtask.g_execution"), eg),
|
||||
])
|
||||
# Three steps, as tabs: Nội dung → Lịch chạy → Liên kết. The five group
|
||||
# boxes are re-parented into three pages — none is dropped, they are
|
||||
# grouped by the question being answered rather than stacked in one
|
||||
# scroll where the later ones are out of sight.
|
||||
self.step_tabs = QTabWidget()
|
||||
self.step_tabs.setObjectName("taskSteps")
|
||||
pages = [
|
||||
("schedtask.step_content", [self._basic_box, ig]),
|
||||
("schedtask.step_schedule", [sg]),
|
||||
("schedtask.step_link", [dg, eg]),
|
||||
]
|
||||
self._step_keys = [key for key, _ in pages]
|
||||
for _key, groups in pages:
|
||||
page = QWidget()
|
||||
pv = QVBoxLayout(page)
|
||||
pv.setContentsMargins(4, 8, 4, 4)
|
||||
for g in groups:
|
||||
root.removeWidget(g)
|
||||
pv.addWidget(g)
|
||||
pv.addStretch(1)
|
||||
wrap = QScrollArea()
|
||||
wrap.setWidgetResizable(True)
|
||||
wrap.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
wrap.setWidget(page)
|
||||
self.step_tabs.addTab(wrap, "")
|
||||
outer.removeWidget(scroll)
|
||||
body = QHBoxLayout()
|
||||
body.setSpacing(10)
|
||||
body.addWidget(self.section_list)
|
||||
body.addWidget(scroll, 1)
|
||||
outer.insertLayout(0, body, 1)
|
||||
# Same floor as Settings: the dialog may not be narrower than its own
|
||||
# content at the font in use, so nothing is ever cut off sideways.
|
||||
self.setMinimumWidth(self.section_list.width()
|
||||
+ content.sizeHint().width() + 60)
|
||||
scroll.setParent(None)
|
||||
outer.insertWidget(0, self.step_tabs, 1)
|
||||
self._retranslate_steps()
|
||||
# Floor the width at what the widest page needs, at the font in use.
|
||||
self.setMinimumWidth(max(p.widget().sizeHint().width()
|
||||
for p in self.step_tabs.findChildren(QScrollArea)) + 60)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.button(QDialogButtonBox.Save).setIcon(icon("save"))
|
||||
@@ -465,6 +477,11 @@ class TaskEditorDialog(QDialog):
|
||||
from .widgets import guard_wheel
|
||||
guard_wheel(self)
|
||||
|
||||
def _retranslate_steps(self) -> None:
|
||||
"""Name the three step tabs for the current language."""
|
||||
for i, key in enumerate(self._step_keys):
|
||||
self.step_tabs.setTabText(i, f"{i + 1}. {tr(key)}")
|
||||
|
||||
def _apply_hints(self) -> None:
|
||||
"""Tooltip hints on every non-obvious control, so each option explains
|
||||
itself on hover."""
|
||||
|
||||
Reference in New Issue
Block a user