fix(i18n): đổi ngôn ngữ áp lại toàn bộ chữ trên màn hình
Trước đây rất nhiều widget gọi setText(tr(...)) một lần lúc dựng, nên sau khi đổi ngôn ngữ một nửa màn hình vẫn giữ tiếng cũ. Thêm họ bind_text/bind_tip/ bind_placeholder/bind_items/bind_dynamic trong i18n: khoá dịch được gắn thẳng vào widget (giữ tham chiếu yếu) và tự áp lại mỗi lần set_language. - co4e sidebar, node property panel, run control, routing toggle, nav rail, top bar, filter scaffold, usage chart: chuyển sang binding hoặc tự đăng ký on_language_changed thay vì trông chờ nơi nhúng. - RoutingToggle/AutoRunToggle tự đăng ký retranslate và dùng AdjustToContents để bản dịch dài không bị cắt. - BusyOverlay mới: che cửa sổ trong lúc set_language chạy đồng bộ trên GUI thread, tránh cảm giác treo app. - co4e sidebar chỉ đọc thư viện skill một lần (_skill_prefix_lookup) thay vì quét đĩa cho từng skill — trước đó mỗi lần reload tốn ~3.8s đứng GUI. - Bổ sung bản dịch ja/vi còn để nguyên tiếng Anh; sửa chiều cao hàng Settings ở nav rail. - Thêm 4 bộ test UI cho các thay đổi trên. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
58a2a4507d
commit
9459dbe197
@@ -35,7 +35,7 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_text, bind_tip
|
||||
from ...ui.icons import icon
|
||||
from .palette_list import _PaletteList
|
||||
|
||||
@@ -55,9 +55,11 @@ class AgentListPanel(QWidget):
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
"""Danh sách agent ở cột trái Co4E Studio, kèm nút tạo mới."""
|
||||
super().__init__(parent)
|
||||
self.new_btn = QPushButton(tr("co4e.new"))
|
||||
# Bound, not set once: this panel has no retranslate hook of its own, and
|
||||
# Co4ETab (which owns the language callback) cannot reach these tooltips.
|
||||
self.new_btn = bind_text(QPushButton(), "co4e.new")
|
||||
self.new_btn.setIcon(icon("plus"))
|
||||
self.new_btn.setToolTip(tr("co4e.tt_new_agent"))
|
||||
bind_tip(self.new_btn, "co4e.tt_new_agent")
|
||||
self.new_btn.setObjectName("co4eSectionAction")
|
||||
self.new_btn.setFlat(True)
|
||||
self.new_btn.setCursor(Qt.PointingHandCursor)
|
||||
@@ -75,11 +77,11 @@ class AgentListPanel(QWidget):
|
||||
# Edit/delete act on the selected row, so they stay with the list.
|
||||
self.edit_btn = QPushButton()
|
||||
self.edit_btn.setIcon(icon("edit"))
|
||||
self.edit_btn.setToolTip(tr("co4e.tt_edit_agent"))
|
||||
bind_tip(self.edit_btn, "co4e.tt_edit_agent")
|
||||
self.edit_btn.setFixedWidth(34)
|
||||
self.del_btn = QPushButton()
|
||||
self.del_btn.setIcon(icon("trash"))
|
||||
self.del_btn.setToolTip(tr("co4e.tt_del_agent"))
|
||||
bind_tip(self.del_btn, "co4e.tt_del_agent")
|
||||
self.del_btn.setFixedWidth(34)
|
||||
# KHONG noi .clicked o day: cung ly do nhu new_btn o tren.
|
||||
btns.addWidget(self.edit_btn)
|
||||
|
||||
@@ -13,7 +13,7 @@ from PySide6.QtWidgets import QSplitter, QWidget
|
||||
from ...core import co4e, skills as skills_mod
|
||||
from ...core.co4e_builtins import BUILTIN_AGENTS
|
||||
from ...core.worker import AgentWorker
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_dynamic, tr
|
||||
from ...ui.chat_view import ChatView
|
||||
from ...ui.icons import icon
|
||||
from ...presentation.co4e.co4e_chat_view import ChatPanel
|
||||
@@ -55,6 +55,12 @@ class Co4EChatMixin:
|
||||
self._co4e_routed_provider = None # routing provider override for the next turn
|
||||
self._vsplit_sizes = [540, 220] # sizes to restore when expanded
|
||||
self._msgs_collapsed = True
|
||||
# The tooltip names the action the button would perform, so it depends on
|
||||
# which way the box is folded — and the fold state lives here, not in the
|
||||
# panel. Bound so a language change re-reads it instead of freezing the
|
||||
# wording set when the tab was built.
|
||||
bind_dynamic(self.chat_toggle_btn, lambda: self.chat_toggle_btn.setToolTip(
|
||||
tr("co4e.tt_expand_msgs" if self._msgs_collapsed else "co4e.tt_collapse_msgs")))
|
||||
return panel
|
||||
def _toggle_messages(self) -> None:
|
||||
"""Show/hide the WHOLE chat box (message list + composer) below the
|
||||
|
||||
@@ -48,7 +48,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from ...core import co4e, skills as skills_mod
|
||||
from ...core.co4e_builtins import BUILTIN_AGENTS
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_placeholder, bind_text, tr
|
||||
from ...theme import current_palette
|
||||
from ...ui.icons import icon
|
||||
from ...ui.routing_toggle import RoutingToggle
|
||||
@@ -223,7 +223,8 @@ class ChatPanel(QWidget):
|
||||
self.header = QWidget(); self.header.setObjectName("msgHeader")
|
||||
mh = QHBoxLayout(self.header); mh.setContentsMargins(6, 3, 6, 3); mh.setSpacing(6)
|
||||
self.msgs_icon = QLabel(); self.msgs_icon.setPixmap(icon("message").pixmap(14, 14))
|
||||
self.msgs_title = QLabel(tr("co4e.messages")); self.msgs_title.setObjectName("hint")
|
||||
self.msgs_title = bind_text(QLabel(), "co4e.messages")
|
||||
self.msgs_title.setObjectName("hint")
|
||||
self.chat_toggle_btn = QPushButton()
|
||||
self.chat_toggle_btn.setObjectName("msgToggle")
|
||||
self.chat_toggle_btn.setFlat(True)
|
||||
@@ -253,10 +254,11 @@ class ChatPanel(QWidget):
|
||||
crow.addWidget(self.usage_total_lbl)
|
||||
_inp = QWidget(); row = QHBoxLayout(_inp); row.setContentsMargins(0, 0, 0, 0)
|
||||
self.chat_input = _ChatInput()
|
||||
self.chat_input.setPlaceholderText(tr("co4e.chat_placeholder"))
|
||||
bind_placeholder(self.chat_input, "co4e.chat_placeholder")
|
||||
# KHONG noi .submit o day: cung ly do nhu chat_toggle_btn o tren
|
||||
# (ben goi noi toi _chat_send cua chinh no).
|
||||
self.chat_send_btn = QPushButton(tr("co4e.send")); self.chat_send_btn.setIcon(icon("send"))
|
||||
self.chat_send_btn = bind_text(QPushButton(), "co4e.send")
|
||||
self.chat_send_btn.setIcon(icon("send"))
|
||||
# KHONG noi .clicked o day: cung ly do nhu tren.
|
||||
row.addWidget(self.chat_input, 1)
|
||||
# Off/Auto/Manual routing toggle for Co4E (surface key "co4e").
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import List
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import QComboBox, QFrame, QHBoxLayout, QLabel, QLineEdit, QPushButton, QScrollArea, QSizePolicy, QSpacerItem, QSplitter, QTabBar, QTabWidget, QVBoxLayout, QWidget
|
||||
from ...core import co4e
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_text, tr
|
||||
from ...theme import current_palette
|
||||
from ...ui.co4e_canvas import Co4ECanvas
|
||||
from ...ui.icons import icon
|
||||
@@ -141,7 +141,9 @@ class Co4ELayoutMixin:
|
||||
self.runs_btn.setToolTip(tr("co4e.tt_runs_tab"))
|
||||
self.runs_btn.toggled.connect(self._show_runs)
|
||||
|
||||
bar.addWidget(QLabel(tr("co4e.flow_name")))
|
||||
# Bound: nothing else holds this label, so a one-shot tr() here would
|
||||
# leave "Flow" stuck in the language the toolbar was built in.
|
||||
bar.addWidget(bind_text(QLabel(), "co4e.flow_name"))
|
||||
bar.addWidget(self.name_edit, 1)
|
||||
bar.addWidget(self.add_step_btn)
|
||||
bar.addWidget(self.save_btn)
|
||||
|
||||
@@ -39,7 +39,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_text, bind_tip
|
||||
from ...ui.icons import icon
|
||||
|
||||
|
||||
@@ -66,13 +66,15 @@ class RunsPagePanel(QWidget):
|
||||
hdr = QHBoxLayout()
|
||||
# The Runs page covers the flow toolbar, so it carries its own way back —
|
||||
# otherwise the toggle that opened it is off screen.
|
||||
self.back_btn = QPushButton(tr("co4e.back_to_flow"))
|
||||
# Bound, not set once: this panel has no retranslate hook of its own, and
|
||||
# Co4ETab (which owns the language callback) cannot reach these strings.
|
||||
self.back_btn = bind_text(QPushButton(), "co4e.back_to_flow")
|
||||
self.back_btn.setIcon(icon("chevron-left"))
|
||||
self.back_btn.setToolTip(tr("co4e.tt_back_to_flow"))
|
||||
bind_tip(self.back_btn, "co4e.tt_back_to_flow")
|
||||
# KHONG noi .clicked o day: ben goi (Co4ETab) tu quyet dinh slot nao
|
||||
# xu ly - panel chi dung widget, khong biet _show_runs la gi.
|
||||
hdr.addWidget(self.back_btn)
|
||||
self.title_label = QLabel(tr("co4e.running_flows"))
|
||||
self.title_label = bind_text(QLabel(), "co4e.running_flows")
|
||||
self.title_label.setObjectName("hint")
|
||||
hdr.addWidget(self.title_label)
|
||||
# Show + open the workspace folder where flow outputs land (below the tab,
|
||||
@@ -85,21 +87,21 @@ class RunsPagePanel(QWidget):
|
||||
# ca hai deu thuoc Co4ETab (can ctx/manager de biet duong dan that).
|
||||
hdr.addWidget(self.ws_folder_btn)
|
||||
hdr.addStretch(1)
|
||||
self.stop_btn = QPushButton(tr("co4e.stop"))
|
||||
self.stop_btn = bind_text(QPushButton(), "co4e.stop")
|
||||
self.stop_btn.setIcon(icon("stop"))
|
||||
self.stop_btn.setObjectName("danger")
|
||||
self.stop_btn.setToolTip(tr("co4e.tt_stop_run"))
|
||||
bind_tip(self.stop_btn, "co4e.tt_stop_run")
|
||||
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren.
|
||||
self.rename_btn = QPushButton(tr("co4e.rename_run"))
|
||||
self.rename_btn = bind_text(QPushButton(), "co4e.rename_run")
|
||||
self.rename_btn.setIcon(icon("edit"))
|
||||
self.rename_btn.setToolTip(tr("co4e.tt_rename_run"))
|
||||
bind_tip(self.rename_btn, "co4e.tt_rename_run")
|
||||
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren.
|
||||
self.del_btn = QPushButton(tr("co4e.delete_run"))
|
||||
self.del_btn = bind_text(QPushButton(), "co4e.delete_run")
|
||||
self.del_btn.setIcon(icon("trash"))
|
||||
self.del_btn.setToolTip(tr("co4e.tt_delete_run"))
|
||||
bind_tip(self.del_btn, "co4e.tt_delete_run")
|
||||
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren.
|
||||
self.clear_btn = QPushButton(tr("co4e.clear_done"))
|
||||
self.clear_btn.setToolTip(tr("co4e.tt_clear_runs"))
|
||||
self.clear_btn = bind_text(QPushButton(), "co4e.clear_done")
|
||||
bind_tip(self.clear_btn, "co4e.tt_clear_runs")
|
||||
# KHONG noi .clicked o day: cung ly do nhu back_btn o tren. (Ban goc
|
||||
# noi thang toi lambda: self.manager.clear_finished(), khong qua mot
|
||||
# method rieng - Co4ETab van giu dung quirk do khi noi lai signal nay.)
|
||||
@@ -113,7 +115,7 @@ class RunsPagePanel(QWidget):
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.table.setToolTip(tr("co4e.tt_runs_list"))
|
||||
bind_tip(self.table, "co4e.tt_runs_list")
|
||||
# KHONG noi .itemDoubleClicked o day: cung ly do nhu back_btn o tren.
|
||||
# Right-click a run → Open / Delete (delete a single old run from history).
|
||||
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
|
||||
@@ -11,14 +11,42 @@ from typing import List
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QListWidget, QListWidgetItem, QPushButton, QSplitter, QVBoxLayout, QWidget
|
||||
from ...core import co4e, skills as skills_mod
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_dynamic, bind_text, bind_tip, tr
|
||||
from ...ui.icons import icon
|
||||
from ...presentation.co4e.agent_list_panel import AgentListPanel
|
||||
from ...presentation.co4e.co4e_chat_view import _skill_names
|
||||
from ...presentation.co4e.palette_list import _PaletteList
|
||||
from ...presentation.co4e.skills_list_panel import SkillsListPanel
|
||||
|
||||
|
||||
def _skill_prefix_lookup(all_skills):
|
||||
"""Answer ``skills.skill_prefix_for`` from an ALREADY-LOADED skill list.
|
||||
|
||||
``skill_prefix_for`` re-reads the whole skill folder on every call, so
|
||||
asking it once per skill made a sidebar reload cost one full disk scan per
|
||||
skill — measured at ~3.8s of frozen GUI thread on a 121-skill library, and
|
||||
that reload runs on every language switch.
|
||||
|
||||
The scan order and the blank-instructions rule are copied from
|
||||
``skill_prefix_for`` deliberately: a namesake with no instructions must NOT
|
||||
end the search, or a skill's text silently becomes empty in an agent prompt.
|
||||
"""
|
||||
cache: dict = {}
|
||||
|
||||
def lookup(name: str) -> str:
|
||||
"""The ``## Skill: <name>\\n<instructions>`` block for one name, or ''."""
|
||||
if not name:
|
||||
return ""
|
||||
low = name.strip().lower()
|
||||
if low not in cache:
|
||||
cache[low] = next(
|
||||
(f"## Skill: {s.name}\n{s.instructions.strip()}" for s in all_skills
|
||||
if (s.slug == low or s.name.lower() == low) and s.instructions.strip()),
|
||||
"")
|
||||
return cache[low]
|
||||
|
||||
return lookup
|
||||
|
||||
|
||||
class Co4ESidebarMixin:
|
||||
"""Cột trái của Co4E Studio: Workflows, Agents, Skills và Flow Status."""
|
||||
def _build_sidebar(self) -> QWidget:
|
||||
@@ -66,9 +94,12 @@ class Co4ESidebarMixin:
|
||||
col = _Col(self.side_split)
|
||||
|
||||
# --- WORKFLOWS ---------------------------------------------------
|
||||
self.wf_new_btn = QPushButton(tr("co4e.new"))
|
||||
# Bound, not set once: Co4ETab._retranslate reloads the sidebar's LIST
|
||||
# CONTENTS, but these headings, buttons and tooltips are built here and
|
||||
# nothing re-applied them — they stayed in the language of app start-up.
|
||||
self.wf_new_btn = bind_text(QPushButton(), "co4e.new")
|
||||
self.wf_new_btn.setIcon(icon("plus"))
|
||||
self.wf_new_btn.setToolTip(tr("co4e.tt_new_wf"))
|
||||
bind_tip(self.wf_new_btn, "co4e.tt_new_wf")
|
||||
self.wf_new_btn.setObjectName("co4eSectionAction")
|
||||
self.wf_new_btn.setFlat(True)
|
||||
self.wf_new_btn.setCursor(Qt.PointingHandCursor)
|
||||
@@ -78,7 +109,7 @@ class Co4ESidebarMixin:
|
||||
# Draggable: drag a flow onto the canvas to merge it in (Nova-style);
|
||||
# double-click loads it onto the canvas.
|
||||
self.wf_list = _PaletteList(payload_role=Qt.UserRole + 2)
|
||||
self.wf_list.setToolTip(tr("co4e.drag_hint"))
|
||||
bind_tip(self.wf_list, "co4e.drag_hint")
|
||||
self.wf_list.itemDoubleClicked.connect(self._load_selected_workflow)
|
||||
self.wf_list.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.wf_list.customContextMenuRequested.connect(self._wf_context_menu)
|
||||
@@ -93,8 +124,9 @@ class Co4ESidebarMixin:
|
||||
wl.addLayout(wf_btns)
|
||||
# Its own row: sharing one line with the three icon buttons cut "Chạy
|
||||
# nền" down to "Chạ" as soon as the sidebar hit its narrow width.
|
||||
self.wf_runbg_btn = QPushButton(tr("co4e.run_bg")); self.wf_runbg_btn.setIcon(icon("play"))
|
||||
self.wf_runbg_btn.setToolTip(tr("co4e.tt_run_bg"))
|
||||
self.wf_runbg_btn = bind_text(QPushButton(), "co4e.run_bg")
|
||||
self.wf_runbg_btn.setIcon(icon("play"))
|
||||
bind_tip(self.wf_runbg_btn, "co4e.tt_run_bg")
|
||||
self.wf_runbg_btn.clicked.connect(self._run_selected_in_background)
|
||||
wl.addWidget(self.wf_runbg_btn)
|
||||
col.addWidget(self._section("co4e.tab_workflows", wf_body, self.wf_new_btn), 3)
|
||||
@@ -135,12 +167,12 @@ class Co4ESidebarMixin:
|
||||
self.runs_more_btn.setIcon(icon("chevron-right"))
|
||||
self.runs_more_btn.setFixedWidth(30)
|
||||
self.runs_more_btn.setFlat(True)
|
||||
self.runs_more_btn.setToolTip(tr("co4e.tt_runs_tab"))
|
||||
bind_tip(self.runs_more_btn, "co4e.tt_runs_tab")
|
||||
self.runs_more_btn.clicked.connect(lambda: self._show_runs(True))
|
||||
runs_body = QWidget(); rl = QVBoxLayout(runs_body)
|
||||
rl.setContentsMargins(0, 0, 0, 0); rl.setSpacing(4)
|
||||
self.runs_side_list = QListWidget()
|
||||
self.runs_side_list.setToolTip(tr("co4e.tt_runs_tab"))
|
||||
bind_tip(self.runs_side_list, "co4e.tt_runs_tab")
|
||||
self.runs_side_list.itemClicked.connect(self._on_side_run_clicked)
|
||||
rl.addWidget(self.runs_side_list, 1)
|
||||
col.addWidget(self._section("co4e.runs_tab", runs_body, self.runs_more_btn), 2)
|
||||
@@ -202,7 +234,10 @@ class Co4ESidebarMixin:
|
||||
v.addWidget(body, 1)
|
||||
|
||||
self._sections[key] = (head, body, stretch)
|
||||
self._sync_section_arrow(key)
|
||||
# bind_dynamic, not bind_text: the heading is the fold arrow plus the
|
||||
# translated name in caps, so re-applying it means re-running the whole
|
||||
# line rather than pushing one key into setText.
|
||||
bind_dynamic(head, lambda k=key: self._sync_section_arrow(k))
|
||||
return box
|
||||
def _fold_section(self, key: str, body: QWidget, box: QWidget, on: bool) -> None:
|
||||
"""Fold/unfold a section AND give its height back to the others.
|
||||
@@ -223,7 +258,7 @@ class Co4ESidebarMixin:
|
||||
head.setText(("▾ " if head.isChecked() else "▸ ") + tr(key).upper())
|
||||
def _icon_btn(self, icon_name: str, tip_key: str, slot) -> QPushButton:
|
||||
"""Dựng một nút icon nhỏ (rộng 34px) kèm tooltip cho hàng công cụ của mục."""
|
||||
b = QPushButton(); b.setIcon(icon(icon_name)); b.setToolTip(tr(tip_key))
|
||||
b = QPushButton(); b.setIcon(icon(icon_name)); bind_tip(b, tip_key)
|
||||
b.setFixedWidth(34)
|
||||
b.clicked.connect(slot)
|
||||
return b
|
||||
@@ -254,13 +289,16 @@ class Co4ESidebarMixin:
|
||||
co4e._step_dict(step))
|
||||
it.setData(Qt.UserRole + 1, ca.id)
|
||||
self.agent_list.addItem(it)
|
||||
# Skills
|
||||
# Skills — the library is read ONCE here and both the names and the
|
||||
# instructions come out of that one read (see _skill_prefix_lookup).
|
||||
self.skill_list.clear()
|
||||
for name in _skill_names():
|
||||
content = skills_mod.skill_prefix_for(name)
|
||||
all_skills = skills_mod.list_skills() + skills_mod.builtin_skills()
|
||||
skill_prefix = _skill_prefix_lookup(all_skills)
|
||||
for skill in all_skills:
|
||||
name = skill.name
|
||||
payload = co4e._step_dict(co4e.Step(
|
||||
label=name, agent_slug=co4e.slugify(name), role="SKILL", icon="sparkle",
|
||||
instructions=content, skills=[name]))
|
||||
instructions=skill_prefix(name), skills=[name]))
|
||||
self.skill_list.addItem(self._palette_item(name, "sparkle", payload))
|
||||
@staticmethod
|
||||
def _palette_item(text: str, icon_name: str, payload: dict) -> QListWidgetItem:
|
||||
|
||||
@@ -39,7 +39,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from ...config import PROVIDER_LABELS
|
||||
from ...core.co4e import PERMISSION_PRESETS, STEP_DONE, STEP_RUNNING, Step
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_items, bind_placeholder, bind_text, bind_tip, tr
|
||||
from ...ui.icons import icon, icon_picker_combo
|
||||
from .node_property_actions_mixin import _StepConfigActionsMixin
|
||||
from .step_config_section import _add_section
|
||||
@@ -83,26 +83,30 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
|
||||
self.label_edit = QLineEdit()
|
||||
self.label_edit.textChanged.connect(self._on_edit)
|
||||
form.addRow(tr("co4e.f_label"), self.label_edit)
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_label"), self.label_edit)
|
||||
|
||||
self.role_edit = QLineEdit()
|
||||
self.role_edit.textChanged.connect(self._on_edit)
|
||||
form.addRow(tr("co4e.f_role"), self.role_edit)
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_role"), self.role_edit)
|
||||
|
||||
# Dropdown of every icon in the registry (Monitoring's Icon Management
|
||||
# set + built-ins), each row previewing its actual glyph — still
|
||||
# editable so a not-yet-added custom name can be typed directly.
|
||||
self.icon_edit = icon_picker_combo()
|
||||
self.icon_edit.lineEdit().setPlaceholderText(tr("co4e.f_icon_placeholder"))
|
||||
# Kept on self because the combo's line edit belongs to C++: a binding
|
||||
# holds its widget weakly, so with no owner on this side the Python
|
||||
# wrapper could be collected and the binding silently dropped.
|
||||
self._icon_line = self.icon_edit.lineEdit()
|
||||
bind_placeholder(self._icon_line, "co4e.f_icon_placeholder")
|
||||
self.icon_edit.currentTextChanged.connect(self._on_edit)
|
||||
form.addRow(tr("co4e.f_icon"), self.icon_edit)
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_icon"), self.icon_edit)
|
||||
|
||||
self.instructions_edit = QPlainTextEdit()
|
||||
self.instructions_edit.setMaximumHeight(120)
|
||||
self.instructions_edit.textChanged.connect(self._on_edit)
|
||||
self.gen_btn = QPushButton(tr("co4e.ai_draft"))
|
||||
self.gen_btn = bind_text(QPushButton(), "co4e.ai_draft")
|
||||
self.gen_btn.setIcon(icon("sparkle"))
|
||||
self.gen_btn.setToolTip(tr("co4e.ai_draft_tooltip"))
|
||||
bind_tip(self.gen_btn, "co4e.ai_draft_tooltip")
|
||||
self.gen_btn.setEnabled(ctx is not None)
|
||||
self.gen_btn.clicked.connect(self._ai_draft)
|
||||
instr_box = QWidget()
|
||||
@@ -110,15 +114,15 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
ib.setContentsMargins(0, 0, 0, 0)
|
||||
ib.addWidget(self.instructions_edit)
|
||||
ib.addWidget(self.gen_btn, alignment=Qt.AlignRight)
|
||||
form.addRow(tr("co4e.f_instructions"), instr_box)
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_instructions"), instr_box)
|
||||
|
||||
# Extra context — free-text background/info fed to the step at run time
|
||||
# (in addition to instructions, attachments and upstream outputs).
|
||||
self.context_edit = QPlainTextEdit()
|
||||
self.context_edit.setMaximumHeight(90)
|
||||
self.context_edit.setPlaceholderText(tr("co4e.f_context_placeholder"))
|
||||
bind_placeholder(self.context_edit, "co4e.f_context_placeholder")
|
||||
self.context_edit.textChanged.connect(self._on_edit)
|
||||
form.addRow(tr("co4e.f_context"), self.context_edit)
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_context"), self.context_edit)
|
||||
|
||||
form2, _model_card = _add_section(outer, tr("co4e.tab_model_perm"))
|
||||
|
||||
@@ -128,28 +132,32 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
self.model_combo.editTextChanged.connect(self._on_edit)
|
||||
self.load_models_btn = QPushButton()
|
||||
self.load_models_btn.setIcon(icon("download"))
|
||||
self.load_models_btn.setToolTip(tr("co4e.load_models_tooltip"))
|
||||
bind_tip(self.load_models_btn, "co4e.load_models_tooltip")
|
||||
self.load_models_btn.clicked.connect(self._load_models)
|
||||
self.load_models_btn.setEnabled(ctx is not None)
|
||||
model_row.addWidget(self.model_combo, 1)
|
||||
model_row.addWidget(self.load_models_btn)
|
||||
mrow = QWidget(); mrow.setLayout(model_row)
|
||||
form2.addRow(tr("co4e.f_model"), mrow)
|
||||
form2.addRow(bind_text(QLabel(), "co4e.f_model"), mrow)
|
||||
|
||||
self.perm_combo = QComboBox()
|
||||
for preset in PERMISSION_PRESETS:
|
||||
self.perm_combo.addItem(tr(f"co4e.perm.{preset}"), preset)
|
||||
perm_keys = [f"co4e.perm.{preset}" for preset in PERMISSION_PRESETS]
|
||||
for preset, key in zip(PERMISSION_PRESETS, perm_keys):
|
||||
self.perm_combo.addItem(tr(key), preset)
|
||||
# Only the visible labels follow the language — the data column stays
|
||||
# the preset id that ``_on_edit`` persists onto the Step.
|
||||
bind_items(self.perm_combo, perm_keys)
|
||||
self.perm_combo.currentIndexChanged.connect(self._on_edit)
|
||||
form2.addRow(tr("co4e.f_permission"), self.perm_combo)
|
||||
form2.addRow(bind_text(QLabel(), "co4e.f_permission"), self.perm_combo)
|
||||
|
||||
verify_row = QHBoxLayout()
|
||||
self.verify_chk = QCheckBox(tr("co4e.f_self_verify"))
|
||||
self.verify_chk = bind_text(QCheckBox(), "co4e.f_self_verify")
|
||||
self.verify_chk.toggled.connect(self._on_edit)
|
||||
self.rounds_spin = QSpinBox()
|
||||
self.rounds_spin.setRange(1, 5)
|
||||
self.rounds_spin.valueChanged.connect(self._on_edit)
|
||||
verify_row.addWidget(self.verify_chk)
|
||||
verify_row.addWidget(QLabel(tr("co4e.f_verify_rounds")))
|
||||
verify_row.addWidget(bind_text(QLabel(), "co4e.f_verify_rounds"))
|
||||
verify_row.addWidget(self.rounds_spin)
|
||||
verify_row.addStretch(1)
|
||||
vrow = QWidget(); vrow.setLayout(verify_row)
|
||||
@@ -161,15 +169,15 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
self.skills_list = QListWidget()
|
||||
self.skills_list.setMaximumHeight(110)
|
||||
self.skills_list.itemChanged.connect(self._on_edit)
|
||||
form3.addRow(tr("co4e.f_skills"), self.skills_list)
|
||||
form3.addRow(bind_text(QLabel(), "co4e.f_skills"), self.skills_list)
|
||||
|
||||
# Attachments — files whose extracted text is fed to this step at run time.
|
||||
self.attach_list = QListWidget()
|
||||
self.attach_list.setMaximumHeight(80)
|
||||
self.attach_add_btn = QPushButton(tr("co4e.attach_add"))
|
||||
self.attach_add_btn = bind_text(QPushButton(), "co4e.attach_add")
|
||||
self.attach_add_btn.setIcon(icon("plus"))
|
||||
self.attach_add_btn.clicked.connect(self._add_attachment)
|
||||
self.attach_del_btn = QPushButton(tr("co4e.attach_remove"))
|
||||
self.attach_del_btn = bind_text(QPushButton(), "co4e.attach_remove")
|
||||
self.attach_del_btn.setIcon(icon("trash"))
|
||||
self.attach_del_btn.clicked.connect(self._del_attachment)
|
||||
att_btns = QHBoxLayout()
|
||||
@@ -177,7 +185,7 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
att_btns.addWidget(self.attach_del_btn)
|
||||
att_btns.addStretch(1)
|
||||
abtn = QWidget(); abtn.setLayout(att_btns)
|
||||
form3.addRow(tr("co4e.f_attachments"), self.attach_list)
|
||||
form3.addRow(bind_text(QLabel(), "co4e.f_attachments"), self.attach_list)
|
||||
form3.addRow("", abtn)
|
||||
|
||||
# Parallel sub-agents get their OWN section — same header style as
|
||||
@@ -189,10 +197,10 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
self.sub_list = QListWidget()
|
||||
self.sub_list.setMaximumHeight(90)
|
||||
self.sub_list.itemDoubleClicked.connect(self._edit_subagent) # re-pick agent
|
||||
self.sub_add_btn = QPushButton(tr("co4e.add_subagent"))
|
||||
self.sub_add_btn = bind_text(QPushButton(), "co4e.add_subagent")
|
||||
self.sub_add_btn.setIcon(icon("plus"))
|
||||
self.sub_add_btn.clicked.connect(self._add_subagent)
|
||||
self.sub_del_btn = QPushButton(tr("co4e.del_subagent"))
|
||||
self.sub_del_btn = bind_text(QPushButton(), "co4e.del_subagent")
|
||||
self.sub_del_btn.setIcon(icon("trash"))
|
||||
self.sub_del_btn.clicked.connect(self._del_subagent)
|
||||
sub_btns = QHBoxLayout()
|
||||
@@ -205,17 +213,17 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
|
||||
# Footer actions — one compact row (Run · Run from here · Delete),
|
||||
# kept below every section, not inside one of the cards.
|
||||
self.run_btn = QPushButton(tr("co4e.run"))
|
||||
self.run_btn = bind_text(QPushButton(), "co4e.run")
|
||||
self.run_btn.setIcon(icon("play"))
|
||||
self.run_btn.setToolTip(tr("co4e.run_this_step"))
|
||||
bind_tip(self.run_btn, "co4e.run_this_step")
|
||||
self.run_btn.clicked.connect(lambda: self.run_node.emit(self._node_id))
|
||||
self.run_from_btn = QPushButton(tr("co4e.run_from_here"))
|
||||
self.run_from_btn.setToolTip(tr("co4e.run_from_here"))
|
||||
self.run_from_btn = bind_text(QPushButton(), "co4e.run_from_here")
|
||||
bind_tip(self.run_from_btn, "co4e.run_from_here")
|
||||
self.run_from_btn.clicked.connect(lambda: self.run_from.emit(self._node_id))
|
||||
self.del_btn = QPushButton()
|
||||
self.del_btn.setIcon(icon("trash"))
|
||||
self.del_btn.setObjectName("danger")
|
||||
self.del_btn.setToolTip(tr("co4e.delete_step"))
|
||||
bind_tip(self.del_btn, "co4e.delete_step")
|
||||
self.del_btn.setFixedWidth(38)
|
||||
self.del_btn.clicked.connect(lambda: self.delete_node.emit(self._node_id))
|
||||
foot = QHBoxLayout()
|
||||
|
||||
@@ -29,7 +29,7 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from ...i18n import tr
|
||||
from ...i18n import bind_text, bind_tip
|
||||
from .palette_list import _PaletteList
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ class SkillsListPanel(QWidget):
|
||||
cái gì.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.manage_btn = QPushButton(tr("co4e.manage_skills"))
|
||||
self.manage_btn.setToolTip(tr("co4e.tt_manage_skills"))
|
||||
# Bound, like AgentListPanel's: the panel owns how its own button reads,
|
||||
# so no embedder has to remember it in a retranslate method.
|
||||
self.manage_btn = bind_text(QPushButton(), "co4e.manage_skills")
|
||||
bind_tip(self.manage_btn, "co4e.tt_manage_skills")
|
||||
self.manage_btn.setObjectName("co4eSectionAction")
|
||||
self.manage_btn.setFlat(True)
|
||||
self.manage_btn.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
@@ -121,6 +121,13 @@ class UsageChartWidget(QWidget):
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho nhãn và tooltip."""
|
||||
# setItemText, chứ không clear()+addItem(): cột data của hai combo này
|
||||
# là thứ quyết định kỳ và chỉ số đang xem, dựng lại danh sách sẽ reset cả
|
||||
# hai. Khoá dịch suy ra từ chính cột data nên không phải chép lại danh
|
||||
# sách giá trị ở hai nơi.
|
||||
for combo, prefix in ((self.gran_combo, "gran"), (self.metric_combo, "metric")):
|
||||
for i in range(combo.count()):
|
||||
combo.setItemText(i, tr(f"dashboard.{prefix}_{combo.itemData(i)}"))
|
||||
self.currency_lbl.setText(tr("monitoring.overview_currency"))
|
||||
self.currency_combo.setToolTip(tr("dashboard.currency_tooltip"))
|
||||
self._chart_title.setText(tr("dashboard.chart_title"))
|
||||
|
||||
@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
|
||||
QSplitter, QTableWidget, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ....i18n import tr
|
||||
from ....i18n import bind_tip, tr
|
||||
from ....ui.icons import icon
|
||||
from .event_table import PAGE_SIZE_OPTIONS, ClickOutsideCloser, EventTable
|
||||
from .event_detail_panel import EventDetailPanel
|
||||
@@ -83,7 +83,10 @@ def build_filter_scaffold(
|
||||
search.textChanged.connect(table.apply_filter)
|
||||
ai_btn = QPushButton(tr("monitoring.ai_filter_btn"))
|
||||
ai_btn.setIcon(icon("sparkle"))
|
||||
ai_btn.setToolTip(tr("monitoring.ai_filter_tooltip"))
|
||||
# Bound rather than set once: this scaffold builds the button for all
|
||||
# three event tabs, and none of their retranslate() methods can reach a
|
||||
# tooltip that was applied here.
|
||||
bind_tip(ai_btn, "monitoring.ai_filter_tooltip")
|
||||
ai_btn.setCursor(Qt.PointingHandCursor)
|
||||
if on_ai_filter is not None:
|
||||
ai_btn.clicked.connect(lambda: on_ai_filter(search, ai_btn))
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Lớp phủ "đang xử lý" ở cấp cửa sổ, dành cho tác vụ chặn GUI thread.
|
||||
|
||||
Vì sao là file riêng chứ không nhét vào ``main_window.py``: file đó chỉ còn 9
|
||||
dòng vật lý dưới trần 400 của Gate S, và một lớp phủ cấp cửa sổ là một trách
|
||||
nhiệm riêng (guardrail G6).
|
||||
|
||||
Cùng lý do ``repaint()`` với panel bận của GraphRAG — xem
|
||||
``presentation/graph/structure_graph_view.py:139-151``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from time import perf_counter
|
||||
from typing import Callable
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
# Dưới mức này người dùng chưa kịp nhận ra mình đang đợi, nên một lớp phủ toàn
|
||||
# cửa sổ chỉ kịp nháy lên rồi tắt — tự nó là một khuyết tật giao diện, không
|
||||
# phải một lời trấn an.
|
||||
_NOTICEABLE_MS = 400.0
|
||||
|
||||
|
||||
class BusyOverlay(QWidget):
|
||||
"""A window-wide "please wait" cover for work that blocks the GUI thread.
|
||||
|
||||
Deliberately NOT registered with :func:`i18n.on_language_changed`: the text
|
||||
is supplied by the caller right before the block and must stay in the
|
||||
language the rest of the screen is still showing.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
"""Build the cover hidden; it sizes itself to the parent on every show."""
|
||||
super().__init__(parent)
|
||||
self.setObjectName("busyOverlay")
|
||||
# A QWidget SUBCLASS ignores a stylesheet background without this
|
||||
# attribute; a plain QWidget instance (the panel below) does not need it.
|
||||
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
# Chưa đo được lượt nào: xem mục ``run_blocking``.
|
||||
self._last_ms: float | None = None
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.addStretch(1)
|
||||
row = QHBoxLayout()
|
||||
row.addStretch(1)
|
||||
self._panel = QWidget()
|
||||
self._panel.setObjectName("busyOverlayPanel")
|
||||
inner = QHBoxLayout(self._panel)
|
||||
inner.setContentsMargins(24, 18, 24, 18)
|
||||
self._label = QLabel()
|
||||
inner.addWidget(self._label)
|
||||
row.addWidget(self._panel)
|
||||
row.addStretch(1)
|
||||
lay.addLayout(row)
|
||||
lay.addStretch(1)
|
||||
self.hide()
|
||||
|
||||
def text(self) -> str:
|
||||
"""Chữ đang hiện trên lớp phủ (dùng cho test)."""
|
||||
return self._label.text()
|
||||
|
||||
def run_blocking(self, message: str, work: Callable[[], None]) -> None:
|
||||
"""Run ``work`` on the GUI thread, covered only when that is worth doing.
|
||||
|
||||
Nothing can time the freeze WHILE it happens: the GUI thread stops, so
|
||||
no timer fires and no watchdog can raise the cover mid-way. The only
|
||||
honest clock is the PREVIOUS run of this same call, so that is what
|
||||
decides. No measurement yet (the first switch of a process) errs
|
||||
towards showing: one flash is a smaller defect than a multi-second
|
||||
freeze with nothing on screen to explain it.
|
||||
|
||||
The result is self-calibrating. A fast machine flashes once per launch
|
||||
and then stays out of the way; a slow one, or a big skill library, gets
|
||||
the cover on every switch from the second one on.
|
||||
|
||||
``work`` is timed and its exceptions propagate — the cover still comes
|
||||
down, so a raising callback cannot leave it stuck on screen forever.
|
||||
"""
|
||||
if self._last_ms is None or self._last_ms >= _NOTICEABLE_MS:
|
||||
self.show_busy(message)
|
||||
started = perf_counter()
|
||||
try:
|
||||
work()
|
||||
finally:
|
||||
self._last_ms = (perf_counter() - started) * 1000.0
|
||||
self.hide_busy()
|
||||
|
||||
def show_busy(self, message: str) -> None:
|
||||
"""Show the cover and FORCE it onto the screen right now.
|
||||
|
||||
``repaint()``, not ``update()``: the caller is about to block the GUI
|
||||
thread, so a queued paint would only run once the freeze is over — the
|
||||
one moment the cover is no longer needed.
|
||||
|
||||
No animated progress bar on purpose: with no event loop running,
|
||||
nothing would move; only static text is guaranteed to be readable.
|
||||
"""
|
||||
self._label.setText(message)
|
||||
self.setGeometry(self.parent().rect())
|
||||
self.show()
|
||||
self.raise_()
|
||||
self.repaint()
|
||||
|
||||
def hide_busy(self) -> None:
|
||||
"""Release the cover. Call from ``finally`` so a raising callback
|
||||
cannot leave it stuck on screen forever."""
|
||||
self.hide()
|
||||
@@ -260,6 +260,12 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
|
||||
tr("app.nav.expand_tooltip") if self._nav_collapsed else tr("app.nav.collapse_tooltip"))
|
||||
if hasattr(self, "provider_lbl"):
|
||||
self.provider_lbl.setText(tr("app.provider"))
|
||||
# The label is hidden — the combo names itself through its tooltip
|
||||
# (see top_bar._build_account_row), so that is the one users read.
|
||||
self.provider_combo.setToolTip(tr("app.provider"))
|
||||
if hasattr(self, "nav_project"):
|
||||
self.nav_project.setToolTip(tr("app.nav.project_pick"))
|
||||
self.nav_recents_hdr.setText(tr("app.nav.recents"))
|
||||
if hasattr(self, "settings_btn"):
|
||||
self.settings_btn.setText(tr("app.settings"))
|
||||
if hasattr(self, "theme_btn"):
|
||||
@@ -271,6 +277,13 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
|
||||
if getattr(self, "help_agent", None) is not None:
|
||||
self.help_agent.retranslate()
|
||||
self._tray.retranslate()
|
||||
# Thanh trạng thái (góc dưới bên trái) nhận thông báo từ hàng chục nơi
|
||||
# qua signal ``status_message``, và signal đó mang CHUỖI ĐÃ DỊCH chứ
|
||||
# không mang khoá — nên không thể dịch lại câu đang hiện. Đưa nó về câu
|
||||
# nền của ngôn ngữ mới: câu cũ không đọng lại bằng thứ tiếng vừa rời đi,
|
||||
# mà chỗ đó cũng không trống trơn. Thông báo là ghi chú về một việc vừa
|
||||
# xong, nên bỏ nó đi khi đổi ngôn ngữ không làm mất thông tin nào.
|
||||
self.statusBar().showMessage(tr("app.status.ready"))
|
||||
|
||||
# ---- system tray (run in background when the window is closed) ---
|
||||
|
||||
|
||||
@@ -253,10 +253,24 @@ class NavRailMixin:
|
||||
tree.blockSignals(blocked)
|
||||
# Both destination lists are exactly as tall as their rows; the
|
||||
# stretch in between belongs to RECENTS.
|
||||
#
|
||||
# The frame, and nothing else. A flat ``+ 8`` here used to leave 6px
|
||||
# of dead space under the last row of each list, and because the
|
||||
# Settings button sits DIRECTLY under nav_bottom (nvl has no
|
||||
# spacing), that space landed between Giám sát and Settings only —
|
||||
# so three rows that read as one list were spaced 18/26px. Padding
|
||||
# a row is the item delegate's job; this is the frame's.
|
||||
row_h = 0
|
||||
for tree in (self.nav, self.nav_bottom):
|
||||
n = tree.topLevelItemCount()
|
||||
row_h = tree.sizeHintForRow(0) if n else 0
|
||||
tree.setFixedHeight(n * row_h + 8)
|
||||
row_h = tree.sizeHintForRow(0) if n else row_h
|
||||
tree.setFixedHeight(n * row_h + 2 * tree.frameWidth())
|
||||
# Settings is one more row of the same list, so it gets the rows'
|
||||
# own height rather than a second set of paddings guessed to match
|
||||
# it — the only way the three stay evenly spaced when the font (and
|
||||
# with it ``sizeHintForRow``) is not the one this was tuned on.
|
||||
if row_h and hasattr(self, "_nav_settings_btn"):
|
||||
self._nav_settings_btn.setFixedHeight(row_h)
|
||||
if keep:
|
||||
self._select_nav_row(*keep)
|
||||
finally:
|
||||
|
||||
@@ -54,7 +54,12 @@ class TopBarMixin:
|
||||
self._nav_settings_btn.setCursor(Qt.PointingHandCursor)
|
||||
self._nav_settings_btn.clicked.connect(self._open_settings)
|
||||
srow = QHBoxLayout(self._nav_settings_btn)
|
||||
srow.setContentsMargins(_NAV_ROW_INSET, 6, 8, 6)
|
||||
# No vertical padding of its own: ``_rebuild_nav`` pins this button to the
|
||||
# nav rows' OWN height, so the 6px a row pads with is already inside
|
||||
# that number. Adding it again here made the row taller than the button
|
||||
# (28 wanted, 20 given), which both clipped the icon and pushed the text
|
||||
# 8px below an even pitch with Dashboard / Giám sát.
|
||||
srow.setContentsMargins(_NAV_ROW_INSET, 0, 8, 0)
|
||||
srow.setSpacing(_NAV_ROW_GAP)
|
||||
self._nav_settings_icon = QLabel()
|
||||
self._nav_settings_icon.setPixmap(_icon("settings").pixmap(16, 16))
|
||||
@@ -63,6 +68,9 @@ class TopBarMixin:
|
||||
srow.addWidget(self._nav_settings_icon)
|
||||
srow.addWidget(self._nav_settings_text)
|
||||
srow.addStretch(1)
|
||||
# The first _rebuild_nav() ran before this button existed (it is what
|
||||
# fills the list this row belongs under), so take the height here too.
|
||||
self._nav_settings_btn.setFixedHeight(self.nav_bottom.sizeHintForRow(0))
|
||||
nvl.addWidget(self._nav_settings_btn)
|
||||
self._account_row = self._build_account_row()
|
||||
|
||||
@@ -194,6 +202,39 @@ class TopBarMixin:
|
||||
# Khong bao "dang dung <provider>" o thanh trang thai: chinh bo chon
|
||||
# provider nam ngay tren man hinh va da hien thu vua chon, nen dong thong
|
||||
# bao chi nhac lai mot thu nguoi dung vua tu tay lam.
|
||||
def _lang_busy_overlay(self):
|
||||
"""The window's busy cover, built on first use.
|
||||
|
||||
Built lazily so a window that never changes language never gets one —
|
||||
and so ``_open_settings`` can be checked for "no switch, no flash".
|
||||
"""
|
||||
overlay = getattr(self, "_lang_busy", None)
|
||||
if overlay is None:
|
||||
from .busy_overlay import BusyOverlay
|
||||
overlay = BusyOverlay(self)
|
||||
self._lang_busy = overlay
|
||||
return overlay
|
||||
|
||||
def _switch_language(self, lang: str) -> None:
|
||||
"""Apply a new UI language behind a busy cover.
|
||||
|
||||
``set_language`` runs every registered widget's re-translation on the
|
||||
GUI thread, which on a large skill library takes long enough to look
|
||||
like a hang. Nothing can raise a cover once that has started (no event
|
||||
loop is left running), so it goes up FIRST — see ``busy_overlay.py``.
|
||||
|
||||
The message is read before the switch on purpose: mid-switch the only
|
||||
language the user can still read is the one being left behind.
|
||||
"""
|
||||
message = tr("app.lang.switching")
|
||||
self.language_combo.setEnabled(False)
|
||||
try:
|
||||
self._lang_busy_overlay().run_blocking(message, lambda: set_language(lang))
|
||||
finally:
|
||||
# In a ``finally`` so a listener that raises cannot leave the
|
||||
# switcher locked for the rest of the session.
|
||||
self.language_combo.setEnabled(True)
|
||||
|
||||
def _on_language_changed(self, _idx: int) -> None:
|
||||
"""Đổi ngôn ngữ giao diện; trùng ngôn ngữ hiện tại thì bỏ qua để không dựng lại
|
||||
toàn bộ chữ vô ích.
|
||||
@@ -203,7 +244,7 @@ class TopBarMixin:
|
||||
return
|
||||
self.ctx.config.language = lang
|
||||
self.ctx.save()
|
||||
set_language(lang) # notifies every registered persistent widget
|
||||
self._switch_language(lang) # notifies every registered persistent widget
|
||||
def _open_settings(self) -> None:
|
||||
"""Mở hộp thoại Cài đặt; bấm Lưu thì áp lại theme và làm mới thanh trên."""
|
||||
dlg = SettingsDialog(self.ctx, self)
|
||||
@@ -214,7 +255,10 @@ class TopBarMixin:
|
||||
from ...ui.icons import icon as _theme_icon
|
||||
self.theme_btn.setIcon(
|
||||
_theme_icon(self._THEME_ICONS.get(self.ctx.config.theme, "monitor")))
|
||||
set_language(self.ctx.config.language) # apply if changed in Settings
|
||||
# Guarded, not left to set_language's own no-op check: the cover
|
||||
# around the switch would otherwise flash on every Save.
|
||||
if self.ctx.config.language != get_language():
|
||||
self._switch_language(self.ctx.config.language)
|
||||
# reflect provider/theme/language changes
|
||||
i = self.provider_combo.findData(self.ctx.config.active_provider)
|
||||
if i >= 0:
|
||||
|
||||
Reference in New Issue
Block a user