fix(i18n): dịch nốt chữ do Qt tự vẽ, và thêm dòng phiên bản ở góc phải
CI / test (pull_request) Canceled after 0s

Người dùng báo: chọn tiếng Nhật mà nhóm Sandbox Security, nút Save/Cancel và
nhiều chỗ khác vẫn tiếng Anh. Bộ test i18n cũ vẫn xanh vì nó chỉ bắt lỗi "có
dịch nhưng không ai áp lại" — hai lỗ thật nằm chỗ khác:

* Chuỗi HARDCODE không đi qua ``tr()`` bao giờ (``QPushButton("Unlock")``), nên
  phép đo "đổi ngôn ngữ rồi tìm chỗ không mang mốc" thấy nó đứng yên ở cả hai
  lần chụp và coi là bình thường.
* Nhãn nút do CHÍNH Qt vẽ. ``QDialogButtonBox``, ``QMessageBox.question`` và
  ``QInputDialog.get*`` lấy chữ từ bảng dịch riêng của Qt; ứng dụng không cài
  ``QTranslator`` nào và bản PySide6 đang dùng cũng không đóng gói file
  ``qtbase_*.qm`` nào để cài — nên chúng luôn rơi về tiếng Anh.

``ui/dialog_buttons.py`` gán nhãn của dự án đè lên nhãn Qt: ``dialog_buttons``
(10 hộp thoại), ``confirm`` (13 hộp Có/Không), ``ask_text``/``ask_multiline``/
``ask_item`` (16 hộp nhập liệu). Cùng với 17 chuỗi hardcode và 5 câu lỗi mà
``core/tasks.py`` trả thẳng ra hộp thoại — nay trả KHOÁ i18n, nơi hiển thị mới
gọi ``tr()`` — là 61 chỗ.

Ba chỗ nữa cùng lớp lỗi, phát hiện khi rà lại:

* ``_add_section`` nhận chuỗi ĐÃ dịch nên bốn tiêu đề mục của Step config đứng
  nguyên ở ngôn ngữ lúc dựng panel. Nay nhận khoá + ``bind_dynamic`` để không
  mất trạng thái gập/mở khi đổi ngôn ngữ.
* Thẻ tool ở Giám sát ▸ Công cụ hiện thẳng ``spec.description`` — chuỗi gửi cho
  MÔ HÌNH trong schema function-calling, phải giữ tiếng Anh. Thêm bộ mô tả hiển
  thị riêng cho 9 tool.
* Tên nhóm catalog ở tab Connector ("Other (any generic MCP server)").

Kèm theo, phần giao diện người dùng yêu cầu:

* ``__version__`` 2.26.0 -> 0.0.1, một nguồn cho tiêu đề cửa sổ, tab Giới thiệu
  và dòng mới ở góc phải thanh trạng thái (thay dòng ghi công tác giả).
* Tắt size grip: nó vẽ một vệt ngay bên phải dòng phiên bản. Cửa sổ vẫn kéo
  giãn được từ các cạnh.
* ``_NAV_SETTINGS_GAP`` 10 -> 4: hàng Cài đặt bớt xa nhóm Dashboard/Giám sát.

Hai test SẼ TREO nếu không sửa kèm: chúng patch ``QInputDialog.getText/getItem``
để tự trả lời, mà code nay gọi ``ask_text``/``ask_item`` — patch không còn chặn
được và hộp thoại thật sẽ mở ra chờ người bấm.

