diff --git a/gui/config_tab.py b/gui/config_tab.py index 757819f..6c791e5 100644 --- a/gui/config_tab.py +++ b/gui/config_tab.py @@ -13,6 +13,7 @@ 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 @@ -95,7 +96,7 @@ class ModelFetchThread(QThread): def create_section_header(title): """Create a clean section header without boxes.""" - label = QLabel(title) + label = QLabel(platform_glyph(title)) label.setStyleSheet(""" QLabel { font-size: 13px; @@ -247,29 +248,13 @@ class ConfigTab(QWidget): def create_nav_button(self, icon_text, tooltip): """Create a navigation button for the top bar.""" + from gui.platform_glyph import configure_nav_toolbutton + btn = QToolButton() - btn.setText(icon_text) btn.setToolTip(tooltip) btn.setFixedSize(50, 50) btn.setCheckable(True) - btn.setStyleSheet(""" - QToolButton { - background-color: transparent; - border: none; - border-bottom: 3px solid transparent; - color: #cccccc; - font-size: 24px; - padding: 5px; - } - QToolButton:hover { - background-color: #3e3e42; - } - QToolButton:checked { - background-color: #37373d; - border-bottom: 3px solid #007acc; - color: #ffffff; - } - """) + configure_nav_toolbutton(btn, icon_text, horizontal=True) return btn def switch_page(self, index): @@ -611,7 +596,7 @@ class ConfigTab(QWidget): button_layout = QHBoxLayout() button_layout.setSpacing(10) - reset_button = QPushButton("🔄 Reset to Defaults") + reset_button = QPushButton(platform_glyph("🔄 Reset to Defaults")) reset_button.clicked.connect(self.reset_to_defaults_with_save) reset_button.setMinimumHeight(32) diff --git a/gui/csv_tab.py b/gui/csv_tab.py index cbb1188..a173583 100644 --- a/gui/csv_tab.py +++ b/gui/csv_tab.py @@ -8,6 +8,7 @@ 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 @@ -17,7 +18,7 @@ except ImportError: def create_section_label(text): """Create a section label for grouping settings.""" - label = QLabel(text) + label = QLabel(platform_glyph(text)) label.setStyleSheet(""" QLabel { font-size: 12px; @@ -247,7 +248,7 @@ class CSVTab(QWidget): button_layout = QHBoxLayout() button_layout.setSpacing(10) - self.reset_button = QPushButton("🔄 Reset to Defaults") + self.reset_button = QPushButton(platform_glyph("🔄 Reset to Defaults")) self.reset_button.clicked.connect(self.reset_to_defaults_with_message) self.reset_button.setMaximumWidth(180) self.reset_button.setMinimumHeight(32) diff --git a/gui/log_viewer.py b/gui/log_viewer.py index e4b9dc8..78d5c23 100644 --- a/gui/log_viewer.py +++ b/gui/log_viewer.py @@ -7,6 +7,7 @@ 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 @@ -34,7 +35,7 @@ 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("📝 Translation Log") + header = QLabel(platform_glyph("📝 Translation Log")) header.setStyleSheet(""" QLabel { font-size: 13px; diff --git a/gui/main.py b/gui/main.py index 62bb8dd..5b52402 100644 --- a/gui/main.py +++ b/gui/main.py @@ -455,29 +455,13 @@ class DazedMTLGUI(QMainWindow): def create_nav_button(self, icon_text, tooltip): """Create a navigation button for the sidebar.""" + from gui.platform_glyph import configure_nav_toolbutton + btn = QToolButton() - btn.setText(icon_text) btn.setToolTip(tooltip) btn.setFixedSize(60, 50) btn.setCheckable(True) - btn.setStyleSheet(""" - QToolButton { - background-color: transparent; - border: none; - border-left: 3px solid transparent; - color: #cccccc; - font-size: 24px; - padding: 5px; - } - QToolButton:hover { - background-color: #3e3e42; - } - QToolButton:checked { - background-color: #37373d; - border-left: 3px solid #007acc; - color: #ffffff; - } - """) + configure_nav_toolbutton(btn, icon_text, horizontal=False) return btn def switch_page(self, index): diff --git a/gui/platform_glyph.py b/gui/platform_glyph.py new file mode 100644 index 0000000..59e8d5d --- /dev/null +++ b/gui/platform_glyph.py @@ -0,0 +1,162 @@ +"""Linux fallbacks for emoji that PyQt5 often cannot render (tofu boxes). + +Only replaces glyphs known to fail on typical Linux font stacks. +Windows/macOS get the original strings unchanged. +""" + +from __future__ import annotations + +import re +import sys + +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QColor, QFont, QIcon, QPainter, QPixmap + +# Toolbar buttons, section headers, mixed labels +_LINUX_EMOJI_FALLBACKS: dict[str, str] = { + "\U00002795": "+", # ➕ add + "\U0001F4C1": "\u2302", # 📁 folder → ⌂ + "\U0001F4C2": "\u2302", # 📂 open folder + "\U0001F504": "\u21bb", # 🔄 refresh → ↻ + "\U0001F4DD": "", # 📝 memo (section headers) + "\U0001F6D1": "\u25a0", # 🛑 stop → ■ + "\U0001F4E5": "\u2193", # 📥 inbox tray → ↓ + "\U0001F527": "\u2699", # 🔧 wrench → ⚙ + "\U0001F511": "", # 🔑 key (section headers) + "\U0001F310": "", # 🌐 globe (section headers) + "\U0001F5A5": "", # 🖥 desktop (section headers) + "\U00002699\uFE0F": "\u2699", # ⚙️ → ⚙ + "\U0000270F\uFE0F": "\u270E", # ✏️ → ✎ +} + +# Icon-only nav tabs — never empty; BMP symbols so metrics match across the bar +_LINUX_NAV_FALLBACKS: dict[str, str] = { + "\U0001F310": "\u25ce", # 🌐 Translation → ◎ + "\U0001F527": "\u2699", # 🔧 General → ⚙ + "\U0001F3AE": "\u25c6", # 🎮 MV/MZ → ◆ + "\U0001F43A": "\u25c8", # 🐺 Wolf → ◈ + "\U0001F4C4": "\u2630", # 📄 CSV → ☰ + "\U00002694\uFE0F": "\u2694", # ⚔️ SRPG → ⚔ + "\U00002694": "\u2694", + "\U0001F504": "\u21bb", # 🔄 Update → ↻ + "\U00002699\uFE0F": "\u2699", # ⚙️ Config → ⚙ + "\U0000270F\uFE0F": "\u270E", # ✏️ Rewrite → ✎ + "\U000026A1": "\u26a1", # ⚡ Workflow +} + + +def _apply_fallbacks(text: str, mapping: dict[str, str]) -> str: + out = text + for emoji, replacement in mapping.items(): + out = out.replace(emoji, replacement) + out = re.sub(r" +", " ", out) + return out.strip() + + +def platform_glyph(text: str) -> str: + """Return UI text with unsupported emoji swapped on Linux only.""" + if not sys.platform.startswith("linux") or not text: + return text + return _apply_fallbacks(text, _LINUX_EMOJI_FALLBACKS) + + +def platform_nav_glyph(icon: str) -> str: + """Linux fallback for icon-only nav tabs (never returns empty).""" + if not sys.platform.startswith("linux") or not icon: + return icon + + out = _apply_fallbacks(icon, _LINUX_NAV_FALLBACKS) + if not out: + out = _apply_fallbacks(icon, _LINUX_EMOJI_FALLBACKS) + return out or icon + + +def _linux_nav_pixmap(glyph: str, *, size: int, font_px: int, color: QColor) -> QPixmap: + pixmap = QPixmap(size, size) + pixmap.fill(Qt.transparent) + + painter = QPainter(pixmap) + font = QFont("DejaVu Sans") + font.setPixelSize(font_px) + painter.setFont(font) + painter.setPen(color) + painter.drawText(pixmap.rect(), Qt.AlignCenter, glyph) + painter.end() + return pixmap + + +def _linux_nav_icon(icon_text: str, *, size: int, font_px: int) -> QIcon: + """Render a nav glyph into a fixed-size pixmap so every tab aligns the same.""" + glyph = platform_nav_glyph(icon_text) + icon = QIcon() + gray = _linux_nav_pixmap(glyph, size=size, font_px=font_px, color=QColor("#cccccc")) + white = _linux_nav_pixmap(glyph, size=size, font_px=font_px, color=QColor("#ffffff")) + icon.addPixmap(gray, QIcon.Normal, QIcon.Off) + icon.addPixmap(gray, QIcon.Disabled, QIcon.Off) + icon.addPixmap(white, QIcon.Active, QIcon.Off) + icon.addPixmap(white, QIcon.Selected, QIcon.Off) + return icon + + +def _nav_toolbutton_stylesheet(*, horizontal: bool, icon_only: bool) -> str: + border_prop = "border-bottom" if horizontal else "border-left" + if icon_only: + return f""" + QToolButton {{ + background-color: transparent; + border: none; + {border_prop}: 3px solid transparent; + color: #cccccc; + padding: 0px; + margin: 0px; + }} + QToolButton:hover {{ + background-color: #3e3e42; + }} + QToolButton:checked {{ + background-color: #37373d; + {border_prop}: 3px solid #007acc; + color: #ffffff; + }} + """ + + font_size = "18px" if sys.platform.startswith("linux") else "24px" + return f""" + QToolButton {{ + background-color: transparent; + border: none; + {border_prop}: 3px solid transparent; + color: #cccccc; + font-size: {font_size}; + font-family: 'DejaVu Sans Mono', monospace; + padding: 0px; + margin: 0px; + }} + QToolButton:hover {{ + background-color: #3e3e42; + }} + QToolButton:checked {{ + background-color: #37373d; + {border_prop}: 3px solid #007acc; + color: #ffffff; + }} + """ + + +def configure_nav_toolbutton(btn, icon_text: str, *, horizontal: bool = False) -> None: + """Apply nav icon — on Linux render BMP symbols into fixed pixmaps for alignment.""" + 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) + font_px = max(22, int(icon_dim * 0.78)) + btn.setIcon(_linux_nav_icon(icon_text, size=icon_dim, font_px=font_px)) + btn.setIconSize(QSize(icon_dim, icon_dim)) + btn.setText("") + btn.setToolButtonStyle(Qt.ToolButtonIconOnly) + btn.setStyleSheet(_nav_toolbutton_stylesheet(horizontal=horizontal, icon_only=True)) + return + + btn.setIcon(QIcon()) + btn.setText(icon_text) + btn.setToolButtonStyle(Qt.ToolButtonTextOnly) + btn.setStyleSheet(_nav_toolbutton_stylesheet(horizontal=horizontal, icon_only=False)) diff --git a/gui/rpgmaker_tab.py b/gui/rpgmaker_tab.py index ba92bc5..ef7c86b 100644 --- a/gui/rpgmaker_tab.py +++ b/gui/rpgmaker_tab.py @@ -10,6 +10,7 @@ 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 @@ -25,8 +26,8 @@ except Exception: def create_section_label(text): - """Create a section label for grouping checkboxes.""" - label = QLabel(text) + """Create a section label for grouping settings.""" + label = QLabel(platform_glyph(text)) label.setStyleSheet(""" QLabel { font-size: 13px; @@ -456,7 +457,7 @@ class RPGMakerTab(QWidget): # Reset button button_layout = QHBoxLayout() - self.reset_button = QPushButton("🔄 Reset to Defaults") + self.reset_button = QPushButton(platform_glyph("🔄 Reset to Defaults")) self.reset_button.clicked.connect(self.reset_to_defaults_with_message) self.reset_button.setMinimumHeight(32) self.reset_button.setMaximumWidth(160) diff --git a/gui/srpg_tab.py b/gui/srpg_tab.py index 36061a2..43f0262 100644 --- a/gui/srpg_tab.py +++ b/gui/srpg_tab.py @@ -9,6 +9,7 @@ from PyQt5.QtWidgets import ( ) from PyQt5.QtCore import pyqtSignal import re +from gui.platform_glyph import platform_glyph try: import set_defaults @@ -19,7 +20,7 @@ except Exception: def create_section_label(text): """Create a styled section header label.""" - label = QLabel(text) + label = QLabel(platform_glyph(text)) label.setStyleSheet("font-size: 12px; font-weight: bold; color: #007acc; padding: 2px 0px;") return label @@ -83,7 +84,7 @@ class SRPGTab(QWidget): button_layout = QHBoxLayout() button_layout.setSpacing(10) - self.reset_btn = QPushButton("🔄 Reset to Defaults") + self.reset_btn = QPushButton(platform_glyph("🔄 Reset to Defaults")) self.reset_btn.clicked.connect(self.reset_to_defaults_with_message) self.reset_btn.setMaximumWidth(180) self.reset_btn.setMinimumHeight(32) diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 3dda224..3264b64 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -31,6 +31,7 @@ from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess, QEvent, QRect, QSettings from PyQt5.QtGui import QFont from gui.log_viewer import LogViewer +from gui.platform_glyph import platform_glyph def _strip_ansi(text): @@ -41,7 +42,7 @@ def _strip_ansi(text): def create_section_header(title): """Create a clean section header without boxes.""" - label = QLabel(title) + label = QLabel(platform_glyph(title)) label.setStyleSheet(""" QLabel { font-size: 13px; @@ -849,7 +850,7 @@ class TranslationTab(QWidget): deselect_all_btn.setStyleSheet(icon_button_style) file_buttons.addWidget(deselect_all_btn) - add_files_btn = QPushButton("➕") + 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) @@ -861,13 +862,13 @@ class TranslationTab(QWidget): remove_files_btn.setStyleSheet(icon_button_style) file_buttons.addWidget(remove_files_btn) - open_folder_btn = QPushButton("📁") + 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("🔄") + 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) @@ -1120,12 +1121,12 @@ class TranslationTab(QWidget): 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("📂") + self.open_translations_button = QPushButton(platform_glyph("📂")) 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("🔄") + self.sync_translated_button = QPushButton(platform_glyph("🔄")) 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) @@ -1195,7 +1196,7 @@ 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("🛑") + self.stop_button = QPushButton(platform_glyph("🛑")) self.stop_button.setToolTip("Stop Translation") self.stop_button.clicked.connect(self.stop_translation) self.stop_button.setStyleSheet(stop_button_style) diff --git a/gui/wolf_tab.py b/gui/wolf_tab.py index a773e11..cc507e1 100644 --- a/gui/wolf_tab.py +++ b/gui/wolf_tab.py @@ -9,6 +9,7 @@ from PyQt5.QtWidgets import ( ) from PyQt5.QtCore import pyqtSignal import re +from gui.platform_glyph import platform_glyph try: import set_defaults CANONICAL_DEFAULTS = getattr(set_defaults, 'DEFAULTS', None) @@ -17,7 +18,7 @@ except Exception: def create_section_label(text): """Create a styled section header label.""" - label = QLabel(text) + label = QLabel(platform_glyph(text)) label.setStyleSheet("font-size: 12px; font-weight: bold; color: #007acc; padding: 2px 0px;") return label @@ -171,7 +172,7 @@ class WolfTab(QWidget): button_layout = QHBoxLayout() button_layout.setSpacing(10) - self.reset_btn = QPushButton("🔄 Reset to Defaults") + self.reset_btn = QPushButton(platform_glyph("🔄 Reset to Defaults")) self.reset_btn.clicked.connect(self.reset_to_defaults_with_message) self.reset_btn.setMaximumWidth(180) self.reset_btn.setMinimumHeight(32) diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 1a2f433..ada3d4f 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -444,7 +444,9 @@ def _make_btn(text: str, color: str = "#007acc") -> QPushButton: def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton: """Compact icon-only button matching the Translation tab action buttons.""" - btn = QPushButton(icon_text) + from gui.platform_glyph import platform_glyph + + btn = QPushButton(platform_glyph(icon_text)) btn.setToolTip(tooltip) btn.setFont(QFont("Segoe UI", 12)) btn.setFixedSize(40, 36)