Align rail Settings, translate the help transcript, style the checkers
Settings sat 7px further in than the Dashboard/Giám sát rows above it — its
QSS gave it a 6px side margin where those rows start at the rail edge. At the
collapsed 54px width that put its icon near the middle of the rail, which is
what "thu gọn menu lại ra giữa" was describing. Icon and label now start on
the same x as the rows, open and collapsed, in both themes.
The help panel's transcript is rendered HTML, so switching language re-labelled
the chrome but left the greeting — and the "AI Assistant" speaker label — in
whatever language the panel was built in. retranslate() now rewrites the
greeting (matched by identity, so a real reply is never touched) and re-renders.
The sparkle is #FDBE59, sampled from the audit page's own render. Its CSS says
.spark{color:#0F9B8A}, but the glyph is the ✨ emoji and a colour emoji ignores
CSS colour, so the page has always drawn a gold star.
Behind all three: MainWindow does not style itself — run() calls
app.setStyleSheet — so 12 of 13 checkers were measuring a window with no
padding, margins or borders. Every QSS-driven layout bug was invisible to them,
and an unstyled window reported an icon drift that does not exist. Added
_apply_theme() and wired it through.
Two checker repairs that followed:
· check_no_hscroll flagged the 9pt dialogs on sizeHintForColumn(0), which
returns 182px at 9pt, 11pt and 14pt alike. Nothing was clipped. It now
compares the painted text against the width actually on screen, and fails
on a squeezed list (24 combos) where the old test passed.
· the checkers print Vietnamese and died mid-report on a cp932 console.
New: check_rail_align (icons hold one line, both themes, both states) and
check_help_i18n (transcript follows the language). Both verified to fail
without their fix.
15/15 checkers pass. check_nav and check_design_parity segfault in Qt teardown
roughly one run in three — pre-existing, after the verdict prints, and it
happens with or without the theme change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a358a20556
commit
d060d5679a
@@ -471,11 +471,16 @@ QComboBox#navProjectPick {
|
|||||||
}
|
}
|
||||||
QPushButton#navSettingsBtn {
|
QPushButton#navSettingsBtn {
|
||||||
background: transparent; border: none; color: $text_muted;
|
background: transparent; border: none; color: $text_muted;
|
||||||
padding: 6px 8px; text-align: left; border-radius: ${radius}px; margin: 2px 6px 6px 6px;
|
padding: 6px 8px 6px 7px; text-align: left; border-radius: ${radius}px;
|
||||||
|
/* No side margin: Settings reads as one more row under Dashboard/Giám sát,
|
||||||
|
so its icon has to start on their x. A 6px margin put it at 14 — near
|
||||||
|
enough the middle of the collapsed 54px rail to look centred. */
|
||||||
|
margin: 2px 0px 6px 0px;
|
||||||
}
|
}
|
||||||
QPushButton#navSettingsBtn:hover { background: $nav_hover; color: $text; }
|
QPushButton#navSettingsBtn:hover { background: $nav_hover; color: $text; }
|
||||||
QPushButton#navSettingsBtn:pressed { background: $active; }
|
QPushButton#navSettingsBtn:pressed { background: $active; }
|
||||||
|
|
||||||
|
|
||||||
/* ---- surfaces --------------------------------------------------------- */
|
/* ---- surfaces --------------------------------------------------------- */
|
||||||
QGroupBox {
|
QGroupBox {
|
||||||
background: $surface; border: 1px solid $border; border-radius: ${radius_lg}px;
|
background: $surface; border: 1px solid $border; border-radius: ${radius_lg}px;
|
||||||
|
|||||||
@@ -75,6 +75,22 @@ def _load_fonts() -> int:
|
|||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_theme(app, name: str | None = None) -> str:
|
||||||
|
"""Load the app's real stylesheet onto `app`.
|
||||||
|
|
||||||
|
`MainWindow` does not style itself — `run()` calls `app.setStyleSheet` — so a
|
||||||
|
checker that builds the window directly measures a window with no padding,
|
||||||
|
no margins and no borders. Every QSS-driven layout bug is invisible there.
|
||||||
|
"""
|
||||||
|
from cowork_local import theme
|
||||||
|
from cowork_local.config import AppConfig
|
||||||
|
|
||||||
|
name = name or AppConfig.load().theme
|
||||||
|
theme.set_active_theme(name)
|
||||||
|
app.setStyleSheet(theme.stylesheet(name))
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def _freeze_schedulers() -> None:
|
def _freeze_schedulers() -> None:
|
||||||
"""No-op the background engines so nothing is executed while we capture."""
|
"""No-op the background engines so nothing is executed while we capture."""
|
||||||
from cowork_local.core.task_scheduler import TaskScheduler
|
from cowork_local.core.task_scheduler import TaskScheduler
|
||||||
|
|||||||
+4
-1
@@ -12,6 +12,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -19,7 +21,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
# Every control the sidebar and the flow area had before these changes.
|
# Every control the sidebar and the flow area had before these changes.
|
||||||
EXPECTED = [
|
EXPECTED = [
|
||||||
@@ -40,6 +42,7 @@ def main() -> int:
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
_load_fonts()
|
_load_fonts()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled widget, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -23,7 +25,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
# controls.json lists every control in a FILE, and several files hold more than
|
# controls.json lists every control in a FILE, and several files hold more than
|
||||||
# one class (schedule_task_tab.py alone has the tab plus three dialogs). Only
|
# one class (schedule_task_tab.py alone has the tab plus three dialogs). Only
|
||||||
@@ -125,6 +127,7 @@ def main() -> int:
|
|||||||
_load_fonts()
|
_load_fonts()
|
||||||
_freeze_schedulers()
|
_freeze_schedulers()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled window, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -17,7 +19,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
HEADER = ["_title", "chart_prev_btn", "_chart_period_lbl", "chart_next_btn",
|
HEADER = ["_title", "chart_prev_btn", "_chart_period_lbl", "chart_next_btn",
|
||||||
"gran_combo", "metric_combo", "currency_lbl", "currency_combo",
|
"gran_combo", "metric_combo", "currency_lbl", "currency_combo",
|
||||||
@@ -31,6 +33,7 @@ def main() -> int:
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
_load_fonts()
|
_load_fonts()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled widget, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys␍
|
import sys␍
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -23,7 +25,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def page_proposals():
|
def page_proposals():
|
||||||
@@ -57,6 +59,7 @@ def build():
|
|||||||
_load_fonts()
|
_load_fonts()
|
||||||
_freeze_schedulers()
|
_freeze_schedulers()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled window, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -18,7 +20,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
# Every field each dialog must still offer after the move.
|
# Every field each dialog must still offer after the move.
|
||||||
SETTINGS_FIELDS = [
|
SETTINGS_FIELDS = [
|
||||||
@@ -78,6 +80,7 @@ def main() -> int:
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
_load_fonts()
|
_load_fonts()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled widget, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -18,7 +20,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -28,6 +30,7 @@ def main() -> int:
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
_load_fonts()
|
_load_fonts()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled widget, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""The help panel must follow a language switch — transcript included.
|
||||||
|
|
||||||
|
retranslate() re-labelled the chrome but not the rendered HTML transcript, so
|
||||||
|
the greeting and the "AI Assistant" speaker label stayed in the language the
|
||||||
|
panel happened to be built in.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(REPO.parent))
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from capture_screens import ( # noqa: E402
|
||||||
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
sandbox = _isolate_home()
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8") # ja/vi text on a cp932 console
|
||||||
|
app = QApplication([])
|
||||||
|
_load_fonts()
|
||||||
|
_freeze_schedulers()
|
||||||
|
_apply_theme(app)
|
||||||
|
|
||||||
|
from cowork_local.config import CONFIG_DIR
|
||||||
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
from seed_demo_data import seed
|
||||||
|
seed()
|
||||||
|
|
||||||
|
from cowork_local.app import MainWindow
|
||||||
|
from cowork_local.config import AppConfig
|
||||||
|
from cowork_local.i18n import set_language
|
||||||
|
from cowork_local.state import AppContext
|
||||||
|
|
||||||
|
set_language("vi")
|
||||||
|
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
||||||
|
win.resize(1400, 900)
|
||||||
|
win.show()
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
panel = win.help_agent
|
||||||
|
fails = []
|
||||||
|
for lang in ("en", "ja", "vi"):
|
||||||
|
win._set_language(lang) if hasattr(win, "_set_language") else set_language(lang)
|
||||||
|
if not hasattr(win, "_set_language"):
|
||||||
|
panel.retranslate()
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
# The panel greets by the signed-in name, not the placeholder.
|
||||||
|
want = panel._greeting()
|
||||||
|
shown = re.sub("<[^>]+>", " ", panel.log.toHtml())
|
||||||
|
shown = " ".join(shown.split())
|
||||||
|
# compare on the stable half of the sentence, the name is substituted
|
||||||
|
probe = " ".join(want.split())[:28]
|
||||||
|
state = "ok" if probe and probe in shown else "MISSING"
|
||||||
|
print(f"{lang}: title={panel.title.text()!r} greeting={state}")
|
||||||
|
if state != "ok":
|
||||||
|
print(f" muon: {probe!r}")
|
||||||
|
print(f" thay: {shown[:160]!r}")
|
||||||
|
if state != "ok":
|
||||||
|
fails.append(f"{lang}: transcript still shows another language")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("PASS panel follows the language" if not fails
|
||||||
|
else "\n".join(f"FAIL {f}" for f in fails))
|
||||||
|
sys.stdout.flush()
|
||||||
|
os._exit(1 if fails else 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -12,6 +12,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys␍
|
import sys␍
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -19,7 +21,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
# The rail, top to bottom, as the audit page's rail() helper draws it.
|
# The rail, top to bottom, as the audit page's rail() helper draws it.
|
||||||
RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"]
|
RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"]
|
||||||
@@ -39,6 +41,7 @@ def main() -> int:
|
|||||||
_load_fonts()
|
_load_fonts()
|
||||||
_freeze_schedulers()
|
_freeze_schedulers()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled window, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -23,7 +25,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
# Real-world panels, from a small laptop up to 4K-at-150%-effective.
|
# Real-world panels, from a small laptop up to 4K-at-150%-effective.
|
||||||
SIZES = [(1280, 720), (1366, 768), (1600, 900), (1920, 1080), (2560, 1440)]
|
SIZES = [(1280, 720), (1366, 768), (1600, 900), (1920, 1080), (2560, 1440)]
|
||||||
@@ -40,6 +42,7 @@ def main() -> int:
|
|||||||
_load_fonts()
|
_load_fonts()
|
||||||
_freeze_schedulers()
|
_freeze_schedulers()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled window, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -15,6 +15,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -22,7 +24,7 @@ sys.path.insert(0, str(REPO.parent)) # `import cowork_loc
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent)) # sibling tools
|
sys.path.insert(0, str(Path(__file__).resolve().parent)) # sibling tools
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def rows(tree):
|
def rows(tree):
|
||||||
@@ -44,6 +46,7 @@ def main() -> int:
|
|||||||
_load_fonts()
|
_load_fonts()
|
||||||
_freeze_schedulers()
|
_freeze_schedulers()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled window, not a bare one
|
||||||
from cowork_local.config import CONFIG_DIR
|
from cowork_local.config import CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
@@ -18,7 +20,7 @@ sys.path.insert(0, str(REPO.parent))
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from capture_screens import _isolate_home, _load_fonts # noqa: E402
|
from capture_screens import _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
WIDTHS = (1100, 964, 820, 700)
|
WIDTHS = (1100, 964, 820, 700)
|
||||||
POINTS = (9, 11, 14) # ~100%, ~125%, ~150% display scaling
|
POINTS = (9, 11, 14) # ~100%, ~125%, ~150% display scaling
|
||||||
@@ -54,10 +56,40 @@ def hscroll(dlg, app):
|
|||||||
idx = dlg.findChild(QListWidget, "sectionIndex")
|
idx = dlg.findChild(QListWidget, "sectionIndex")
|
||||||
over_idx = False
|
over_idx = False
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
over_idx = idx.sizeHintForColumn(0) > idx.viewport().width()
|
over_idx = _index_elides(idx)
|
||||||
return over_area, over_idx
|
return over_area, over_idx
|
||||||
|
|
||||||
|
|
||||||
|
def _index_elides(idx) -> bool:
|
||||||
|
"""True when a section name does not fit the visible width of the list.
|
||||||
|
|
||||||
|
Two earlier attempts got this wrong:
|
||||||
|
· `sizeHintForColumn(0) > viewport().width()` returns 182px at 9pt, 11pt
|
||||||
|
and 14pt alike — it does not track the font, so it called the 9pt
|
||||||
|
dialog broken while nothing on screen was clipped.
|
||||||
|
· Asking the delegate whether it elides. It does not: the view lays each
|
||||||
|
row out at its natural width and the viewport simply clips what runs
|
||||||
|
past it, so a list squeezed to 90px still reported "no elision".
|
||||||
|
|
||||||
|
So compare the painted text against the width that is actually on screen.
|
||||||
|
"""
|
||||||
|
from PySide6.QtGui import QFontMetrics
|
||||||
|
from PySide6.QtWidgets import QStyle, QStyleOptionViewItem
|
||||||
|
|
||||||
|
for i in range(idx.count()):
|
||||||
|
row = idx.indexFromItem(idx.item(i))
|
||||||
|
opt = QStyleOptionViewItem()
|
||||||
|
idx.initViewItemOption(opt)
|
||||||
|
opt.rect = idx.visualRect(row)
|
||||||
|
idx.itemDelegate().initStyleOption(opt, row)
|
||||||
|
box = idx.style().subElementRect(QStyle.SE_ItemViewItemText, opt, idx)
|
||||||
|
label = idx.item(i).text()
|
||||||
|
visible = idx.viewport().width() - box.left()
|
||||||
|
if QFontMetrics(opt.font).horizontalAdvance(label) > visible:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
sandbox = _isolate_home()
|
sandbox = _isolate_home()
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont
|
||||||
@@ -66,6 +98,7 @@ def main() -> int:
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
_load_fonts()
|
_load_fonts()
|
||||||
|
|
||||||
|
_apply_theme(app) # measure the styled widget, not a bare one
|
||||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import ast
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import os
|
|||||||
import runpy
|
import runpy
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Rail icons must sit on one vertical line, and stay there when it collapses.
|
||||||
|
|
||||||
|
Two bugs this catches:
|
||||||
|
· "Cài đặt" sat 42px right of "Dashboard"/"Giám sát" — its QSS margin pushed
|
||||||
|
the button in while the tree rows above start at the rail edge.
|
||||||
|
· Collapsing re-placed the icon of every label-less button, sliding + to the
|
||||||
|
middle of the 54px rail.
|
||||||
|
|
||||||
|
Runs with the app's real stylesheet loaded. Without it the window has no
|
||||||
|
padding, margins or borders and neither bug is visible — see _apply_theme.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(REPO.parent))
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from capture_screens import ( # noqa: E402
|
||||||
|
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts)
|
||||||
|
|
||||||
|
TOL = 2 # px; anti-aliasing on an icon edge
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
sandbox = _isolate_home()
|
||||||
|
from PySide6.QtCore import QPoint
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
_load_fonts()
|
||||||
|
_freeze_schedulers()
|
||||||
|
theme_name = _apply_theme(app)
|
||||||
|
|
||||||
|
from cowork_local.config import CONFIG_DIR
|
||||||
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
from seed_demo_data import seed
|
||||||
|
seed()
|
||||||
|
|
||||||
|
from cowork_local.app import MainWindow
|
||||||
|
from cowork_local.config import AppConfig
|
||||||
|
from cowork_local.i18n import set_language
|
||||||
|
from cowork_local.state import AppContext
|
||||||
|
|
||||||
|
set_language("vi")
|
||||||
|
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
||||||
|
win.resize(1400, 900)
|
||||||
|
win.show()
|
||||||
|
app.processEvents()
|
||||||
|
rail = win._nav_wrap
|
||||||
|
|
||||||
|
def ink_x(w):
|
||||||
|
"""Leftmost painted pixel of a widget, in rail coordinates."""
|
||||||
|
img = w.grab().toImage()
|
||||||
|
bg = img.pixelColor(w.width() - 3, 2)
|
||||||
|
for x in range(img.width()):
|
||||||
|
for y in range(2, img.height() - 2):
|
||||||
|
c = img.pixelColor(x, y)
|
||||||
|
if (abs(c.red() - bg.red()) + abs(c.green() - bg.green())
|
||||||
|
+ abs(c.blue() - bg.blue())) > 60:
|
||||||
|
return w.mapTo(rail, QPoint(x, 0)).x()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tree_text_x(tree):
|
||||||
|
from PySide6.QtWidgets import QStyle, QStyleOptionViewItem
|
||||||
|
index = tree.indexFromItem(tree.topLevelItem(0), 0)
|
||||||
|
opt = QStyleOptionViewItem()
|
||||||
|
tree.initViewItemOption(opt)
|
||||||
|
opt.rect = tree.visualRect(index)
|
||||||
|
tree.itemDelegate().initStyleOption(opt, index)
|
||||||
|
txt = tree.style().subElementRect(QStyle.SE_ItemViewItemText, opt, tree)
|
||||||
|
return tree.mapTo(rail, QPoint(txt.left(), 0)).x()
|
||||||
|
|
||||||
|
def btn_text_x(w):
|
||||||
|
"""Left edge of the label: first ink past the icon's gap."""
|
||||||
|
from PySide6.QtGui import QIcon
|
||||||
|
if not w.text():
|
||||||
|
return None
|
||||||
|
img = w.grab().toImage()
|
||||||
|
bg = img.pixelColor(w.width() - 3, 2)
|
||||||
|
ink = []
|
||||||
|
for x in range(img.width()):
|
||||||
|
for y in range(2, img.height() - 2):
|
||||||
|
c = img.pixelColor(x, y)
|
||||||
|
if (abs(c.red() - bg.red()) + abs(c.green() - bg.green())
|
||||||
|
+ abs(c.blue() - bg.blue())) > 60:
|
||||||
|
ink.append(x)
|
||||||
|
break
|
||||||
|
if not ink:
|
||||||
|
return None
|
||||||
|
for a, b in zip(ink, ink[1:]): # first gap = icon/label spacing
|
||||||
|
if b - a > 2:
|
||||||
|
return w.mapTo(rail, QPoint(b, 0)).x()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tree_icon_x(tree):
|
||||||
|
from PySide6.QtWidgets import QStyle, QStyleOptionViewItem
|
||||||
|
index = tree.indexFromItem(tree.topLevelItem(0), 0)
|
||||||
|
opt = QStyleOptionViewItem()
|
||||||
|
tree.initViewItemOption(opt)
|
||||||
|
opt.rect = tree.visualRect(index)
|
||||||
|
tree.itemDelegate().initStyleOption(opt, index)
|
||||||
|
deco = tree.style().subElementRect(
|
||||||
|
QStyle.SE_ItemViewItemDecoration, opt, tree)
|
||||||
|
return tree.mapTo(rail, QPoint(deco.left(), 0)).x()
|
||||||
|
|
||||||
|
def snapshot():
|
||||||
|
app.processEvents()
|
||||||
|
out = {"nav rows": tree_icon_x(win.nav),
|
||||||
|
"bottom rows": tree_icon_x(win.nav_bottom),
|
||||||
|
"bottom rows text": tree_text_x(win.nav_bottom)}
|
||||||
|
for label, attr in (("MENU", "_nav_toggle_btn"),
|
||||||
|
("new chat", "nav_new_chat"),
|
||||||
|
("settings", "_nav_settings_btn")):
|
||||||
|
w = getattr(win, attr, None)
|
||||||
|
if w is not None and w.isVisible():
|
||||||
|
out[label] = ink_x(w)
|
||||||
|
if label == "settings":
|
||||||
|
out["settings text"] = btn_text_x(w)
|
||||||
|
return out
|
||||||
|
|
||||||
|
fails = []
|
||||||
|
for theme_name in ("dark", "light"):
|
||||||
|
_apply_theme(app, theme_name)
|
||||||
|
app.processEvents()
|
||||||
|
if win._nav_collapsed:
|
||||||
|
win._toggle_nav()
|
||||||
|
opened = snapshot()
|
||||||
|
win._toggle_nav()
|
||||||
|
app.processEvents()
|
||||||
|
closed = snapshot()
|
||||||
|
win._toggle_nav()
|
||||||
|
app.processEvents()
|
||||||
|
fails += compare(theme_name, rail, opened, closed)
|
||||||
|
|
||||||
|
print()
|
||||||
|
for f in fails:
|
||||||
|
print(f"FAIL {f}")
|
||||||
|
print("PASS every rail icon holds its line" if not fails
|
||||||
|
else f"{len(fails)} problem(s)")
|
||||||
|
sys.stdout.flush()
|
||||||
|
os._exit(1 if fails else 0)
|
||||||
|
|
||||||
|
|
||||||
|
def compare(theme_name, rail, opened, closed):
|
||||||
|
print()
|
||||||
|
print(f"theme={theme_name}")
|
||||||
|
print(f"{'element':<12}{'open':>7}{'collapsed':>11}{'drift':>8}")
|
||||||
|
fails = []
|
||||||
|
for key in opened:
|
||||||
|
a, b = opened[key], closed.get(key)
|
||||||
|
drift = "-" if a is None or b is None else f"{b - a:+d}"
|
||||||
|
print(f"{key:<12}{str(a):>7}{str(b):>11}{drift:>8}")
|
||||||
|
if a is not None and b is not None and abs(b - a) > TOL:
|
||||||
|
fails.append(f"{key}: icon moves {b - a:+d}px when the rail collapses")
|
||||||
|
|
||||||
|
# Settings is a button but reads as one more row in the bottom list, so
|
||||||
|
# both its icon and its label have to start where theirs do.
|
||||||
|
for state, snap in (("open", opened), ("collapsed", closed)):
|
||||||
|
for what in ("", " text"):
|
||||||
|
ref, got = snap.get("bottom rows" + what), snap.get("settings" + what)
|
||||||
|
if ref is not None and got is not None and abs(got - ref) > TOL:
|
||||||
|
fails.append(f"{theme_name} {state}: settings{what} x={got} but "
|
||||||
|
f"the rows above it start at x={ref}")
|
||||||
|
return fails
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
|||||||
+23
-9
@@ -46,7 +46,12 @@ _PANEL_W, _PANEL_H = 340, 460 # expanded chat panel size
|
|||||||
# assistant is teal, not the app accent, and the same in both themes —
|
# assistant is teal, not the app accent, and the same in both themes —
|
||||||
# it is one recognisable object floating over every screen.
|
# it is one recognisable object floating over every screen.
|
||||||
_TEAL_BG, _TEAL_LINE = "#E6F6F4", "#7FD0C4"
|
_TEAL_BG, _TEAL_LINE = "#E6F6F4", "#7FD0C4"
|
||||||
_TEAL_TEXT, _TEAL_SPARK = "#0F6E62", "#0F9B8A"
|
_TEAL_TEXT = "#0F6E62"
|
||||||
|
# The page's CSS says .spark{color:#0F9B8A}, but the glyph is the ✨ EMOJI and a
|
||||||
|
# colour emoji ignores CSS colour — so what the page actually renders is the
|
||||||
|
# gold star. Sampled from the page's own render of section 27 (1055 pixels of
|
||||||
|
# the star, averaged): #FDBE59.
|
||||||
|
_SPARK_GOLD = "#FDBE59"
|
||||||
|
|
||||||
# The three states the floating assistant cycles through.
|
# The three states the floating assistant cycles through.
|
||||||
_HIDDEN, _LAUNCHER_ST, _PANEL = "hidden", "launcher", "panel"
|
_HIDDEN, _LAUNCHER_ST, _PANEL = "hidden", "launcher", "panel"
|
||||||
@@ -120,9 +125,12 @@ class HelpAgentWidget(QWidget):
|
|||||||
self._worker: Optional[AgentWorker] = None
|
self._worker: Optional[AgentWorker] = None
|
||||||
# Conversation history (excludes the system prompt, prepended per call).
|
# Conversation history (excludes the system prompt, prepended per call).
|
||||||
# Seeded with the greeting so the panel always opens on a friendly hello.
|
# Seeded with the greeting so the panel always opens on a friendly hello.
|
||||||
self._history: List[Dict[str, str]] = [
|
# Kept by identity so retranslate() can rewrite it without having to
|
||||||
{"role": "assistant", "content": self._greeting()}
|
# guess which language the visible text is in — and without touching a
|
||||||
]
|
# real reply that happens to look like a greeting.
|
||||||
|
self._greet_msg: Dict[str, str] = {
|
||||||
|
"role": "assistant", "content": self._greeting()}
|
||||||
|
self._history: List[Dict[str, str]] = [self._greet_msg]
|
||||||
self.setAttribute(Qt.WA_StyledBackground, True)
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||||
self._pal = self._compute_palette()
|
self._pal = self._compute_palette()
|
||||||
self._build_edge_tab()
|
self._build_edge_tab()
|
||||||
@@ -147,7 +155,7 @@ class HelpAgentWidget(QWidget):
|
|||||||
self._apply_style()
|
self._apply_style()
|
||||||
muted = self._pal.text_muted
|
muted = self._pal.text_muted
|
||||||
self.edge_tab.setIcon(icon("chevron-left", color=_TEAL_TEXT))
|
self.edge_tab.setIcon(icon("chevron-left", color=_TEAL_TEXT))
|
||||||
self.launcher.setIcon(icon("sparkle", size=_DOT_ICON, color=_TEAL_SPARK))
|
self.launcher.setIcon(icon("sparkle", size=_DOT_ICON, color=_SPARK_GOLD))
|
||||||
self.min_btn.setIcon(icon("minus", color=muted))
|
self.min_btn.setIcon(icon("minus", color=muted))
|
||||||
self._render()
|
self._render()
|
||||||
|
|
||||||
@@ -164,8 +172,8 @@ class HelpAgentWidget(QWidget):
|
|||||||
border-radius: {_DOT // 2}px; color: {_TEAL_TEXT}; font-weight: 700;
|
border-radius: {_DOT // 2}px; color: {_TEAL_TEXT}; font-weight: 700;
|
||||||
font-size: 12px; padding: 0; text-align: center; }}
|
font-size: 12px; padding: 0; text-align: center; }}
|
||||||
#helpLauncher[pill="true"] {{ text-align: left; padding-left: 6px; }}
|
#helpLauncher[pill="true"] {{ text-align: left; padding-left: 6px; }}
|
||||||
#helpLauncher:hover {{ background: #D5EFEA; border-color: {_TEAL_SPARK}; }}
|
#helpLauncher:hover {{ background: #D5EFEA; border-color: {_TEAL_LINE}; }}
|
||||||
#helpLauncher:focus {{ border: 1px solid {_TEAL_SPARK}; }}
|
#helpLauncher:focus {{ border: 1px solid {_TEAL_TEXT}; }}
|
||||||
#helpEdgeTab {{ background: {_TEAL_BG}; border: 1px solid {_TEAL_LINE};
|
#helpEdgeTab {{ background: {_TEAL_BG}; border: 1px solid {_TEAL_LINE};
|
||||||
border-right: none; border-top-left-radius: {r}px;
|
border-right: none; border-top-left-radius: {r}px;
|
||||||
border-bottom-left-radius: {r}px; }}
|
border-bottom-left-radius: {r}px; }}
|
||||||
@@ -213,7 +221,7 @@ class HelpAgentWidget(QWidget):
|
|||||||
# is gone — hiding to the edge is now a line in the panel's ⋯ menu.
|
# is gone — hiding to the edge is now a line in the panel's ⋯ menu.
|
||||||
self.launcher = _HoverPill(self)
|
self.launcher = _HoverPill(self)
|
||||||
self.launcher.setObjectName("helpLauncher")
|
self.launcher.setObjectName("helpLauncher")
|
||||||
self.launcher.setIcon(icon("sparkle", size=_DOT_ICON, color=_TEAL_SPARK))
|
self.launcher.setIcon(icon("sparkle", size=_DOT_ICON, color=_SPARK_GOLD))
|
||||||
self.launcher.setCursor(Qt.PointingHandCursor)
|
self.launcher.setCursor(Qt.PointingHandCursor)
|
||||||
self.launcher.setToolTip(tr("help_agent.open_tooltip"))
|
self.launcher.setToolTip(tr("help_agent.open_tooltip"))
|
||||||
self.launcher.clicked.connect(self._expand)
|
self.launcher.clicked.connect(self._expand)
|
||||||
@@ -233,7 +241,7 @@ class HelpAgentWidget(QWidget):
|
|||||||
hb.setContentsMargins(12, 8, 8, 8)
|
hb.setContentsMargins(12, 8, 8, 8)
|
||||||
self.title_icon = QLabel(header)
|
self.title_icon = QLabel(header)
|
||||||
self.title_icon.setPixmap(
|
self.title_icon.setPixmap(
|
||||||
icon("sparkle", size=16, color=_TEAL_SPARK).pixmap(16, 16))
|
icon("sparkle", size=16, color=_SPARK_GOLD).pixmap(16, 16))
|
||||||
hb.addWidget(self.title_icon)
|
hb.addWidget(self.title_icon)
|
||||||
self.title = QLabel(tr("help_agent.title"), header)
|
self.title = QLabel(tr("help_agent.title"), header)
|
||||||
self.title.setObjectName("helpTitle")
|
self.title.setObjectName("helpTitle")
|
||||||
@@ -444,6 +452,12 @@ class HelpAgentWidget(QWidget):
|
|||||||
self.send_btn.setEnabled(not busy)
|
self.send_btn.setEnabled(not busy)
|
||||||
|
|
||||||
def retranslate(self) -> None:
|
def retranslate(self) -> None:
|
||||||
|
# The transcript is rendered HTML, so switching language left the
|
||||||
|
# greeting — and every "AI Assistant" speaker label — in the language
|
||||||
|
# the panel was built in.
|
||||||
|
if self._history and self._history[0] is self._greet_msg:
|
||||||
|
self._greet_msg["content"] = self._greeting()
|
||||||
|
self._render()
|
||||||
self.title.setText(tr("help_agent.title"))
|
self.title.setText(tr("help_agent.title"))
|
||||||
self.input.setPlaceholderText(tr("help_agent.placeholder"))
|
self.input.setPlaceholderText(tr("help_agent.placeholder"))
|
||||||
self.launcher.setToolTip(tr("help_agent.open_tooltip"))
|
self.launcher.setToolTip(tr("help_agent.open_tooltip"))
|
||||||
|
|||||||
Reference in New Issue
Block a user