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