feat: CJK font rendering, force quit confirmation, settings i18n

- Rebuild CJK font subset (1421 glyphs) and convert CFF→TTF for
  stb_truetype compatibility, fixing Chinese/Japanese/Korean rendering
- Add force quit confirmation dialog with cancel/confirm actions
- Show force quit tooltip immediately on hover (no delay)
- Translate hardcoded English strings in settings dropdowns
  (auto-lock timeouts, slider "Off" labels)
- Fix mojibake en-dashes in 7 translation JSON files
- Add helper scripts: build_cjk_subset, convert_cjk_to_ttf,
  check_font_coverage, fix_mojibake
This commit is contained in:
2026-04-12 10:32:58 -05:00
parent 821c54ba2b
commit fbdba1a001
28 changed files with 5471 additions and 4909 deletions

View File

@@ -419,6 +419,20 @@ configure_file(
@ONLY
)
# INCBIN uses .incbin assembler directives that reference font files at
# assembly time — CMake doesn't track these implicit dependencies.
# Tell CMake that the generated source depends on the actual font binaries
# so a font file change triggers recompilation.
set_source_files_properties(
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
PROPERTIES OBJECT_DEPENDS
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon
${APP_SOURCES}
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp

Binary file not shown.

Binary file not shown.

View File

@@ -652,7 +652,7 @@
"show": "Anzeigen",
"show_hidden": "Ausgeblendete anzeigen (%d)",
"show_qr_code": "QR-Code anzeigen",
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)",
"showing_transactions": "Zeige %d%d von %d Transaktionen (gesamt: %zu)",
"simple_background": "Einfacher Hintergrund",
"start_mining": "Mining starten",
"status": "Status",

View File

@@ -652,7 +652,7 @@
"show": "Afficher",
"show_hidden": "Afficher masqués (%d)",
"show_qr_code": "Afficher le code QR",
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)",
"showing_transactions": "Affichage %d%d sur %d transactions (total : %zu)",
"simple_background": "Arrière-plan simple",
"start_mining": "Démarrer le minage",
"status": "Statut",

View File

@@ -652,7 +652,7 @@
"show": "表示",
"show_hidden": "非表示を表示 (%d)",
"show_qr_code": "QRコードを表示",
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu",
"showing_transactions": "%d%d / %d 件の取引を表示中(合計:%zu",
"simple_background": "シンプル背景",
"start_mining": "マイニング開始",
"status": "ステータス",

View File

@@ -652,7 +652,7 @@
"show": "표시",
"show_hidden": "숨겨진 항목 표시 (%d)",
"show_qr_code": "QR 코드 표시",
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
"showing_transactions": "%d%d / %d건의 거래 표시 중 (총: %zu)",
"simple_background": "단순 배경",
"start_mining": "채굴 시작",
"status": "상태",

View File

@@ -652,7 +652,7 @@
"show": "Mostrar",
"show_hidden": "Mostrar ocultos (%d)",
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)",
"showing_transactions": "Mostrando %d%d de %d transações (total: %zu)",
"simple_background": "Fundo simples",
"start_mining": "Iniciar Mineração",
"status": "Status",

View File

@@ -652,7 +652,7 @@
"show": "Показать",
"show_hidden": "Показать скрытые (%d)",
"show_qr_code": "Показать QR-код",
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)",
"showing_transactions": "Показано %d%d из %d транзакций (всего: %zu)",
"simple_background": "Простой фон",
"start_mining": "Начать майнинг",
"status": "Статус",

View File

@@ -652,7 +652,7 @@
"show": "显示",
"show_hidden": "显示已隐藏 (%d)",
"show_qr_code": "显示二维码",
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu",
"showing_transactions": "显示第 %d%d 笔,共 %d 笔交易(总计:%zu",
"simple_background": "简单背景",
"start_mining": "开始挖矿",
"status": "状态",

131
scripts/build_cjk_subset.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Build a NotoSansCJK subset font containing all characters used by
the zh, ja, and ko translation files, plus common CJK punctuation
and symbols.
Usage:
python3 scripts/build_cjk_subset.py
Requires: pip install fonttools brotli
"""
import json
import os
from fontTools.ttLib import TTFont
from fontTools import subset as ftsubset
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
# Collect all characters used in CJK translation files
needed = set()
for lang in ['zh', 'ja', 'ko']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
for v in data.values():
if isinstance(v, str):
for c in v:
cp = ord(c)
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
needed.add(cp)
# Also add common CJK ranges that future translations might use:
# - CJK punctuation and symbols (3000-303F)
# - Hiragana (3040-309F)
# - Katakana (30A0-30FF)
# - Bopomofo (3100-312F)
# - CJK quotation marks, brackets
for cp in range(0x3000, 0x3100):
needed.add(cp)
for cp in range(0x3100, 0x3130):
needed.add(cp)
# Fullwidth ASCII variants (commonly mixed in CJK text)
for cp in range(0xFF01, 0xFF5F):
needed.add(cp)
print(f"Total non-ASCII characters to include: {len(needed)}")
# Check which of these the source font supports
font = TTFont(SOURCE_FONT)
cmap = font.getBestCmap()
supportable = needed & set(cmap.keys())
unsupported = needed - set(cmap.keys())
print(f"Supported by source font: {len(supportable)}")
if unsupported:
print(f"Not in source font (will use fallback): {len(unsupported)}")
for cp in sorted(unsupported)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
# Build the subset using pyftsubset CLI-style API
args = [
SOURCE_FONT,
f'--output-file={OUTPUT_FONT}',
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
'--no-hinting',
'--desubroutinize',
]
ftsubset.main(args)
# Convert CFF outlines to TrueType (glyf) outlines.
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib import newTable
tmp_otf = OUTPUT_FONT + '.tmp.otf'
os.rename(OUTPUT_FONT, tmp_otf)
conv = TTFont(tmp_otf)
if 'CFF ' in conv:
print("Converting CFF -> TrueType outlines...")
glyphOrder = conv.getGlyphOrder()
glyphSet = conv.getGlyphSet()
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception:
glyf_table.glyphs[gname] = TTGlyph()
del conv['CFF ']
if 'VORG' in conv:
del conv['VORG']
conv['glyf'] = glyf_table
conv['loca'] = loca_table
conv['head'].indexToLocFormat = 1
if 'maxp' in conv:
conv['maxp'].version = 0x00010000
conv.sfntVersion = "\x00\x01\x00\x00"
conv.save(OUTPUT_FONT)
conv.close()
os.remove(tmp_otf)
size = os.path.getsize(OUTPUT_FONT)
print(f"\nOutput: {OUTPUT_FONT}")
print(f"Size: {size / 1024:.0f} KB")
# Verify
verify = TTFont(OUTPUT_FONT)
verify_cmap = set(verify.getBestCmap().keys())
still_missing = needed - verify_cmap
print(f"Verified glyphs in subset: {len(verify_cmap)}")
if still_missing:
# These are chars not in the source font - expected for some Hangul/Hiragana
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
for cp in sorted(still_missing)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
else:
print("All needed characters are covered!")

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Check which characters in translation files fall outside the font glyph ranges."""
import json
import unicodedata
import glob
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
# Glyph ranges from typography.cpp (regular font + CJK merge)
RANGES = [
# Regular font ranges
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
(0x0100, 0x024F), # Latin Extended-A + B
(0x0370, 0x03FF), # Greek and Coptic
(0x0400, 0x04FF), # Cyrillic
(0x0500, 0x052F), # Cyrillic Supplement
(0x2000, 0x206F), # General Punctuation
(0x2190, 0x21FF), # Arrows
(0x2200, 0x22FF), # Mathematical Operators
(0x2600, 0x26FF), # Miscellaneous Symbols
# CJK ranges
(0x2E80, 0x2FDF), # CJK Radicals
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
(0x3100, 0x312F), # Bopomofo
(0x31F0, 0x31FF), # Katakana Extensions
(0x3400, 0x4DBF), # CJK Extension A
(0x4E00, 0x9FFF), # CJK Unified Ideographs
(0xAC00, 0xD7AF), # Hangul Syllables
(0xFF00, 0xFFEF), # Fullwidth Forms
]
def in_ranges(cp):
return any(lo <= cp <= hi for lo, hi in RANGES)
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
lang = os.path.basename(path).replace('.json', '')
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
missing = {}
for key, val in data.items():
if not isinstance(val, str):
continue
for c in val:
cp = ord(c)
if cp > 0x7F and not in_ranges(cp):
if c not in missing:
missing[c] = []
missing[c].append(key)
if missing:
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
for c in sorted(missing, key=lambda x: ord(x)):
cp = ord(c)
name = unicodedata.name(c, 'UNKNOWN')
keys = missing[c][:3]
key_str = ', '.join(keys)
if len(missing[c]) > 3:
key_str += f' (+{len(missing[c])-3} more)'
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
else:
print(f"=== {lang}.json: OK (all characters covered) ===")

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Check which characters needed by translations are missing from bundled fonts."""
import json
import os
from fontTools.ttLib import TTFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
# Load font cmaps
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
cjk_cmap = set(cjk.getBestCmap().keys())
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
combined = cjk_cmap | ubuntu_cmap
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
print(f"Combined: {len(combined)}")
print()
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
needed = set()
for v in data.values():
if isinstance(v, str):
for c in v:
needed.add(ord(c))
missing = sorted(needed - combined)
if missing:
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
for cp in missing[:20]:
c = chr(cp)
print(f" U+{cp:04X} {c}")
if len(missing) > 20:
print(f" ... and {len(missing) - 20} more")
else:
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
so we need glyf-based TrueType outlines instead.
Two approaches:
1. Direct CFF->TTF conversion via cu2qu (fontTools)
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
This script tries approach 1 first, falls back to approach 2.
"""
import os
import sys
import json
import glob
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
def get_needed_codepoints():
"""Collect all unique codepoints from CJK translation files."""
codepoints = set()
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
with open(lang_file, "r", encoding="utf-8") as f:
data = json.load(f)
for value in data.values():
if isinstance(value, str):
for ch in value:
cp = ord(ch)
# Include CJK + Hangul + fullwidth + CJK symbols/kana
if cp >= 0x2E80:
codepoints.add(cp)
return codepoints
def convert_cff_to_ttf():
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
from fontTools.ttLib import TTFont
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
print(f"Loading {SRC_OTF}...")
font = TTFont(SRC_OTF)
# Verify it's CFF
if "CFF " not in font:
print("Font is not CFF, skipping conversion")
return False
cff = font["CFF "]
top = cff.cff.topDictIndex[0]
print(f"ROS: {getattr(top, 'ROS', None)}")
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
glyphOrder = font.getGlyphOrder()
print(f"Glyphs: {len(glyphOrder)}")
# Use fontTools' built-in otf2ttf if available
try:
from fontTools.otf2ttf import otf_to_ttf
otf_to_ttf(font)
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
except ImportError:
pass
# Manual conversion using cu2qu
print("Using manual CFF->TTF conversion with cu2qu...")
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.pointPen import SegmentToPointPen
from fontTools import ttLib
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
import struct
# Get glyph set
glyphSet = font.getGlyphSet()
# Create new glyf table
from fontTools.ttLib import newTable
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
max_error = 1.0 # em-units tolerance for cubic->quadratic
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception as e:
# Fallback: empty glyph
glyf_table.glyphs[gname] = TTGlyph()
# Replace CFF with glyf
del font["CFF "]
if "VORG" in font:
del font["VORG"]
font["glyf"] = glyf_table
font["loca"] = loca_table
# Add required tables for TTF
# head table needs indexToLocFormat
font["head"].indexToLocFormat = 1 # long format
# Create maxp for TrueType
if "maxp" in font:
font["maxp"].version = 0x00010000
# Update sfntVersion
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def download_and_subset():
"""Download NotoSansSC-Regular.ttf and subset it."""
import urllib.request
from fontTools.ttLib import TTFont
from fontTools import subset
# Google Fonts provides static TTF files
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
# Actually, we want TTF. Let's try the variable font approach.
# Or better: use google-fonts API for static TTF
# NotoSansSC static TTF from Google Fonts CDN
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
if not os.path.exists(tmp_font):
print(f"Downloading NotoSansSC-Regular.ttf...")
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
# This is a TTC (font collection), too large.
# Use the OTF we already have and convert it.
return False
print(f"Using {tmp_font}")
font = TTFont(tmp_font)
cmap = font.getBestCmap()
print(f"Source has {len(cmap)} cmap entries")
needed = get_needed_codepoints()
print(f"Need {len(needed)} CJK codepoints")
# Subset
subsetter = subset.Subsetter()
subsetter.populate(unicodes=needed)
subsetter.subset(font)
font.save(DST_TTF)
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def verify_result():
"""Verify the output TTF has glyf outlines and correct characters."""
from fontTools.ttLib import TTFont
font = TTFont(DST_TTF)
cmap = font.getBestCmap()
print(f"\n--- Verification ---")
print(f"Format: {font.sfntVersion!r}")
print(f"Has glyf: {'glyf' in font}")
print(f"Has CFF: {'CFF ' in font}")
print(f"Cmap entries: {len(cmap)}")
# Check key characters
test_chars = {
"": 0x5386, "": 0x53F2, # Chinese: history
"": 0x6982, "": 0x8FF0, # Chinese: overview
"": 0x8BBE, "": 0x7F6E, # Chinese: settings
}
for name, cp in test_chars.items():
status = "YES" if cp in cmap else "NO"
print(f" {name} (U+{cp:04X}): {status}")
size = os.path.getsize(DST_TTF)
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
font.close()
if __name__ == "__main__":
print("=== CJK Font CFF -> TTF Converter ===\n")
if convert_cff_to_ttf():
verify_result()
else:
print("Direct conversion failed, trying download approach...")
if download_and_subset():
verify_result()
else:
print("ERROR: Could not convert font")
sys.exit(1)

36
scripts/fix_mojibake.py Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
import os
import glob
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
MOJIBAKE_FIXES = {
'\u00e2\u0080\u0093': '\u2013', # en dash
'\u00e2\u0080\u0094': '\u2014', # em dash
'\u00e2\u0080\u0099': '\u2019', # right single quote
'\u00e2\u0080\u009c': '\u201c', # left double quote
'\u00e2\u0080\u009d': '\u201d', # right double quote
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
}
total_fixed = 0
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
with open(path, 'r', encoding='utf-8') as f:
raw = f.read()
original = raw
for bad, good in MOJIBAKE_FIXES.items():
if bad in raw:
count = raw.count(bad)
raw = raw.replace(bad, good)
lang = os.path.basename(path)
print(f" {lang}: fixed {count} x {repr(good)}")
total_fixed += count
if raw != original:
with open(path, 'w', encoding='utf-8') as f:
f.write(raw)
print(f"\nTotal fixes: {total_fixed}")

View File

@@ -2535,12 +2535,50 @@ void App::renderShutdownScreen()
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(forceLabel, btnSize)) {
DEBUG_LOGF("Force quit requested by user after %.0fs\n", shutdown_timer_);
shutdown_complete_ = true;
force_quit_confirm_ = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("force_quit_warning"));
}
ImGui::PopStyleColor(3);
}
// Force Quit confirmation popup
if (force_quit_confirm_) {
ImGui::OpenPopup("##ForceQuitConfirm");
force_quit_confirm_ = false;
}
ImVec2 popupCenter(wp.x + vp_size.x * 0.5f, wp.y + vp_size.y * 0.5f);
ImGui::SetNextWindowPos(popupCenter, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##ForceQuitConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted(TR("force_quit_confirm_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::TextUnformatted(TR("force_quit_confirm_msg"));
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) {
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
shutdown_complete_ = true;
ImGui::CloseCurrentPopup();
}
ImGui::PopStyleColor(3);
ImGui::EndPopup();
}
ImGui::Spacing();
ImGui::Spacing();

View File

@@ -372,6 +372,7 @@ private:
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)

View File

@@ -12,4 +12,4 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -300,7 +300,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
cjkCfg.GlyphRanges = cjkRanges;
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
if (mergeResult) {
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
g_noto_cjk_subset_size, name);
} else {
DEBUG_LOGF("Typography: WARNING — CJK merge FAILED for %s (size=%u)\n",
name, g_noto_cjk_subset_size);
}
}
} else {
DEBUG_LOGF("Typography: Failed to load %s\n", name);

View File

@@ -713,7 +713,7 @@ void RenderSettingsPage(App* app) {
{
char blur_fmt[16];
if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -735,7 +735,7 @@ void RenderSettingsPage(App* app) {
{
char noise_fmt[16];
if (sp_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
@@ -1001,7 +1001,7 @@ void RenderSettingsPage(App* app) {
{
char blur_fmt[16];
if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -1019,7 +1019,7 @@ void RenderSettingsPage(App* app) {
{
char noise_fmt[16];
if (sp_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
@@ -1586,7 +1586,7 @@ void RenderSettingsPage(App* app) {
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
int timeout = app->settings()->getAutoLockTimeout();
const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" };
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
int selTimeout = 0;
for (int i = 0; i < 6; i++) {

View File

@@ -261,6 +261,10 @@ void I18n::loadBuiltinEnglish()
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
strings_["force_quit"] = "Force Quit";
strings_["force_quit_warning"] = "This will immediately kill the daemon without a clean shutdown. May require a blockchain resync.";
strings_["force_quit_confirm_title"] = "Force Quit?";
strings_["force_quit_confirm_msg"] = "This will immediately kill the daemon without a clean shutdown.\nThis may corrupt the blockchain index and require a resync.";
strings_["force_quit_yes"] = "Force Quit";
strings_["reduce_motion"] = "Reduce Motion";
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
strings_["ago"] = "ago";
@@ -281,6 +285,13 @@ void I18n::loadBuiltinEnglish()
strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
strings_["settings_configure_explorer"] = "Configure external block explorer links";
strings_["settings_auto_lock"] = "AUTO-LOCK";
strings_["timeout_off"] = "Off";
strings_["timeout_1min"] = "1 min";
strings_["timeout_5min"] = "5 min";
strings_["timeout_15min"] = "15 min";
strings_["timeout_30min"] = "30 min";
strings_["timeout_1hour"] = "1 hour";
strings_["slider_off"] = "Off";
strings_["settings_wallet_file_size"] = "Wallet file size: %s";
strings_["settings_wallet_location"] = "Wallet location: %s";
strings_["settings_rpc_note"] = "Note: Connection settings are usually auto-detected from DRAGONX.conf";