Ba cổng mới trong ``tests/ui/test_i18n_khong_hardcode_chu.py`` canh ở mức cấu
trúc (không ai được dựng lại kiểu cũ); đã kiểm chúng CẮN trên bản trước khi sửa:
10 + 13 + 17 vi phạm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 11:09:42 +09:00
co-authored by Claude Opus 5
parent c05de8054a
commit ab04d68a22
52 changed files with 898 additions and 190 deletions
+6 -6
View File
@@ -18,7 +18,7 @@ from typing import Dict, List, Optional
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout,
QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit, QMessageBox,
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMessageBox,
QPushButton, QSplitter, QTableWidget, QTableWidgetItem, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget,
)
@@ -28,6 +28,7 @@ from ..core import usage_tracker as ut
from ..core.worker import AgentWorker
from ..i18n import on_language_changed, tr
from ..state import AppContext
from .dialog_buttons import ask_text, confirm, dialog_buttons
from .icons import DOT_AMBER, icon
from .widgets import fmt_tokens
@@ -151,7 +152,7 @@ class AccountEditDialog(QDialog):
self.group_combo.setCurrentIndex(idx)
self.group_combo.setEnabled(allow_group_edit)
form.addRow(tr("accounts.f_group"), self.group_combo)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
form.addRow(buttons)
@@ -529,9 +530,8 @@ class AccountsTab(QWidget):
return
if not self._is_admin() and account.role != "user":
return # Sub-admin can only remove ordinary members, never another sub-admin
if QMessageBox.question(
self, tr("accounts.delete_title"),
tr("accounts.delete_confirm", username=account.username)) != QMessageBox.Yes:
if not confirm(self, tr("accounts.delete_title"),
tr("accounts.delete_confirm", username=account.username)):
return
accounts.delete_account(account.username, self._accounts_dir())
if account.group_id:
@@ -703,7 +703,7 @@ class AccountsTab(QWidget):
"""Hỏi tên rồi tạo một nhóm mới. Chỉ Admin."""
if not self._is_admin():
return
name, ok = QInputDialog.getText(self, tr("accounts.new_group_title"), tr("accounts.f_group_name"))
name, ok = ask_text(self, tr("accounts.new_group_title"), tr("accounts.f_group_name"))
if not (ok and name.strip()):
return
group = groups.new_group(name.strip())
+4 -5
View File
@@ -12,7 +12,7 @@ from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QComboBox, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget,
QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton, QScrollArea,
QListWidgetItem, QPlainTextEdit, QPushButton, QScrollArea,
QSplitter, QVBoxLayout, QWidget,
)
@@ -22,6 +22,7 @@ from ..core.custom_agents import (
)
from ..core.worker import AgentWorker
from ..i18n import tr
from .dialog_buttons import confirm
from .icons import icon
@@ -300,10 +301,8 @@ class AgentManagerTab(QWidget):
agent = self._current_agent()
if agent is None:
return
if QMessageBox.question(
self, tr("agentmgr.delete_btn"),
tr("agentmgr.delete_confirm", name=agent.name),
) != QMessageBox.Yes:
if not confirm(self, tr("agentmgr.delete_btn"),
tr("agentmgr.delete_confirm", name=agent.name)):
return
delete_agent(agent.name)
self._reload_list()
+4 -3
View File
@@ -9,7 +9,7 @@ from typing import List
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QInputDialog,
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton,
QVBoxLayout, QWidget,
)
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
from ..core.co4e import PERMISSION_PRESETS, CustomAgent
from ..core.worker import AgentWorker
from ..i18n import tr
from .dialog_buttons import ask_multiline, dialog_buttons
from .icons import icon, icon_picker_combo
@@ -114,7 +115,7 @@ class Co4EAgentDialog(QDialog):
form.addRow(tr("co4e.f_attachments"), self.attach_list)
form.addRow("", abtn)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
form.addRow(buttons)
@@ -129,7 +130,7 @@ class Co4EAgentDialog(QDialog):
role = self.role_edit.text().strip()
if not name and not role:
return
hint, ok = QInputDialog.getMultiLineText(
hint, ok = ask_multiline(
self, tr("co4e.ai_draft_hint_title"), tr("co4e.ai_draft_hint_label"))
if not ok:
return
+11 -9
View File
@@ -13,7 +13,7 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit, QMessageBox,
QDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QScrollArea, QVBoxLayout, QWidget,
)
@@ -21,6 +21,7 @@ from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES
from ..core.worker import AgentWorker
from ..i18n import bind_text, on_language_changed, tr
from ..state import AppContext
from .dialog_buttons import confirm
from .ext_connector_dialog import ExtConnectorEditDialog
from .icons import icon
from .widgets import FlowLayout, ToggleSwitch, enable_height_for_width, style_card
@@ -129,11 +130,13 @@ class ConnectorsPanel(QWidget):
Có một công tắc tổng: tắt là agent không nối ra connector ngoài nào cả.
"""
# Khoá dịch, không phải chữ sẵn: nhãn nhóm dựng lại trong _reload_connectors
# nên tr() ở đó là đủ (xem i18n/connectors.py).
_EXT_CATEGORY_LABELS = {
"cad": "CAD (NX / CATIA / SolidWorks / AutoCAD)",
"cae": "CAE (ANSA / ABAQUS / HyperWorks / ANSYS)",
"ms365": "MS365 (Microsoft 365 / OneDrive / SharePoint)",
"other": "Other (any generic MCP server)",
"cad": "connectors.cat_cad",
"cae": "connectors.cat_cae",
"ms365": "connectors.cat_ms365",
"other": "connectors.cat_other",
}
_EXT_CATEGORY_ICONS = {"cad": "wrench", "cae": "ruler", "ms365": "cloud", "other": "plug"}
# TEMPORARY: only OneDrive + SharePoint (auto-connect via locally-synced
@@ -232,7 +235,7 @@ class ConnectorsPanel(QWidget):
icon_lbl = QLabel()
icon_lbl.setPixmap(icon(self._EXT_CATEGORY_ICONS.get(cat, "plug"), size=18).pixmap(18, 18))
hdr.addWidget(icon_lbl)
name, _, subtitle = self._EXT_CATEGORY_LABELS.get(cat, cat).partition(" (")
name, _, subtitle = tr(self._EXT_CATEGORY_LABELS.get(cat, cat)).partition(" (")
name_lbl = QLabel(name)
name_lbl.setStyleSheet("font-weight:700;")
hdr.addWidget(name_lbl)
@@ -373,9 +376,8 @@ class ConnectorsPanel(QWidget):
def _delete_ext_entry(self, cat: str, entry: dict) -> None:
"""Xoá một connector MCP sau khi hỏi xác nhận."""
if QMessageBox.question(
self, tr("settings.ext_delete_btn"),
tr("settings.ext_delete_confirm", name=entry.get("name", ""))) != QMessageBox.Yes:
if not confirm(self, tr("settings.ext_delete_btn"),
tr("settings.ext_delete_confirm", name=entry.get("name", ""))):
return
self.ctx.config.ext_connectors[cat].remove(entry)
self.ctx.save()
+110
View File
@@ -0,0 +1,110 @@
"""Nút hộp thoại mang chữ của ứng dụng, không phải chữ mặc định của Qt.
``QDialogButtonBox(Save | Cancel)``, ``QMessageBox.question`` và
``QInputDialog.get*`` lấy nhãn nút từ bảng dịch của CHÍNH Qt. Ứng dụng không cài
``QTranslator`` nào — và bản PySide6 đang dùng cũng không đóng gói sẵn file
``qtbase_*.qm`` nào để cài — nên bảng đó luôn rơi về tiếng Anh: chọn tiếng Nhật
thì hộp thoại vẫn hiện "Save" / "Cancel" / "&Yes" / "&No". Đây là lỗi người dùng
báo, và nó không nằm ở dây nối i18n của dự án — ``tr()`` chưa từng được hỏi tới.
Các hàm dưới đây gán nhãn của dự án đè lên nhãn Qt. Dùng chúng thay cho việc
dựng ``QDialogButtonBox`` / gọi ``QMessageBox.question`` / ``QInputDialog.get*``
trực tiếp.
"""
from __future__ import annotations
from typing import Callable, Iterable, Tuple
from PySide6.QtWidgets import (
QDialog, QDialogButtonBox, QInputDialog, QMessageBox, QWidget,
)
from ..i18n import bind_text, tr
#: tên gọi trong ``**override`` -> (cờ nút chuẩn, khoá dịch mặc định)
_STANDARD = {
"save": (QDialogButtonBox.Save, "dialog.save"),
"cancel": (QDialogButtonBox.Cancel, "dialog.cancel"),
"ok": (QDialogButtonBox.Ok, "dialog.ok"),
"close": (QDialogButtonBox.Close, "dialog.close"),
}
def dialog_buttons(standard, **override: str) -> QDialogButtonBox:
"""``QDialogButtonBox`` có nhãn đổi theo ngôn ngữ đang chọn.
``override`` đổi khoá dịch của một nút cụ thể — ``ok="schedtask.ai_confirm"``
cho hộp thoại muốn nút OK mang chữ riêng. Phải đi qua đây chứ không
``setText(tr(...))`` sau khi dựng: lần đổi ngôn ngữ kế tiếp, ràng buộc bên
dưới sẽ áp lại khoá mặc định và xoá mất chữ riêng đó.
"""
box = QDialogButtonBox(standard)
for name, (flag, key) in _STANDARD.items():
button = box.button(flag)
if button is not None:
bind_text(button, override.get(name, key))
return box
def confirm(parent: QWidget | None, title: str, body: str) -> bool:
"""Hỏi Có/Không, trả về True nếu người dùng đồng ý.
Thay cho ``QMessageBox.question(...) == QMessageBox.Yes``, vốn hiện "&Yes" /
"&No" tiếng Anh ở mọi ngôn ngữ. Hộp thoại dựng lại mỗi lần mở nên gọi
``tr()`` một lần khi dựng là đủ, không cần ràng buộc.
"""
box = QMessageBox(QMessageBox.Question, title, body,
QMessageBox.Yes | QMessageBox.No, parent)
box.button(QMessageBox.Yes).setText(tr("dialog.yes"))
box.button(QMessageBox.No).setText(tr("dialog.no"))
return box.exec() == QMessageBox.Yes
def _hoi(parent: QWidget | None, title: str, label: str,
dung: Callable[[QInputDialog], None]) -> Tuple[str, bool]:
"""Khung chung của ba hàm hỏi bên dưới; ``dung`` chỉnh riêng từng kiểu.
Các hàm tĩnh ``QInputDialog.get*`` tự dựng hộp thoại bên trong nên không
chạm được vào nút của nó — phải tự dựng thì mới đặt được nhãn.
"""
dlg = QInputDialog(parent)
dlg.setWindowTitle(title)
dlg.setLabelText(label)
dlg.setOkButtonText(tr("dialog.ok"))
dlg.setCancelButtonText(tr("dialog.cancel"))
dung(dlg)
# exec() TRƯỚC, đọc giá trị SAU: gói cả hai vào một tuple thì Python tính
# textValue() trước và trả về chữ có sẵn lúc chưa ai gõ gì.
dong_y = dlg.exec() == QDialog.Accepted
return dlg.textValue(), dong_y
def ask_text(parent: QWidget | None, title: str, label: str,
text: str = "") -> Tuple[str, bool]:
"""Như ``QInputDialog.getText``, nhưng nút bấm theo ngôn ngữ đang chọn."""
return _hoi(parent, title, label, lambda d: d.setTextValue(text))
def ask_multiline(parent: QWidget | None, title: str, label: str,
text: str = "") -> Tuple[str, bool]:
"""Như ``QInputDialog.getMultiLineText``, nút bấm theo ngôn ngữ đang chọn."""
def dung(d: QInputDialog) -> None:
d.setOption(QInputDialog.UsePlainTextEditForTextInput)
d.setTextValue(text)
return _hoi(parent, title, label, dung)
def ask_item(parent: QWidget | None, title: str, label: str,
items: Iterable[str], current: int = 0,
editable: bool = False) -> Tuple[str, bool]:
"""Như ``QInputDialog.getItem``, nút bấm theo ngôn ngữ đang chọn."""
muc = list(items)
def dung(d: QInputDialog) -> None:
d.setComboBoxItems(muc)
d.setComboBoxEditable(editable)
if muc:
d.setTextValue(muc[current])
return _hoi(parent, title, label, dung)
+2 -1
View File
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
from ..core.ext_connectors import CATEGORIES, PRESETS
from ..i18n import tr
from .dialog_buttons import dialog_buttons
# ms365 has no user-created entries here (see ConnectorsPanel) — it auto-connects
# via its own built-in OneDrive/SharePoint toggles, so it's left off this picker.
@@ -119,7 +120,7 @@ class ExtConnectorEditDialog(QDialog):
self._enabled = bool(connector.get("enabled", False))
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._on_accept)
buttons.rejected.connect(self.reject)
lay.addWidget(buttons)
+8 -7
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import (
QHBoxLayout, QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem,
QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
QMessageBox, QPushButton, QVBoxLayout, QWidget,
)
@@ -17,6 +17,7 @@ from ..core import custom_icons
from ..i18n import on_language_changed, tr
from ..state import AppContext
from . import icons as icons_mod
from .dialog_buttons import ask_multiline, ask_text
from .icons import icon
@@ -116,8 +117,8 @@ class IconsAdminTab(QWidget):
path, _ = QFileDialog.getOpenFileName(self, tr("icons_admin.add"), "", "SVG (*.svg)")
if not path:
return
name, ok = QInputDialog.getText(self, tr("icons_admin.name_prompt"),
tr("icons_admin.name_prompt"))
name, ok = ask_text(self, tr("icons_admin.name_prompt"),
tr("icons_admin.name_prompt"))
if not ok:
return
try:
@@ -129,12 +130,12 @@ class IconsAdminTab(QWidget):
def _add_from_svg_text(self) -> None:
"""Thêm icon bằng cách dán thẳng mã SVG."""
name, ok = QInputDialog.getText(self, tr("icons_admin.name_prompt"),
tr("icons_admin.name_prompt"))
name, ok = ask_text(self, tr("icons_admin.name_prompt"),
tr("icons_admin.name_prompt"))
if not ok or not name.strip():
return
svg, ok = QInputDialog.getMultiLineText(self, tr("icons_admin.paste"),
tr("icons_admin.paste_prompt"))
svg, ok = ask_multiline(self, tr("icons_admin.paste"),
tr("icons_admin.paste_prompt"))
if not ok:
return
try:
+2 -1
View File
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import (
)
from ..i18n import tr
from .dialog_buttons import dialog_buttons
class McpServerEditDialog(QDialog):
@@ -41,7 +42,7 @@ class McpServerEditDialog(QDialog):
self.args.setPlaceholderText(tr("mcp.args_placeholder"))
lay.addWidget(self.args)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._on_accept)
buttons.rejected.connect(self.reject)
lay.addWidget(buttons)
+11 -8
View File
@@ -25,6 +25,7 @@ from PySide6.QtWidgets import (
)
from ..i18n import tr
from .dialog_buttons import dialog_buttons
from .icons import IconLabel
from .widgets import ToggleSwitch
@@ -108,18 +109,18 @@ class SettingsDialog(QDialog):
sbl = QVBoxLayout(self.sandbox_group)
# --- Password protection for Sandbox Security (at top) ---
self.sandbox_pw_label = IconLabel("lock", "Sandbox Security Password")
self.sandbox_pw_label = IconLabel("lock", tr("settings.sandbox_pw_label"))
sbl.addWidget(self.sandbox_pw_label)
pw_row = QHBoxLayout()
self.sandbox_pw_edit = QLineEdit("")
self.sandbox_pw_edit.setPlaceholderText("Enter password to edit sandbox settings")
self.sandbox_pw_edit.setPlaceholderText(tr("settings.sandbox_pw_placeholder"))
self.sandbox_pw_edit.setEchoMode(QLineEdit.Password)
pw_row.addWidget(self.sandbox_pw_edit, 1)
self.sandbox_unlock_btn = QPushButton("Unlock")
self.sandbox_unlock_btn = QPushButton(tr("settings.sandbox_unlock_btn"))
self.sandbox_unlock_btn.clicked.connect(self._sandbox_unlock)
pw_row.addWidget(self.sandbox_unlock_btn)
self.sandbox_locked_status = IconLabel("lock", "Locked (changes disabled)", color="#c00")
self.sandbox_locked_status = IconLabel("lock", tr("settings.sandbox_locked"), color="#c00")
self.sandbox_locked_status.text_label().setStyleSheet("color: #c00; font-weight: bold;")
pw_row.addWidget(self.sandbox_locked_status)
sbl.addLayout(pw_row)
@@ -229,7 +230,7 @@ class SettingsDialog(QDialog):
widest = max(w.widget().sizeHint().width() for _lab, w in pages)
self.setMinimumWidth(self.section_list.width() + widest + 60)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._save)
buttons.rejected.connect(self.reject)
outer.addWidget(buttons)
@@ -314,15 +315,17 @@ class SettingsDialog(QDialog):
return
if _sandbox_password_matches(pw, self._sandbox_pw):
self._sandbox_unlocked = True
self.sandbox_locked_status.setText("Unlocked")
self.sandbox_locked_status.setText(tr("settings.sandbox_unlocked"))
self.sandbox_locked_status.set_icon("unlock", "#090")
self.sandbox_locked_status.text_label().setStyleSheet("color: #090; font-weight: bold;")
# Enable all sandbox widgets
for w in self._sandbox_widgets:
w.setEnabled(True)
QMessageBox.information(self, "Sandbox Security", "Sandbox settings unlocked.")
QMessageBox.information(self, tr("settings.group.sandbox"),
tr("settings.sandbox_unlocked_body"))
else:
QMessageBox.warning(self, "Wrong Password", "Password incorrect. Sandbox settings remain locked.")
QMessageBox.warning(self, tr("settings.sandbox_pw_wrong_title"),
tr("settings.sandbox_pw_wrong_body"))
def _save(self) -> None:
"""Gom cấu hình từ mọi trang con rồi ghi xuống đĩa."""
+8 -9
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QColor, QPainter, QPen
from PySide6.QtWidgets import (
QAbstractItemView, QApplication, QHBoxLayout, QInputDialog, QLabel,
QLineEdit, QMenu, QMessageBox, QPushButton, QStyle, QStyledItemDelegate,
QAbstractItemView, QApplication, QHBoxLayout, QLabel,
QLineEdit, QMenu, QPushButton, QStyle, QStyledItemDelegate,
QStyleOptionViewItem, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget,
)
@@ -14,6 +14,7 @@ from ..core.history import (
rename_conversation, set_pinned,
)
from ..i18n import on_language_changed, tr
from .dialog_buttons import ask_text, confirm
from .icons import collapse_left_icon, dot_icon, DOT_BLUE, icon
from .widgets import CollapseStrip
from ..state import AppContext
@@ -346,15 +347,14 @@ class HistorySidebar(QWidget):
set_pinned(path, not pinned)
self.refresh()
elif chosen == rename_act:
new, ok = QInputDialog.getText(
new, ok = ask_text(
self, tr("sidebar.rename.title"), tr("sidebar.rename.label"), text=title)
if ok and new.strip():
rename_conversation(path, new.strip())
self.refresh()
elif chosen == del_act:
if QMessageBox.question(
self, tr("sidebar.delete.title"), tr("sidebar.delete.confirm", title=title)
) == QMessageBox.Yes:
if confirm(self, tr("sidebar.delete.title"),
tr("sidebar.delete.confirm", title=title)):
delete_conversation(path)
self.refresh()
self.history_changed.emit()
@@ -374,9 +374,8 @@ class HistorySidebar(QWidget):
"""Confirm, then delete every conversation in ``selected``. Split out
of _bulk_delete_menu so tests can drive it directly without having to
fake a real (modal, event-loop-blocking) QMenu popup."""
if QMessageBox.question(
self, tr("sidebar.delete.title"),
tr("sidebar.delete_multi.confirm", n=len(selected))) != QMessageBox.Yes:
if not confirm(self, tr("sidebar.delete.title"),
tr("sidebar.delete_multi.confirm", n=len(selected))):
return False
for item in selected:
path = item.data(0, Qt.UserRole)
+3 -2
View File
@@ -12,7 +12,7 @@ from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QFileDialog, QHBoxLayout, QInputDialog, QLabel, QListWidget,
QFileDialog, QHBoxLayout, QLabel, QListWidget,
QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout, QWidget,
)
@@ -22,6 +22,7 @@ from ..core.skills import (
)
from ..core.worker import AgentWorker
from ..i18n import tr
from .dialog_buttons import ask_multiline
from .icons import icon
from .skills_dialog import SkillEditDialog
@@ -100,7 +101,7 @@ class SkillManagerTab(QWidget):
QMessageBox.information(self, tr("skills.auto_generate_title"),
tr("skills.auto_generate_unavailable"))
return
prompt, ok = QInputDialog.getMultiLineText(
prompt, ok = ask_multiline(
self, tr("skills.auto_generate_title"),
tr("skills.auto_generate_prompt"), "")
if not ok or not prompt.strip():
+4 -3
View File
@@ -5,7 +5,7 @@ from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QInputDialog, QLabel,
QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel,
QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit,
QPushButton, QVBoxLayout,
)
@@ -17,6 +17,7 @@ from ..core.skills import (
)
from ..core.worker import AgentWorker
from ..i18n import tr
from .dialog_buttons import ask_multiline, dialog_buttons
from .icons import icon
@@ -58,7 +59,7 @@ class SkillEditDialog(QDialog):
self.instr.setMinimumHeight(180)
lay.addWidget(self.instr)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._on_accept)
buttons.rejected.connect(self.reject)
lay.addWidget(buttons)
@@ -192,7 +193,7 @@ class SkillsDialog(QDialog):
QMessageBox.information(self, tr("skills.auto_generate_title"),
tr("skills.auto_generate_unavailable"))
return
prompt, ok = QInputDialog.getMultiLineText(
prompt, ok = ask_multiline(
self, tr("skills.auto_generate_title"),
tr("skills.auto_generate_prompt"), "")
if not ok or not prompt.strip():
+9 -9
View File
@@ -17,7 +17,7 @@ from typing import Dict, List, Optional
from PySide6.QtCore import QDateTime, Qt
from PySide6.QtWidgets import (
QCheckBox, QComboBox, QDateTimeEdit, QDialog, QDialogButtonBox, QFileDialog,
QFormLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit,
QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton,
QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
)
@@ -30,6 +30,7 @@ from ..core.tasks import (
from ..core.projects import list_projects
from ..core.worker import AgentWorker
from ..i18n import tr
from .dialog_buttons import ask_text, dialog_buttons
from .icons import icon
# Only these two task types can be picked when adding/editing a task — Flow,
@@ -451,7 +452,7 @@ class TaskEditorDialog(QDialog):
# matching the five real group boxes, so you always see which group you
# are in and how many are left. (Not tabs: the audit page asks for this
# shape specifically, for consistency with Settings.)
from .widgets import section_panels
from .widgets import guard_wheel, section_panels
self._step_keys = ["schedtask.g_basic", "schedtask.g_schedule",
"schedtask.g_input", "schedtask.g_dependency",
@@ -481,7 +482,7 @@ class TaskEditorDialog(QDialog):
widest = max(w.widget().sizeHint().width() for _lab, w in pages)
self.setMinimumWidth(self.section_list.width() + widest + 60)
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
buttons.button(QDialogButtonBox.Save).setIcon(icon("save"))
buttons.button(QDialogButtonBox.Cancel).setIcon(icon("close"))
buttons.accepted.connect(self._save)
@@ -490,7 +491,6 @@ class TaskEditorDialog(QDialog):
self._apply_hints()
# Scrolling the form must never spin a combo/spin/date box the cursor
# happens to hover — values only change after clicking into a field.
from .widgets import guard_wheel
guard_wheel(self)
def _retranslate_steps(self) -> None:
@@ -540,8 +540,8 @@ class TaskEditorDialog(QDialog):
def _add_link(self) -> None:
"""Thêm một link vào danh sách dữ liệu đầu vào của task."""
url, ok = QInputDialog.getText(self, tr("schedtask.add_link_title"),
tr("schedtask.add_link_label"))
url, ok = ask_text(self, tr("schedtask.add_link_title"),
tr("schedtask.add_link_label"))
url = url.strip()
if ok and url:
self.links_list.addItem(url)
@@ -607,7 +607,7 @@ class TaskEditorDialog(QDialog):
candidates = self.all_tasks + ([self._original] if self._original else [self.task])
err = chain_error(candidates, self.task["task_id"], self.next_combo.currentData())
nxt_id = self.next_combo.currentData()
warn = err or ""
warn = tr(err) if err else ""
if not err and nxt_id:
nxt = next((t for t in self.all_tasks if t["task_id"] == nxt_id), None)
if nxt and nxt.get("status") == "paused":
@@ -719,12 +719,12 @@ class TaskEditorDialog(QDialog):
err = chain_error(self.all_tasks + [self.task], self.task["task_id"],
self.next_combo.currentData())
if err:
QMessageBox.warning(self, tr("schedtask.g_dependency"), err)
QMessageBox.warning(self, tr("schedtask.g_dependency"), tr(err))
return
deps = self._checked_depends_on()
err = depends_cycle_error(self.all_tasks + [self.task], self.task["task_id"], deps)
if err:
QMessageBox.warning(self, tr("schedtask.g_dependency"), err)
QMessageBox.warning(self, tr("schedtask.g_dependency"), tr(err))
return
t = self.task
t["title"] = title