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
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:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
aeecdf5da0
commit
d2101e1f87
+6
-6
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user