Section 16 of the audit page draws the three actions on the title row, a magnifier inside the search box, caps section headings, and names its complaint outright: "Ô icon trong lưới hiện tại không có viền/hover rõ khi rê chuột hay khi đang chọn" — you could not tell which cell you were about to pick. So: Thêm / Dán / Xoá move from the strip under both grids (where they read as belonging to the custom grid alone) up beside the title; the search box gets its magnifier; the two headings become caps through the same #navSectionHdr style the rail uses; and #iconGrid cells take an accent border on hover and on selection, the card language Agent and Công cụ already use. Wording follows the drawing too: "ICON TÍCH HỢP" rather than "Icon có sẵn", and "Tìm icon theo tên…" rather than "Tìm icon có sẵn…". Left alone, and worth a decision: the drawing labels the buttons "+ Thêm icon" and "Xoá", where the app says "Thêm tệp SVG" and "Xóa tùy chỉnh". The longer labels say which file type is wanted and that only custom icons can be deleted, so shortening them to match the drawing would cost more than it gains. check_icons_screen covers the four points. Its first version passed a mutation that dropped the buttons from the header layout — unparented they sit at (0,0), which read as "the same row" — so it now requires the vertical centres to line up AND each button to start right of the title. 20/20 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
"""Icons — a Monitoring sub-tab to browse the built-in icon set and add custom
|
|
icons for agents / flows.
|
|
|
|
Shows the built-in glyphs (the names usable in a Co4E step/agent ``icon`` field)
|
|
and the user's own imported SVG icons, with Add / Delete. Custom icons are saved
|
|
via ``core/custom_icons.py`` and become usable by name immediately.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtCore import QSize, Qt
|
|
from PySide6.QtWidgets import (
|
|
QHBoxLayout, QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
|
QMessageBox, QPushButton, QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from ..core import custom_icons
|
|
from ..i18n import on_language_changed, tr
|
|
from ..state import AppContext
|
|
from . import icons as icons_mod
|
|
from .icons import icon
|
|
|
|
|
|
def _grid() -> QListWidget:
|
|
g = QListWidget()
|
|
g.setObjectName("iconGrid") # accent border on hover/selection, see theme.py
|
|
g.setViewMode(QListWidget.IconMode)
|
|
g.setResizeMode(QListWidget.Adjust)
|
|
g.setMovement(QListWidget.Static)
|
|
g.setIconSize(QSize(28, 28))
|
|
g.setGridSize(QSize(96, 74))
|
|
g.setSpacing(4)
|
|
return g
|
|
|
|
|
|
class IconsAdminTab(QWidget):
|
|
def __init__(self, ctx: AppContext):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
root = QVBoxLayout(self)
|
|
# Header row: the three actions sit beside the title, where the drawing
|
|
# puts them, instead of in a strip below the two grids where they read
|
|
# as belonging to the custom grid alone.
|
|
head = QHBoxLayout()
|
|
self._title = QLabel()
|
|
self._title.setObjectName("monTitle")
|
|
head.addWidget(self._title)
|
|
head.addStretch(1)
|
|
self.add_btn = QPushButton(); self.add_btn.setIcon(icon("plus"))
|
|
self.add_btn.clicked.connect(self._add_icon)
|
|
self.paste_btn = QPushButton()
|
|
self.paste_btn.clicked.connect(self._add_from_svg_text)
|
|
self.del_btn = QPushButton(); self.del_btn.setIcon(icon("trash"))
|
|
self.del_btn.clicked.connect(self._delete_icon)
|
|
for b in (self.add_btn, self.paste_btn, self.del_btn):
|
|
head.addWidget(b)
|
|
root.addLayout(head)
|
|
|
|
self._hint = QLabel(); self._hint.setObjectName("hint"); self._hint.setWordWrap(True)
|
|
root.addWidget(self._hint)
|
|
|
|
# search over built-in names, with the magnifier the drawing asks for
|
|
self.search = QLineEdit()
|
|
self.search.addAction(icon("search"), QLineEdit.LeadingPosition)
|
|
self.search.textChanged.connect(self._reload_builtin)
|
|
root.addWidget(self.search)
|
|
|
|
self._builtin_lbl = QLabel()
|
|
self._builtin_lbl.setObjectName("navSectionHdr") # quiet caps heading
|
|
root.addWidget(self._builtin_lbl)
|
|
self.builtin_grid = _grid()
|
|
root.addWidget(self.builtin_grid, 2)
|
|
|
|
self._custom_lbl = QLabel()
|
|
self._custom_lbl.setObjectName("navSectionHdr")
|
|
root.addWidget(self._custom_lbl)
|
|
self.custom_grid = _grid()
|
|
root.addWidget(self.custom_grid, 1)
|
|
|
|
on_language_changed(self._retranslate)
|
|
self._retranslate()
|
|
|
|
# ---- rendering --------------------------------------------------------
|
|
def _reload_builtin(self, *_a) -> None:
|
|
q = self.search.text().strip().lower()
|
|
self.builtin_grid.clear()
|
|
for name in sorted(icons_mod._PATHS):
|
|
if q and q not in name:
|
|
continue
|
|
it = QListWidgetItem(icon(name), name)
|
|
it.setToolTip(name)
|
|
it.setTextAlignment(Qt.AlignHCenter | Qt.AlignBottom)
|
|
self.builtin_grid.addItem(it)
|
|
|
|
def _reload_custom(self) -> None:
|
|
self.custom_grid.clear()
|
|
for name in custom_icons.list_custom():
|
|
it = QListWidgetItem(icon(name), name)
|
|
it.setToolTip(name)
|
|
it.setData(Qt.UserRole, name)
|
|
it.setTextAlignment(Qt.AlignHCenter | Qt.AlignBottom)
|
|
self.custom_grid.addItem(it)
|
|
|
|
# ---- actions ----------------------------------------------------------
|
|
def _add_icon(self) -> None:
|
|
from PySide6.QtWidgets import QFileDialog
|
|
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"))
|
|
if not ok:
|
|
return
|
|
try:
|
|
custom_icons.add_from_file(path, name.strip())
|
|
except (OSError, ValueError) as exc:
|
|
QMessageBox.warning(self, tr("icons_admin.title"), str(exc))
|
|
return
|
|
self._reload_custom()
|
|
|
|
def _add_from_svg_text(self) -> None:
|
|
name, ok = QInputDialog.getText(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"))
|
|
if not ok:
|
|
return
|
|
try:
|
|
custom_icons.add_svg(name.strip(), svg)
|
|
except ValueError as exc:
|
|
QMessageBox.warning(self, tr("icons_admin.title"), str(exc))
|
|
return
|
|
self._reload_custom()
|
|
|
|
def _delete_icon(self) -> None:
|
|
item = self.custom_grid.currentItem()
|
|
if item is None:
|
|
QMessageBox.information(self, tr("icons_admin.title"), tr("icons_admin.select_custom"))
|
|
return
|
|
custom_icons.delete_custom(item.data(Qt.UserRole))
|
|
self._reload_custom()
|
|
|
|
def _retranslate(self) -> None:
|
|
self._title.setText(tr("monitoring.tab_icons"))
|
|
self._hint.setText(tr("icons_admin.hint"))
|
|
self.search.setPlaceholderText(tr("icons_admin.search"))
|
|
# ICON TÍCH HỢP / ICON TÙY CHỈNH — caps, like every other section
|
|
# heading the audit page draws.
|
|
self._builtin_lbl.setText(tr("icons_admin.builtin").upper())
|
|
self._custom_lbl.setText(tr("icons_admin.custom").upper())
|
|
self.add_btn.setText(tr("icons_admin.add"))
|
|
self.paste_btn.setText(tr("icons_admin.paste"))
|
|
self.del_btn.setText(tr("icons_admin.delete"))
|
|
self._reload_builtin()
|
|
self._reload_custom()
|