Fix icons to work in both linux and windows via qtawesome

This commit is contained in:
DazedAnon 2026-07-07 14:39:56 -05:00
parent a2f3eb084b
commit c781f66ae7
13 changed files with 498 additions and 145 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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
View 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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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
View 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()