fix các bug theo yêu cầu https://fptsoftware362-my.sharepoint.com/❌/g/personal/nampdt_fpt_com/IQAHBJ4A9xqDTLgvt2bhukJEAdRB5LRz2hbJpTivvIiBSYM?wdExp=TEAMS-TREATMENT&web=1&isSPOFile=1&ovuser=f01e930a-b52e-42b1-b70f-a8882b5d043b%2CAnhTNM1%40fpt.com&clickparams=eyJBcHBOYW1lIjoiVGVhbXMtRGVza3RvcCIsIkFwcFZlcnNpb24iOiI0OS8yNjA4MTMxOTMxNyIsIkhhc0ZlZGVyYXRlZFVzZXIiOmZhbHNlfQ%3D%3D --------- Co-authored-by: Duy Le Huu <duylh19@fpt.com> Reviewed-on: #10 Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
This commit was merged in pull request #10.
This commit is contained in:
@@ -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()
|
||||
@@ -163,9 +163,10 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
|
||||
self._built.append(widget is not None)
|
||||
|
||||
self._build_nav_rail(right, rlay)
|
||||
# Landing stays Workspace ▸ Project, exactly as before. Go through _goto
|
||||
# so the page is actually shown — selecting the row alone only moves the
|
||||
# highlight (its signals are blocked to avoid rebuild loops).
|
||||
# Land on the Workspace screen. Go through _goto so the page is actually
|
||||
# shown — selecting the row alone only moves the highlight (its signals
|
||||
# are blocked to avoid rebuild loops). Which Workspace sub-view the user
|
||||
# ends up looking at is settled after _restore_sessions(), below.
|
||||
self._goto(self._ROW_WORKSPACE, self.workspace.current_subtab())
|
||||
self.toast = Toast(self) # top-left "task done" popup
|
||||
# Floating in-app Help assistant — a robot icon pinned bottom-right on
|
||||
@@ -176,14 +177,19 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
|
||||
self.help_agent.status_message.connect(self.statusBar().showMessage)
|
||||
|
||||
self.statusBar().showMessage(tr("app.status.ready"))
|
||||
# Author credit, pinned to the bottom-right corner. A permanent status-bar
|
||||
# widget sits at the right end and is never cleared by showMessage (which
|
||||
# writes on the left).
|
||||
self._credit = QLabel(tr("app.credit"))
|
||||
self._credit.setObjectName("faint")
|
||||
self._credit.setStyleSheet("padding: 0 10px;")
|
||||
self.statusBar().addPermanentWidget(self._credit)
|
||||
# Góc dưới phải: đúng một dòng phiên bản (cùng nguồn với tiêu đề cửa sổ).
|
||||
# Tắt size grip — nó vẽ một vệt ngay bên phải chữ; cửa sổ vẫn kéo được cạnh.
|
||||
self.statusBar().setSizeGripEnabled(False)
|
||||
self.statusBar().addPermanentWidget(QLabel(tr("app.version", v=__version__)))
|
||||
self._restore_sessions()
|
||||
# Open on "All projects…" — literally the same call the nav rail's link
|
||||
# of that name makes, so the rail highlight and the content can never
|
||||
# disagree. This runs AFTER the restore on purpose: _restore_sessions
|
||||
# still reloads the last thread, because recovering it after a crash is
|
||||
# the whole point of it, but the user should first see the list of
|
||||
# everything rather than whichever conversation happened to be open when
|
||||
# the app was last closed.
|
||||
self.goto_all_projects()
|
||||
self._tray.setup()
|
||||
# Start the task scheduler last, once the whole window exists — it
|
||||
# catches up any overdue tasks right away (first tick runs inline).
|
||||
@@ -252,9 +258,14 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin,
|
||||
self._nav_toggle_btn.setText("" if self._nav_collapsed else tr("app.nav.menu_label"))
|
||||
self._nav_toggle_btn.setToolTip(
|
||||
tr("app.nav.expand_tooltip") if self._nav_collapsed else tr("app.nav.collapse_tooltip"))
|
||||
self._credit.setText(tr("app.credit"))
|
||||
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"):
|
||||
@@ -266,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) ---
|
||||
|
||||
|
||||
@@ -163,6 +163,12 @@ class NavRailMixin:
|
||||
self.split = QSplitter(Qt.Horizontal)
|
||||
self.split.addWidget(self._nav_wrap)
|
||||
self.split.addWidget(right)
|
||||
# A QSplitter lets the user drag a pane past its own minimumWidth and
|
||||
# snap it shut at zero — the minimum only governs LAYOUT, not the drag.
|
||||
# So the rail set a 132px floor and still vanished entirely when dragged
|
||||
# hard left, with no handle left wide enough to notice, let alone grab.
|
||||
# Collapsing is what the MENU button is for, and that stops at 54px.
|
||||
self.split.setChildrenCollapsible(False)
|
||||
self.split.setStretchFactor(0, 0)
|
||||
self.split.setStretchFactor(1, 1)
|
||||
self.split.setSizes([_NAV_EXPANDED_WIDTH, 1000])
|
||||
@@ -245,12 +251,19 @@ class NavRailMixin:
|
||||
blocked = tree.blockSignals(True)
|
||||
tree.addTopLevelItem(it)
|
||||
tree.blockSignals(blocked)
|
||||
# Both destination lists are exactly as tall as their rows; the
|
||||
# stretch in between belongs to RECENTS.
|
||||
# Rows plus frame, nothing else: the flat ``+ 8`` this replaces
|
||||
# left 6px of dead space under the last row, and since Settings
|
||||
# sits directly under nav_bottom it fell between Giám sát and
|
||||
# Settings alone — 18/26px for rows that read as one list.
|
||||
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 list, so it takes the rows' own
|
||||
# height instead of paddings guessed to match it.
|
||||
if row_h and hasattr(self, "_nav_settings_btn"):
|
||||
self._nav_settings_btn.setFixedHeight(row_h)
|
||||
if keep:
|
||||
self._select_nav_row(*keep)
|
||||
finally:
|
||||
|
||||
@@ -47,6 +47,60 @@ class PageRegistryMixin:
|
||||
m.status_message.connect(self.statusBar().showMessage)
|
||||
self.monitoring = m
|
||||
return m
|
||||
def current_screen_label(self) -> str:
|
||||
"""Tên màn hình người dùng đang mở, dạng "Workspace ▸ Project".
|
||||
|
||||
Trợ lý Hỗ trợ không nhìn được màn hình, nên "tôi đang ở đâu" là câu nó
|
||||
không thể tự trả lời — chuỗi này được ghép vào prompt của nó
|
||||
(``core/help_knowledge.py``).
|
||||
"""
|
||||
row = self.pages.currentIndex()
|
||||
if not (0 <= row < len(self._nav_defs)):
|
||||
return ""
|
||||
page = tr(self._nav_defs[row][0])
|
||||
if row != self._ROW_WORKSPACE:
|
||||
return page
|
||||
sub = self.workspace.current_subtab()
|
||||
keys = {self.workspace._project_tab_idx: "workspace.tab_project",
|
||||
self.workspace._cowork_tab_idx: "workspace.tab_cowork",
|
||||
self.workspace._co4e_tab_idx: "workspace.tab_co4e",
|
||||
getattr(self.workspace, "_folder_tab_idx", -1): "workspace.tab_folder",
|
||||
self.workspace._graphrag_tab_idx: "workspace.tab_graphrag"}
|
||||
key = keys.get(sub)
|
||||
return f"{page} ▸ {tr(key)}" if key else page
|
||||
|
||||
def help_context(self) -> str:
|
||||
"""Ngữ cảnh sống cho Trợ lý Hỗ trợ: đang ở màn nào, và trên đó bấm được gì.
|
||||
|
||||
Vì sao đọc từ WIDGET ĐANG CHẠY chứ không từ ``docs/screens/controls.json``:
|
||||
file đó được trích tự động nhưng đã cũ — 5/41 file trong đó không còn tồn
|
||||
tại, và nó không có file nào trong ``presentation/`` (chưa sinh lại sau
|
||||
refactor R08). Nạp nó vào prompt là dạy trợ lý về nút của những file đã
|
||||
bị xoá. Cây widget thật thì không bao giờ cũ được.
|
||||
"""
|
||||
from PySide6.QtWidgets import QPushButton, QTabWidget
|
||||
|
||||
from ...core.help_knowledge import screen_context
|
||||
|
||||
page = self.pages.currentWidget()
|
||||
nhan = []
|
||||
if page is not None:
|
||||
for tabs in page.findChildren(QTabWidget):
|
||||
for i in range(tabs.count()):
|
||||
if tabs.isTabVisible(i):
|
||||
nhan.append(f"[tab] {tabs.tabText(i)}")
|
||||
for btn in page.findChildren(QPushButton):
|
||||
text = (btn.text() or "").replace("&", "").strip()
|
||||
if text and btn.isVisible() and btn.isEnabled():
|
||||
nhan.append(text)
|
||||
# Bo trung, giu nguyen thu tu xuat hien tren man hinh.
|
||||
thay, gon = set(), []
|
||||
for n in nhan:
|
||||
if n not in thay:
|
||||
thay.add(n)
|
||||
gon.append(n)
|
||||
return screen_context(self.current_screen_label(), gon)
|
||||
|
||||
def _ensure_page(self, row: int) -> None:
|
||||
"""Build a lazy nav page on first visit and swap it in for its placeholder."""
|
||||
if not (0 <= row < len(self._built)) or self._built[row]:
|
||||
|
||||
@@ -11,11 +11,20 @@ from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QStyledItemDelegate
|
||||
|
||||
# ---- kích thước ---------------------------------------------------------
|
||||
_NAV_EXPANDED_WIDTH = 150
|
||||
_NAV_EXPANDED_WIDTH = 232
|
||||
_NAV_COLLAPSED_WIDTH = 54
|
||||
_NAV_ROW_INSET = 8
|
||||
_NAV_ROW_GAP = 6
|
||||
_NAV_MIN_WIDTH = 132
|
||||
# Khe TRÊN nút Cài đặt, tính bằng khoảng trống thật trong layout của rail.
|
||||
# Không đặt bằng ``margin`` trong QSS: margin của stylesheet được vẽ BÊN TRONG
|
||||
# hộp của widget, mà nút này lại bị ``_rebuild_nav`` ghim đúng chiều cao một
|
||||
# dòng menu — nên margin không mua được một pixel khoảng cách nào.
|
||||
# 10 -> 4: đủ để Cài đặt không dính vào nhóm Dashboard/Giám sát, nhưng không
|
||||
# rộng đến mức trông như hai khu tách rời.
|
||||
_NAV_SETTINGS_GAP = 4
|
||||
# 132 -> 232: o 132px nhan "Cuoc tro chuyen moi" bi cat mat chu. San phai du
|
||||
# rong cho nhan DAI NHAT tren thanh, khong phai cho nhan trung binh.
|
||||
_NAV_MIN_WIDTH = 232
|
||||
_NAV_MAX_SHARE = 0.22
|
||||
_NAV_MAX_CEILING = 360
|
||||
|
||||
|
||||
@@ -114,12 +114,41 @@ class RailProjectMixin:
|
||||
tree.blockSignals(blocked)
|
||||
self.nav_recents_hdr.setVisible(not self._nav_collapsed)
|
||||
self.nav_recents.setVisible(not self._nav_collapsed)
|
||||
def goto_all_projects(self) -> None:
|
||||
"""Mở khung "Tất cả project…": Workspace ▸ Cowork, bảng lịch sử bung ra.
|
||||
|
||||
Phải đi qua ``_goto``, không được gọi thẳng ``show_history_pane()``:
|
||||
``_goto`` là chỗ duy nhất dời vệt sáng trên thanh menu theo nội dung.
|
||||
Gọi tắt thì nội dung sang Cowork còn vệt sáng ở lại Project — và vì
|
||||
``QTreeWidget`` không phát ``currentItemChanged`` khi bấm lại đúng dòng
|
||||
đang chọn, bấm "Project" sẽ KHÔNG có tác dụng gì cho tới khi người dùng
|
||||
bấm sang mục khác rồi bấm về.
|
||||
|
||||
Gom vào một hàm vì có hai đường vào: link "Tất cả project…" ở mục GẦN
|
||||
ĐÂY, và màn hình mặc định lúc mở app.
|
||||
"""
|
||||
sub = self.workspace._cowork_tab_idx
|
||||
# Chua chon project thi cong project dang giu sub-tab Cowork dong.
|
||||
# show_history_pane() mo no bang cua sau, bo qua cong — the la noi dung
|
||||
# o Cowork trong khi ca cong lan vet sang deu noi la Project. Man hinh
|
||||
# dung cho nguoi chua co project chinh la man quan ly project.
|
||||
if sub < 0 or not self.workspace.subtab_available(sub):
|
||||
self._goto(self._ROW_WORKSPACE, self.workspace._project_tab_idx)
|
||||
return
|
||||
self._goto(self._ROW_WORKSPACE, sub)
|
||||
self.workspace.show_history_pane()
|
||||
# Bang lich su duoc nhung trong sub-tab Cowork cua MOT project, nen
|
||||
# WorkspaceTab._bind_project da dat set_project_filter(pid) — no chi hien
|
||||
# thread cua project dang chon. Vao day bang link "Tat ca project…" ma van
|
||||
# con bo loc thi nguoi dung tao 5 project chi thay 1. Xoa bo loc SAU
|
||||
# show_history_pane(), vi ham do di qua duong doi sub-tab.
|
||||
self.sidebar.set_project_filter("")
|
||||
|
||||
def _on_rail_recent(self, item, _col: int = 0) -> None:
|
||||
"""Bấm một dòng trong mục GẦN ĐÂY: mở hội thoại đó, hoặc mở khung "Tất cả project…"."""
|
||||
data = item.data(0, Qt.UserRole) or {}
|
||||
if data.get("all"):
|
||||
self._goto(self._ROW_WORKSPACE, self.workspace._cowork_tab_idx)
|
||||
self.workspace.show_history_pane()
|
||||
self.goto_all_projects()
|
||||
return
|
||||
path = data.get("path")
|
||||
if path:
|
||||
|
||||
@@ -35,7 +35,7 @@ class TopBarMixin:
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton
|
||||
from ...i18n import tr
|
||||
from ...ui.icons import icon as _icon
|
||||
from .rail_metrics import _NAV_ROW_GAP, _NAV_ROW_INSET
|
||||
from .rail_metrics import _NAV_ROW_GAP, _NAV_ROW_INSET, _NAV_SETTINGS_GAP
|
||||
|
||||
# Bottom-pinned group: the places you visit occasionally, kept out of the
|
||||
# way of the ones you live in. A hairline (styled via #navrailBottom in
|
||||
@@ -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,15 @@ 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))
|
||||
# Khe TRÊN hàng Cài đặt, xin thẳng từ layout — thanh rail đặt
|
||||
# ``setSpacing(0)`` nên không có khoảng nào sẵn, và margin trong QSS thì
|
||||
# không mua được pixel nào (xem ``_NAV_SETTINGS_GAP``). Cài đặt là việc
|
||||
# khác với nhóm Dashboard/Giám sát ngay trên nó; dán sát vào thì hai thứ
|
||||
# đọc thành một khối.
|
||||
nvl.addSpacing(_NAV_SETTINGS_GAP)
|
||||
nvl.addWidget(self._nav_settings_btn)
|
||||
self._account_row = self._build_account_row()
|
||||
|
||||
@@ -191,10 +205,42 @@ class TopBarMixin:
|
||||
# Reload the Cowork tab's Agent (Model) list for the newly selected provider.
|
||||
self.cowork.refresh_agents()
|
||||
self.workspace.refresh_ai_models() # + the Folder AI-edit model picker
|
||||
self.statusBar().showMessage(
|
||||
tr("app.status.using_provider",
|
||||
label=PROVIDER_LABELS.get(self.ctx.config.active_provider))
|
||||
)
|
||||
# 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.
|
||||
@@ -204,7 +250,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)
|
||||
@@ -215,7 +261,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