Fix icons to work in both linux and windows via qtawesome
This commit is contained in:
parent
a2f3eb084b
commit
c781f66ae7
13 changed files with 498 additions and 145 deletions
|
|
@ -13,7 +13,6 @@ from PyQt5.QtWidgets import (
|
|||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QThread
|
||||
from PyQt5.QtGui import QIcon
|
||||
from gui.platform_glyph import platform_glyph
|
||||
from dotenv import load_dotenv, set_key, dotenv_values
|
||||
|
||||
from gui.rpgmaker_tab import RPGMakerTab
|
||||
|
|
@ -96,17 +95,18 @@ class ModelFetchThread(QThread):
|
|||
|
||||
def create_section_header(title):
|
||||
"""Create a clean section header without boxes."""
|
||||
label = QLabel(platform_glyph(title))
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
padding: 8px 0px 5px 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
return label
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
return make_section_header(
|
||||
title,
|
||||
"QLabel {"
|
||||
"font-size: 13px;"
|
||||
"font-weight: bold;"
|
||||
"color: #007acc;"
|
||||
"padding: 8px 0px 5px 0px;"
|
||||
"background-color: transparent;"
|
||||
"}",
|
||||
)
|
||||
|
||||
def create_horizontal_line():
|
||||
"""Create a horizontal separator line."""
|
||||
|
|
@ -598,11 +598,15 @@ class ConfigTab(QWidget):
|
|||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
reset_button = QPushButton(platform_glyph("🔄 Reset to Defaults"))
|
||||
from gui import qt_icons
|
||||
|
||||
reset_button = QPushButton()
|
||||
qt_icons.apply_button_icon(reset_button, "🔄 Reset to Defaults", color="#cccccc")
|
||||
reset_button.clicked.connect(self.reset_to_defaults_with_save)
|
||||
reset_button.setMinimumHeight(32)
|
||||
|
||||
save_button = QPushButton("💾 Save Changes")
|
||||
save_button = QPushButton()
|
||||
qt_icons.apply_button_icon(save_button, "💾 Save Changes", color="#cccccc")
|
||||
save_button.clicked.connect(lambda: self.save_to_env(show_message=True))
|
||||
save_button.setMinimumHeight(32)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ from PyQt5.QtWidgets import (
|
|||
QPushButton, QLabel, QMessageBox, QSpinBox, QFrame, QComboBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from gui.platform_glyph import platform_glyph
|
||||
|
||||
try:
|
||||
from .config_integration import ConfigIntegration
|
||||
except ImportError:
|
||||
|
|
@ -18,17 +16,18 @@ except ImportError:
|
|||
|
||||
def create_section_label(text):
|
||||
"""Create a section label for grouping settings."""
|
||||
label = QLabel(platform_glyph(text))
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
padding: 5px 0px 3px 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
return label
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
return make_section_header(
|
||||
text,
|
||||
"QLabel {"
|
||||
"font-size: 12px;"
|
||||
"font-weight: bold;"
|
||||
"color: #007acc;"
|
||||
"padding: 5px 0px 3px 0px;"
|
||||
"background-color: transparent;"
|
||||
"}",
|
||||
)
|
||||
|
||||
|
||||
def create_horizontal_line():
|
||||
|
|
@ -248,7 +247,10 @@ class CSVTab(QWidget):
|
|||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
self.reset_button = QPushButton(platform_glyph("🔄 Reset to Defaults"))
|
||||
from gui.qt_icons import apply_button_icon
|
||||
|
||||
self.reset_button = QPushButton()
|
||||
apply_button_icon(self.reset_button, "🔄 Reset to Defaults", color="#cccccc")
|
||||
self.reset_button.clicked.connect(self.reset_to_defaults_with_message)
|
||||
self.reset_button.setMaximumWidth(180)
|
||||
self.reset_button.setMinimumHeight(32)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from PyQt5.QtWidgets import (
|
|||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
|
||||
from PyQt5.QtGui import QTextCursor, QFont, QTextCharFormat
|
||||
from gui.platform_glyph import platform_glyph
|
||||
from pathlib import Path
|
||||
import datetime
|
||||
import html
|
||||
|
|
@ -35,16 +34,18 @@ class LogViewer(QWidget):
|
|||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Simple header to match left-side styling (use same look as create_section_header)
|
||||
header = QLabel(platform_glyph("📝 Translation Log"))
|
||||
header.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
padding: 8px 0px 5px 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
header = make_section_header(
|
||||
"📝 Translation Log",
|
||||
"QLabel {"
|
||||
"font-size: 13px;"
|
||||
"font-weight: bold;"
|
||||
"color: #007acc;"
|
||||
"padding: 8px 0px 5px 0px;"
|
||||
"background-color: transparent;"
|
||||
"}",
|
||||
)
|
||||
layout.addWidget(header, 0)
|
||||
|
||||
# Match spacing used in left column so the gap between header and
|
||||
|
|
|
|||
|
|
@ -158,9 +158,10 @@ def configure_nav_toolbutton(
|
|||
"""Apply nav icon - engine PNG when available, else emoji/glyph fallback."""
|
||||
from util.paths import ENGINE_ICONS_DIR
|
||||
|
||||
icon_dim = max(28, min(btn.width(), btn.height()) - 8)
|
||||
|
||||
engine_png = (ENGINE_ICONS_DIR / f"{engine_key}.png") if engine_key else None
|
||||
if engine_png is not None and engine_png.is_file():
|
||||
icon_dim = max(28, min(btn.width(), btn.height()) - 8)
|
||||
icon = QIcon(str(engine_png))
|
||||
btn.setIcon(icon)
|
||||
btn.setIconSize(QSize(icon_dim, icon_dim))
|
||||
|
|
@ -171,6 +172,26 @@ def configure_nav_toolbutton(
|
|||
))
|
||||
return
|
||||
|
||||
# Preferred path: render a real vector icon via qtawesome so the glyph is
|
||||
# identical on Windows and Linux (no tofu, no emoji font dependency).
|
||||
from gui import qt_icons
|
||||
|
||||
icon_name = qt_icons.icon_for_emoji(icon_text)
|
||||
if qt_icons.HAS_QTA and icon_name:
|
||||
off_color = "#ff8a80" if update_available else "#cccccc"
|
||||
on_color = "#ff5252" if update_available else "#ffffff"
|
||||
btn.setIcon(qt_icons.two_state_icon(
|
||||
icon_name, icon_dim, off_color=off_color, on_color=on_color,
|
||||
disabled_color="#666666",
|
||||
))
|
||||
btn.setIconSize(QSize(icon_dim, icon_dim))
|
||||
btn.setText("")
|
||||
btn.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
btn.setStyleSheet(_nav_toolbutton_stylesheet(
|
||||
horizontal=horizontal, icon_only=True, update_available=update_available,
|
||||
))
|
||||
return
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# Scale to the button cell (leave room for the 3px active border).
|
||||
icon_dim = max(28, min(btn.width(), btn.height()) - 8)
|
||||
|
|
|
|||
253
gui/qt_icons.py
Normal file
253
gui/qt_icons.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"""Centralized icon helpers backed by qtawesome.
|
||||
|
||||
qtawesome renders icons from bundled fonts (Material Design Icons, etc.) into
|
||||
real ``QIcon``/``QPixmap`` objects, so they look identical on Windows and Linux
|
||||
and avoid the emoji "tofu" problem on Linux font stacks.
|
||||
|
||||
Everything degrades gracefully: if qtawesome is unavailable the helpers fall
|
||||
back to the legacy ``platform_glyph`` text so the UI still renders.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import QSize, Qt
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QWidget
|
||||
|
||||
try:
|
||||
import qtawesome as qta
|
||||
|
||||
HAS_QTA = True
|
||||
except Exception: # pragma: no cover - only when the optional dep is missing
|
||||
qta = None # type: ignore[assignment]
|
||||
HAS_QTA = False
|
||||
|
||||
# Neutral light gray that matches the app's default text on dark surfaces.
|
||||
DEFAULT_COLOR = "#cccccc"
|
||||
ACCENT_COLOR = "#007acc"
|
||||
|
||||
# Map the emoji/symbols used across the GUI to Material Design Icon names.
|
||||
# Multi-character keys (e.g. "►►", "⚙️" with variation selector) are matched
|
||||
# before single characters, so order-independent longest-match lookup works.
|
||||
EMOJI_ICON: dict[str, str] = {
|
||||
"►►": "mdi6.fast-forward",
|
||||
"⚙️": "mdi6.cog",
|
||||
"🖥️": "mdi6.monitor",
|
||||
"🖼️": "mdi6.image-multiple-outline",
|
||||
"⚔️": "mdi6.sword-cross",
|
||||
"⚠️": "mdi6.alert-outline",
|
||||
"🌐": "mdi6.web",
|
||||
"⚡": "mdi6.lightning-bolt",
|
||||
"⚙": "mdi6.cog",
|
||||
"🔧": "mdi6.wrench-outline",
|
||||
"🔑": "mdi6.key-variant",
|
||||
"📝": "mdi6.text-box-outline",
|
||||
"💰": "mdi6.currency-usd",
|
||||
"🖥": "mdi6.monitor",
|
||||
"📁": "mdi6.folder-open-outline",
|
||||
"📂": "mdi6.folder-open-outline",
|
||||
"📄": "mdi6.file-document-outline",
|
||||
"🗺": "mdi6.map-outline",
|
||||
"❓": "mdi6.help-circle-outline",
|
||||
"💬": "mdi6.message-text-outline",
|
||||
"🖼": "mdi6.image-multiple-outline",
|
||||
"🎮": "mdi6.gamepad-variant-outline",
|
||||
"📚": "mdi6.bookshelf",
|
||||
"👤": "mdi6.account",
|
||||
"📋": "mdi6.clipboard-text-outline",
|
||||
"📊": "mdi6.table-column",
|
||||
"📤": "mdi6.export",
|
||||
"📥": "mdi6.import",
|
||||
"🔄": "mdi6.refresh",
|
||||
"↺": "mdi6.refresh",
|
||||
"↻": "mdi6.refresh",
|
||||
"🔍": "mdi6.magnify",
|
||||
"💾": "mdi6.content-save",
|
||||
"🗑️": "mdi6.delete-outline",
|
||||
"🗑": "mdi6.delete-outline",
|
||||
"➕": "mdi6.plus",
|
||||
"🛑": "mdi6.stop-circle-outline",
|
||||
"⚔": "mdi6.sword-cross",
|
||||
"⬇": "mdi6.download",
|
||||
"⬆": "mdi6.upload",
|
||||
"↓": "mdi6.import",
|
||||
"↑": "mdi6.export",
|
||||
"►": "mdi6.play",
|
||||
"▶": "mdi6.play",
|
||||
"◆": "mdi6.diamond-stone",
|
||||
"☑": "mdi6.checkbox-marked-outline",
|
||||
"☐": "mdi6.checkbox-blank-outline",
|
||||
"▼": "mdi6.chevron-down",
|
||||
"◄": "mdi6.chevron-left",
|
||||
"←": "mdi6.arrow-left",
|
||||
"→": "mdi6.arrow-right",
|
||||
"✓": "mdi6.check-bold",
|
||||
"✔": "mdi6.check-bold",
|
||||
"✅": "mdi6.check-circle-outline",
|
||||
"✗": "mdi6.close-thick",
|
||||
"✕": "mdi6.close-thick",
|
||||
"❌": "mdi6.close-circle-outline",
|
||||
"⚠": "mdi6.alert-outline",
|
||||
}
|
||||
|
||||
# Leading symbol/emoji cluster at the start of a button/header label.
|
||||
_LEADING_SYMBOLS = re.compile(
|
||||
r"^[\s]*("
|
||||
r"[\U0001F000-\U0001FAFF\U00002600-\U000027BF\U00002190-\U000021FF"
|
||||
r"\U00002300-\U000023FF\U000025A0-\U000025FF\U00002B00-\U00002BFF"
|
||||
r"\uFE0F\u2028\u2029]+"
|
||||
r")\s*"
|
||||
)
|
||||
|
||||
|
||||
# File-list categories -> (icon name, tint color).
|
||||
_FILE_CATEGORY_ICONS: dict[str, tuple[str, str]] = {
|
||||
"core": ("mdi6.file-document-outline", "#9cdcfe"),
|
||||
"map": ("mdi6.map-outline", "#c5c5c0"),
|
||||
"other": ("mdi6.help-circle-outline", "#888888"),
|
||||
}
|
||||
|
||||
|
||||
def file_category_icon(category: str) -> QIcon:
|
||||
"""Return the list icon for a file category (``core`` / ``map`` / other)."""
|
||||
name, color = _FILE_CATEGORY_ICONS.get(category, _FILE_CATEGORY_ICONS["other"])
|
||||
return get_icon(name, color=color)
|
||||
|
||||
|
||||
def get_icon(name: str, color: str = DEFAULT_COLOR, **kwargs) -> QIcon:
|
||||
"""Return a qtawesome ``QIcon`` (empty ``QIcon`` if unavailable)."""
|
||||
if not HAS_QTA:
|
||||
return QIcon()
|
||||
try:
|
||||
return qta.icon(name, color=color, **kwargs)
|
||||
except Exception:
|
||||
return QIcon()
|
||||
|
||||
|
||||
def get_pixmap(name: str, size: int, color: str = DEFAULT_COLOR) -> QPixmap:
|
||||
"""Render a qtawesome icon to a square ``QPixmap`` (empty if unavailable)."""
|
||||
if not HAS_QTA:
|
||||
return QPixmap()
|
||||
try:
|
||||
return qta.icon(name, color=color).pixmap(QSize(size, size))
|
||||
except Exception:
|
||||
return QPixmap()
|
||||
|
||||
|
||||
def two_state_icon(
|
||||
name: str,
|
||||
size: int,
|
||||
*,
|
||||
off_color: str,
|
||||
on_color: str,
|
||||
disabled_color: str | None = None,
|
||||
) -> QIcon:
|
||||
"""Build a nav ``QIcon`` whose On (checked) pixmap uses a brighter color."""
|
||||
icon = QIcon()
|
||||
if not HAS_QTA:
|
||||
return icon
|
||||
off_pix = get_pixmap(name, size, off_color)
|
||||
on_pix = get_pixmap(name, size, on_color)
|
||||
dis_pix = get_pixmap(name, size, disabled_color or off_color)
|
||||
icon.addPixmap(off_pix, QIcon.Normal, QIcon.Off)
|
||||
icon.addPixmap(on_pix, QIcon.Active, QIcon.Off)
|
||||
icon.addPixmap(on_pix, QIcon.Normal, QIcon.On)
|
||||
icon.addPixmap(on_pix, QIcon.Active, QIcon.On)
|
||||
icon.addPixmap(on_pix, QIcon.Selected, QIcon.On)
|
||||
icon.addPixmap(dis_pix, QIcon.Disabled, QIcon.Off)
|
||||
return icon
|
||||
|
||||
|
||||
def split_leading_symbol(text: str) -> tuple[str | None, str]:
|
||||
"""Split a leading emoji/symbol cluster from *text*.
|
||||
|
||||
Returns ``(icon_name_or_None, remaining_text)``. The icon name is resolved
|
||||
from :data:`EMOJI_ICON` using a longest-match on the leading cluster, then a
|
||||
first-character fallback.
|
||||
"""
|
||||
match = _LEADING_SYMBOLS.match(text)
|
||||
if not match:
|
||||
return None, text
|
||||
cluster = match.group(1).strip()
|
||||
rest = text[match.end():].strip()
|
||||
|
||||
if cluster in EMOJI_ICON:
|
||||
return EMOJI_ICON[cluster], rest
|
||||
# Try progressively shorter prefixes, then the first character.
|
||||
for length in range(len(cluster), 0, -1):
|
||||
prefix = cluster[:length]
|
||||
if prefix in EMOJI_ICON:
|
||||
return EMOJI_ICON[prefix], rest
|
||||
if cluster and cluster[0] in EMOJI_ICON:
|
||||
return EMOJI_ICON[cluster[0]], rest
|
||||
return None, rest
|
||||
|
||||
|
||||
def icon_for_emoji(emoji: str) -> str | None:
|
||||
"""Return the MDI icon name mapped to *emoji*, or ``None``."""
|
||||
name, _ = split_leading_symbol(emoji)
|
||||
return name
|
||||
|
||||
|
||||
def apply_button_icon(btn, raw_text: str, color: str = DEFAULT_COLOR) -> None:
|
||||
"""Set a labeled button's icon from its leading emoji and strip the emoji.
|
||||
|
||||
Falls back to the legacy ``platform_glyph`` text when qtawesome is missing or
|
||||
no icon mapping exists.
|
||||
"""
|
||||
from gui.platform_glyph import platform_glyph
|
||||
|
||||
icon_name, rest = split_leading_symbol(raw_text)
|
||||
if HAS_QTA and icon_name and rest:
|
||||
btn.setIcon(get_icon(icon_name, color=color))
|
||||
btn.setText(rest)
|
||||
return
|
||||
if HAS_QTA and icon_name and not rest:
|
||||
# Icon-only button.
|
||||
btn.setIcon(get_icon(icon_name, color=color))
|
||||
btn.setText("")
|
||||
return
|
||||
btn.setText(platform_glyph(raw_text))
|
||||
|
||||
|
||||
def make_section_header(
|
||||
title: str,
|
||||
text_stylesheet: str,
|
||||
*,
|
||||
icon_color: str = ACCENT_COLOR,
|
||||
icon_px: int = 15,
|
||||
) -> QWidget:
|
||||
"""Build a section header row: a qtawesome icon (from the leading emoji) plus
|
||||
the styled title text.
|
||||
|
||||
When qtawesome is unavailable, falls back to a plain ``QLabel`` using the
|
||||
legacy ``platform_glyph`` text so behavior is unchanged.
|
||||
"""
|
||||
from gui.platform_glyph import platform_glyph
|
||||
|
||||
icon_name, rest = split_leading_symbol(title)
|
||||
|
||||
if not (HAS_QTA and icon_name):
|
||||
label = QLabel(platform_glyph(title))
|
||||
label.setStyleSheet(text_stylesheet)
|
||||
return label
|
||||
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(6)
|
||||
|
||||
icon_label = QLabel()
|
||||
icon_label.setPixmap(get_pixmap(icon_name, icon_px, icon_color))
|
||||
icon_label.setStyleSheet("background: transparent; padding: 0;")
|
||||
icon_label.setAlignment(Qt.AlignVCenter)
|
||||
layout.addWidget(icon_label, 0, Qt.AlignVCenter)
|
||||
|
||||
text_label = QLabel(rest)
|
||||
text_label.setStyleSheet(text_stylesheet)
|
||||
layout.addWidget(text_label, 0, Qt.AlignVCenter)
|
||||
layout.addStretch()
|
||||
|
||||
return container
|
||||
|
|
@ -10,8 +10,6 @@ from PyQt5.QtWidgets import (
|
|||
QTextEdit, QSpinBox, QFrame, QGridLayout
|
||||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from gui.platform_glyph import platform_glyph
|
||||
|
||||
try:
|
||||
from .config_integration import ConfigIntegration
|
||||
except ImportError:
|
||||
|
|
@ -26,17 +24,18 @@ except Exception:
|
|||
|
||||
def create_section_label(text):
|
||||
"""Create a section label for grouping settings."""
|
||||
label = QLabel(platform_glyph(text))
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
padding: 6px 0px 4px 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
return label
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
return make_section_header(
|
||||
text,
|
||||
"QLabel {"
|
||||
"font-size: 13px;"
|
||||
"font-weight: bold;"
|
||||
"color: #007acc;"
|
||||
"padding: 6px 0px 4px 0px;"
|
||||
"background-color: transparent;"
|
||||
"}",
|
||||
)
|
||||
|
||||
|
||||
class RPGMakerTab(QWidget):
|
||||
|
|
@ -456,7 +455,10 @@ class RPGMakerTab(QWidget):
|
|||
|
||||
# Reset button
|
||||
button_layout = QHBoxLayout()
|
||||
self.reset_button = QPushButton(platform_glyph("🔄 Reset to Defaults"))
|
||||
from gui.qt_icons import apply_button_icon
|
||||
|
||||
self.reset_button = QPushButton()
|
||||
apply_button_icon(self.reset_button, "🔄 Reset to Defaults", color="#cccccc")
|
||||
self.reset_button.clicked.connect(self.reset_to_defaults_with_message)
|
||||
self.reset_button.setMinimumHeight(32)
|
||||
self.reset_button.setMaximumWidth(160)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ from PyQt5.QtWidgets import (
|
|||
)
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
import re
|
||||
from gui.platform_glyph import platform_glyph
|
||||
|
||||
try:
|
||||
from util.defaults import DEFAULTS as CANONICAL_DEFAULTS
|
||||
except Exception:
|
||||
|
|
@ -19,9 +17,12 @@ except Exception:
|
|||
|
||||
def create_section_label(text):
|
||||
"""Create a styled section header label."""
|
||||
label = QLabel(platform_glyph(text))
|
||||
label.setStyleSheet("font-size: 12px; font-weight: bold; color: #007acc; padding: 2px 0px;")
|
||||
return label
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
return make_section_header(
|
||||
text,
|
||||
"font-size: 12px; font-weight: bold; color: #007acc; padding: 2px 0px;",
|
||||
)
|
||||
|
||||
|
||||
class SRPGTab(QWidget):
|
||||
|
|
@ -83,7 +84,10 @@ class SRPGTab(QWidget):
|
|||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
self.reset_btn = QPushButton(platform_glyph("🔄 Reset to Defaults"))
|
||||
from gui.qt_icons import apply_button_icon
|
||||
|
||||
self.reset_btn = QPushButton()
|
||||
apply_button_icon(self.reset_btn, "🔄 Reset to Defaults", color="#cccccc")
|
||||
self.reset_btn.clicked.connect(self.reset_to_defaults_with_message)
|
||||
self.reset_btn.setMaximumWidth(180)
|
||||
self.reset_btn.setMinimumHeight(32)
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ from PyQt5.QtWidgets import (
|
|||
QSplitter, QFileDialog, QComboBox, QCheckBox, QProgressBar, QFrame, QFormLayout, QStackedWidget
|
||||
)
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess, QEvent, QRect, QSettings
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess, QEvent, QRect, QSettings, QSize
|
||||
from PyQt5.QtGui import QFont
|
||||
from gui.log_viewer import LogViewer
|
||||
from gui.platform_glyph import platform_glyph
|
||||
from gui import qt_icons
|
||||
|
||||
|
||||
def _strip_ansi(text):
|
||||
|
|
@ -42,17 +42,16 @@ def _strip_ansi(text):
|
|||
|
||||
def create_section_header(title):
|
||||
"""Create a clean section header without boxes."""
|
||||
label = QLabel(platform_glyph(title))
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
padding: 8px 0px 5px 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
return label
|
||||
return qt_icons.make_section_header(
|
||||
title,
|
||||
"QLabel {"
|
||||
"font-size: 13px;"
|
||||
"font-weight: bold;"
|
||||
"color: #007acc;"
|
||||
"padding: 8px 0px 5px 0px;"
|
||||
"background-color: transparent;"
|
||||
"}",
|
||||
)
|
||||
|
||||
def create_horizontal_line():
|
||||
"""Create a horizontal separator line."""
|
||||
|
|
@ -844,53 +843,31 @@ class TranslationTab(QWidget):
|
|||
}
|
||||
"""
|
||||
|
||||
select_all_btn = QPushButton("✓")
|
||||
select_all_btn.setToolTip("Select all files")
|
||||
select_all_btn.clicked.connect(self.select_all_files)
|
||||
select_all_btn.setStyleSheet(first_button_style)
|
||||
file_buttons.addWidget(select_all_btn)
|
||||
|
||||
deselect_all_btn = QPushButton("✗")
|
||||
deselect_all_btn.setToolTip("Deselect all files")
|
||||
deselect_all_btn.clicked.connect(self.deselect_all_files)
|
||||
deselect_all_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(deselect_all_btn)
|
||||
|
||||
add_files_btn = QPushButton(platform_glyph("➕"))
|
||||
add_files_btn.setToolTip("Add files to translate")
|
||||
add_files_btn.clicked.connect(self.add_input_files)
|
||||
add_files_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(add_files_btn)
|
||||
|
||||
remove_files_btn = QPushButton("🗑️")
|
||||
remove_files_btn.setToolTip("Remove selected files")
|
||||
remove_files_btn.clicked.connect(self.remove_selected_files)
|
||||
remove_files_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(remove_files_btn)
|
||||
|
||||
open_folder_btn = QPushButton(platform_glyph("📁"))
|
||||
open_folder_btn.setToolTip("Open files folder in explorer")
|
||||
open_folder_btn.clicked.connect(self.open_input_folder)
|
||||
open_folder_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(open_folder_btn)
|
||||
|
||||
refresh_btn = QPushButton(platform_glyph("🔄"))
|
||||
refresh_btn.setToolTip("Refresh file list")
|
||||
refresh_btn.clicked.connect(self.refresh_file_lists)
|
||||
refresh_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(refresh_btn)
|
||||
_icon_size = QSize(18, 18)
|
||||
|
||||
self.sidebar_export_btn = QPushButton("📤")
|
||||
self.sidebar_export_btn.setToolTip("Export selected files → Game Folder\nCopy translated files for the checked items into your game's data directory")
|
||||
self.sidebar_export_btn.clicked.connect(self._export_selected_files)
|
||||
self.sidebar_export_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(self.sidebar_export_btn)
|
||||
def _icon_button(glyph, tooltip, slot, style):
|
||||
btn = QPushButton()
|
||||
qt_icons.apply_button_icon(btn, glyph, color="#dddddd")
|
||||
btn.setIconSize(_icon_size)
|
||||
btn.setToolTip(tooltip)
|
||||
btn.clicked.connect(slot)
|
||||
btn.setStyleSheet(style)
|
||||
file_buttons.addWidget(btn)
|
||||
return btn
|
||||
|
||||
pricing_test_btn = QPushButton("💰")
|
||||
pricing_test_btn.setToolTip("Check live pricing for the current model")
|
||||
pricing_test_btn.clicked.connect(self._check_model_pricing)
|
||||
pricing_test_btn.setStyleSheet(icon_button_style)
|
||||
file_buttons.addWidget(pricing_test_btn)
|
||||
select_all_btn = _icon_button("✓", "Select all files", self.select_all_files, first_button_style)
|
||||
deselect_all_btn = _icon_button("✗", "Deselect all files", self.deselect_all_files, icon_button_style)
|
||||
add_files_btn = _icon_button("➕", "Add files to translate", self.add_input_files, icon_button_style)
|
||||
remove_files_btn = _icon_button("🗑️", "Remove selected files", self.remove_selected_files, icon_button_style)
|
||||
open_folder_btn = _icon_button("📁", "Open files folder in explorer", self.open_input_folder, icon_button_style)
|
||||
refresh_btn = _icon_button("🔄", "Refresh file list", self.refresh_file_lists, icon_button_style)
|
||||
self.sidebar_export_btn = _icon_button(
|
||||
"📤",
|
||||
"Export selected files → Game Folder\nCopy translated files for the checked items into your game's data directory",
|
||||
self._export_selected_files,
|
||||
icon_button_style,
|
||||
)
|
||||
pricing_test_btn = _icon_button("💰", "Check live pricing for the current model", self._check_model_pricing, icon_button_style)
|
||||
|
||||
# Add stretch to push buttons to top
|
||||
file_buttons.addStretch()
|
||||
|
|
@ -1122,22 +1099,26 @@ class TranslationTab(QWidget):
|
|||
|
||||
# Summary button (shown after completion) - icon-only
|
||||
# Use a simple left-arrow for the back action and place it on the left
|
||||
self.reset_view_button = QPushButton("←")
|
||||
self.reset_view_button = QPushButton()
|
||||
qt_icons.apply_button_icon(self.reset_view_button, "←", color="#dddddd")
|
||||
self.reset_view_button.setToolTip("Back to File Selection")
|
||||
self.reset_view_button.clicked.connect(self.reset_to_file_view)
|
||||
self.reset_view_button.setVisible(False)
|
||||
# Button to open the translations (translated) folder - icon-only
|
||||
self.open_translations_button = QPushButton(platform_glyph("📂"))
|
||||
self.open_translations_button = QPushButton()
|
||||
qt_icons.apply_button_icon(self.open_translations_button, "📂", color="#dddddd")
|
||||
self.open_translations_button.setToolTip("Open the translated files folder")
|
||||
self.open_translations_button.clicked.connect(self.open_output_folder)
|
||||
self.open_translations_button.setVisible(False)
|
||||
# Sync translated/ → files/ (RPG Maker only)
|
||||
self.sync_translated_button = QPushButton(platform_glyph("🔄"))
|
||||
self.sync_translated_button = QPushButton()
|
||||
qt_icons.apply_button_icon(self.sync_translated_button, "🔄", color="#dddddd")
|
||||
self.sync_translated_button.setToolTip("Sync translated/ → files/\nCopy translated files back into files/ so the next phase starts from the latest state")
|
||||
self.sync_translated_button.clicked.connect(self._sync_translated_to_files)
|
||||
self.sync_translated_button.setVisible(False)
|
||||
# Export active files → game folder (RPG Maker only)
|
||||
self.export_active_button = QPushButton("📤")
|
||||
self.export_active_button = QPushButton()
|
||||
qt_icons.apply_button_icon(self.export_active_button, "📤", color="#dddddd")
|
||||
self.export_active_button.setToolTip("Export translated files → Game Folder\nCopy the files from this translation run into your game's data directory")
|
||||
self.export_active_button.clicked.connect(self._export_last_run_files)
|
||||
self.export_active_button.setVisible(False)
|
||||
|
|
@ -1169,11 +1150,10 @@ class TranslationTab(QWidget):
|
|||
self.open_translations_button.setStyleSheet(icon_btn_style)
|
||||
self.sync_translated_button.setStyleSheet(icon_btn_style)
|
||||
self.export_active_button.setStyleSheet(icon_btn_style)
|
||||
# Ensure emoji/icons are readable
|
||||
self.reset_view_button.setFont(QFont('Segoe UI', 12))
|
||||
self.open_translations_button.setFont(QFont('Segoe UI', 12))
|
||||
self.sync_translated_button.setFont(QFont('Segoe UI', 12))
|
||||
self.export_active_button.setFont(QFont('Segoe UI', 12))
|
||||
# Size the icons to fill the compact buttons.
|
||||
for _b in (self.reset_view_button, self.open_translations_button,
|
||||
self.sync_translated_button, self.export_active_button):
|
||||
_b.setIconSize(QSize(20, 20))
|
||||
|
||||
# Create the stop button here so it sits in the same row as the
|
||||
# back/open buttons. Use a compact icon style to match them but
|
||||
|
|
@ -1202,12 +1182,12 @@ class TranslationTab(QWidget):
|
|||
|
||||
# Use a clear stop-sign emoji so the glyph is rendered as a stop icon
|
||||
# and not as a colored square on some platforms.
|
||||
self.stop_button = QPushButton(platform_glyph("🛑"))
|
||||
self.stop_button = QPushButton()
|
||||
qt_icons.apply_button_icon(self.stop_button, "🛑", color="#ffffff")
|
||||
self.stop_button.setToolTip("Stop Translation")
|
||||
self.stop_button.clicked.connect(self.stop_translation)
|
||||
self.stop_button.setStyleSheet(stop_button_style)
|
||||
# Slightly larger font for the emoji to make it visually clear
|
||||
self.stop_button.setFont(QFont('Segoe UI', 14))
|
||||
self.stop_button.setIconSize(QSize(20, 20))
|
||||
self.stop_button.setVisible(False)
|
||||
|
||||
# Place both buttons on the left and totals on the right
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from PyQt5.QtWidgets import (
|
|||
)
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
import re
|
||||
from gui.platform_glyph import platform_glyph
|
||||
try:
|
||||
from util.defaults import DEFAULTS as CANONICAL_DEFAULTS
|
||||
except Exception:
|
||||
|
|
@ -17,9 +16,12 @@ except Exception:
|
|||
|
||||
def create_section_label(text):
|
||||
"""Create a styled section header label."""
|
||||
label = QLabel(platform_glyph(text))
|
||||
label.setStyleSheet("font-size: 12px; font-weight: bold; color: #007acc; padding: 2px 0px;")
|
||||
return label
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
return make_section_header(
|
||||
text,
|
||||
"font-size: 12px; font-weight: bold; color: #007acc; padding: 2px 0px;",
|
||||
)
|
||||
|
||||
class WolfTab(QWidget):
|
||||
config_changed = pyqtSignal()
|
||||
|
|
@ -184,7 +186,10 @@ class WolfTab(QWidget):
|
|||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
self.reset_btn = QPushButton(platform_glyph("🔄 Reset to Defaults"))
|
||||
from gui.qt_icons import apply_button_icon
|
||||
|
||||
self.reset_btn = QPushButton()
|
||||
apply_button_icon(self.reset_btn, "🔄 Reset to Defaults", color="#cccccc")
|
||||
self.reset_btn.clicked.connect(self.reset_to_defaults_with_message)
|
||||
self.reset_btn.setMaximumWidth(180)
|
||||
self.reset_btn.setMinimumHeight(32)
|
||||
|
|
|
|||
|
|
@ -1134,10 +1134,12 @@ class WolfWorkflowTab(QWidget):
|
|||
items = list_wolf_json_files(work_dir, manifest)
|
||||
self._file_items = items
|
||||
self.file_list.clear()
|
||||
from gui.qt_icons import file_category_icon
|
||||
|
||||
for item in items:
|
||||
cat = item["category"]
|
||||
icon = "📄" if cat == "core" else ("🗺" if cat == "map" else "❓")
|
||||
lw = QListWidgetItem(f"{icon} {item['name']} ({item['size_kb']:.1f} KB)")
|
||||
lw = QListWidgetItem(f"{item['name']} ({item['size_kb']:.1f} KB)")
|
||||
lw.setIcon(file_category_icon(cat))
|
||||
lw.setData(Qt.UserRole, item)
|
||||
lw.setFlags(lw.flags() | Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable)
|
||||
lw.setCheckState(Qt.Checked if item["default"] else Qt.Unchecked)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ from util.vocab import read_game_vocab, write_game_vocab
|
|||
|
||||
import jsbeautifier
|
||||
|
||||
from PyQt5.QtCore import Qt, QEvent, QSettings, QThread, QTimer, pyqtSignal
|
||||
from PyQt5.QtCore import Qt, QEvent, QSettings, QSize, QThread, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
|
|
@ -398,7 +398,7 @@ def _make_btn(text: str, color: str = "#007acc") -> QPushButton:
|
|||
Dark utility colours (max channel < 115) use the flat sidebar style.
|
||||
Action colours use flat dark bg + coloured outline + coloured text.
|
||||
"""
|
||||
btn = QPushButton(text)
|
||||
btn = QPushButton()
|
||||
try:
|
||||
c = color.lstrip("#")
|
||||
if len(c) == 3:
|
||||
|
|
@ -409,6 +409,7 @@ def _make_btn(text: str, color: str = "#007acc") -> QPushButton:
|
|||
r = g = b = 0
|
||||
is_flat = False
|
||||
_PAD = "padding:6px 14px;"
|
||||
_icon_color = "#cccccc"
|
||||
if is_flat:
|
||||
btn.setStyleSheet(
|
||||
f"QPushButton{{background-color:#2d2d30;color:#cccccc;"
|
||||
|
|
@ -425,6 +426,7 @@ def _make_btn(text: str, color: str = "#007acc") -> QPushButton:
|
|||
gt = min(255, g + 80)
|
||||
bt = min(255, b + 80)
|
||||
text_color = f"#{rt:02x}{gt:02x}{bt:02x}"
|
||||
_icon_color = text_color
|
||||
base = 0x2d
|
||||
rh = min(255, int(base + (r - base) * 0.18))
|
||||
gh = min(255, int(base + (g - base) * 0.18))
|
||||
|
|
@ -443,6 +445,11 @@ def _make_btn(text: str, color: str = "#007acc") -> QPushButton:
|
|||
f"QPushButton:pressed{{background-color:#1a1a1a;}}"
|
||||
f"QPushButton:disabled{{background-color:#2d2d30;color:#555555;border-color:#444444;}}"
|
||||
)
|
||||
from gui import qt_icons
|
||||
|
||||
qt_icons.apply_button_icon(btn, text, color=_icon_color)
|
||||
if not btn.icon().isNull():
|
||||
btn.setIconSize(QSize(16, 16))
|
||||
return btn
|
||||
|
||||
|
||||
|
|
@ -470,9 +477,11 @@ def _make_text_btn(label: str, tooltip: str = "", *, min_width: int = 52) -> QPu
|
|||
|
||||
def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton:
|
||||
"""Compact icon-only button (e.g. folder browse)."""
|
||||
from gui.platform_glyph import platform_glyph
|
||||
from gui import qt_icons
|
||||
|
||||
btn = QPushButton(platform_glyph(icon_text))
|
||||
btn = QPushButton()
|
||||
qt_icons.apply_button_icon(btn, icon_text, color="#dddddd")
|
||||
btn.setIconSize(QSize(18, 18))
|
||||
btn.setToolTip(tooltip)
|
||||
btn.setFont(QFont("Segoe UI", 12))
|
||||
btn.setFixedSize(40, 36)
|
||||
|
|
@ -1848,7 +1857,10 @@ class WorkflowTab(QWidget):
|
|||
import_row = QHBoxLayout()
|
||||
import_row.setSpacing(8)
|
||||
import_row.setAlignment(Qt.AlignVCenter)
|
||||
import_btn = QPushButton("↓ Import Selected → files/")
|
||||
from gui import qt_icons
|
||||
|
||||
import_btn = QPushButton()
|
||||
qt_icons.apply_button_icon(import_btn, "↓ Import Selected → files/", color="#4da8f0")
|
||||
import_btn.setStyleSheet(
|
||||
"QPushButton{background-color:#2d2d30;color:#4da8f0;border:1px solid #007acc;"
|
||||
"padding:0px;border-radius:4px;font-size:12px;font-weight:bold;"
|
||||
|
|
@ -1862,7 +1874,8 @@ class WorkflowTab(QWidget):
|
|||
import_btn.clicked.connect(lambda _checked=False: self._import_files())
|
||||
self._register_import_button(import_btn)
|
||||
import_row.addWidget(import_btn)
|
||||
clear_translated_btn = QPushButton("✕ Clear translated/")
|
||||
clear_translated_btn = QPushButton()
|
||||
qt_icons.apply_button_icon(clear_translated_btn, "✕ Clear translated/", color="#cc4444")
|
||||
clear_translated_btn.setStyleSheet(
|
||||
"QPushButton{background-color:#2d2d30;color:#cc4444;border:1px solid #8b0000;"
|
||||
"padding:0px;border-radius:4px;font-size:12px;font-weight:bold;"
|
||||
|
|
@ -3381,10 +3394,12 @@ class WorkflowTab(QWidget):
|
|||
self._file_items = items
|
||||
self.file_list.clear()
|
||||
|
||||
from gui.qt_icons import file_category_icon
|
||||
|
||||
for item in items:
|
||||
cat = item["category"]
|
||||
icon = "📄" if cat == "core" else ("🗺" if cat == "map" else "❓")
|
||||
lw = QListWidgetItem(f"{icon} {item['name']} ({item['size_kb']:.1f} KB)")
|
||||
lw = QListWidgetItem(f"{item['name']} ({item['size_kb']:.1f} KB)")
|
||||
lw.setIcon(file_category_icon(cat))
|
||||
lw.setData(Qt.UserRole, item)
|
||||
lw.setFlags(lw.flags() | Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable)
|
||||
lw.setCheckState(Qt.Checked if item["default"] else Qt.Unchecked)
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ tiktoken>=0.8.0
|
|||
tqdm>=4.65.0
|
||||
jsbeautifier>=1.14.0
|
||||
pillow>=12.1.0
|
||||
pefile>=2023.2.7
|
||||
PyQt5>=5.15.0
|
||||
qtawesome>=1.3.0
|
||||
|
|
|
|||
62
tests/test_qt_icons.py
Normal file
62
tests/test_qt_icons.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Tests for the shared qtawesome icon helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
try:
|
||||
from PyQt5.QtWidgets import QApplication, QPushButton
|
||||
|
||||
_HAS_QT = True
|
||||
except Exception: # pragma: no cover - PyQt5 not installed
|
||||
_HAS_QT = False
|
||||
|
||||
|
||||
@unittest.skipUnless(_HAS_QT, "PyQt5 not available")
|
||||
class TestQtIcons(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls._app = QApplication.instance() or QApplication([])
|
||||
|
||||
def test_split_leading_symbol(self) -> None:
|
||||
from gui.qt_icons import split_leading_symbol
|
||||
|
||||
self.assertEqual(split_leading_symbol("🔑 API Configuration"),
|
||||
("mdi6.key-variant", "API Configuration"))
|
||||
self.assertEqual(split_leading_symbol("►► Run Both"),
|
||||
("mdi6.fast-forward", "Run Both"))
|
||||
self.assertEqual(split_leading_symbol("Next →"), (None, "Next →"))
|
||||
# Unmapped leading symbol: emoji is stripped, no icon returned.
|
||||
name, rest = split_leading_symbol("Plain label")
|
||||
self.assertIsNone(name)
|
||||
self.assertEqual(rest, "Plain label")
|
||||
|
||||
def test_file_category_icon_non_null(self) -> None:
|
||||
from gui.qt_icons import HAS_QTA, file_category_icon
|
||||
|
||||
for category in ("core", "map", "other", "unknown-category"):
|
||||
icon = file_category_icon(category)
|
||||
if HAS_QTA:
|
||||
self.assertFalse(icon.isNull(), category)
|
||||
|
||||
def test_apply_button_icon_strips_emoji(self) -> None:
|
||||
from gui.qt_icons import HAS_QTA, apply_button_icon
|
||||
|
||||
btn = QPushButton()
|
||||
apply_button_icon(btn, "💾 Save vocab.txt", color="#cccccc")
|
||||
if HAS_QTA:
|
||||
self.assertEqual(btn.text(), "Save vocab.txt")
|
||||
self.assertFalse(btn.icon().isNull())
|
||||
|
||||
def test_make_section_header_returns_widget(self) -> None:
|
||||
from gui.qt_icons import make_section_header
|
||||
|
||||
w = make_section_header("🌐 Translation Settings", "color:#007acc;")
|
||||
self.assertIsNotNone(w)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue