font and width

This commit is contained in:
DazedAnon 2026-07-08 18:25:01 -05:00
parent 0a79b74703
commit 2b4a0e257f
5 changed files with 294 additions and 30 deletions

View file

@ -86,6 +86,7 @@ from gui.workflow_tab import (
_make_section_label,
)
from util.wolfdawn import names as wolf_names
from util.wolfdawn import codes as wolf_codes
from util.wolfdawn import db_classify as wolf_db
from util.wolfdawn import selective_wrap as wolf_selwrap
from util.wolfdawn import wrap_search as wolf_ws
@ -2663,6 +2664,25 @@ class WolfWorkflowTab(QWidget):
width_row.addStretch()
layout.addLayout(width_row)
font_row = QHBoxLayout()
font_lbl = QLabel("Font size \\f[N]:")
font_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
self._wrap_font_spin = QSpinBox()
self._wrap_font_spin.setRange(8, 32)
self._wrap_font_spin.setValue(18)
self._wrap_font_spin.setFixedWidth(70)
self._wrap_font_detect_label = QLabel("")
self._wrap_font_detect_label.setStyleSheet(
"color:#858585;font-size:11px;background:transparent;"
)
apply_font_btn = _make_btn("Apply \\f to text", "#3a3a3a")
apply_font_btn.clicked.connect(self._apply_wrap_font_size)
font_row.addWidget(font_lbl)
font_row.addWidget(self._wrap_font_spin)
font_row.addWidget(apply_font_btn)
font_row.addWidget(self._wrap_font_detect_label, 1)
layout.addLayout(font_row)
self._wrap_preview_label = QLabel("Wrap preview")
self._wrap_preview_label.setStyleSheet(
"color:#858585;font-size:11px;background:transparent;"
@ -2705,7 +2725,7 @@ class WolfWorkflowTab(QWidget):
layout.addWidget(self._wrap_status_label)
layout.addWidget(_make_hr())
sheets_toggle = QPushButton("▸ Browse database sheets (overflow counts)")
sheets_toggle = QPushButton("▸ Browse sheets & name categories (overflow counts)")
sheets_toggle.setCheckable(True)
sheets_toggle.setChecked(False)
sheets_toggle.setStyleSheet(
@ -2721,8 +2741,8 @@ class WolfWorkflowTab(QWidget):
sheets_panel_layout = QVBoxLayout(self._wrap_sheets_panel)
sheets_panel_layout.setContentsMargins(0, 0, 0, 0)
sheets_panel_layout.addWidget(self._desc(
"Sheets with lines longer than the wrap width above. Double-click to open in "
"the editor (first overflowing line)."
"Database sheets and names.json categories (``note``) with lines longer than "
"the wrap width above. Double-click to open the first overflowing entry."
))
self._wrap_sheets_list = QListWidget()
self._wrap_sheets_list.setMaximumHeight(140)
@ -3094,8 +3114,47 @@ class WolfWorkflowTab(QWidget):
f"{hit.field_name} · longest line {hit.max_line_len} chars"
)
self._wrap_detail_label.setText(detail)
self._sync_wrap_font_controls(text)
self._update_wrap_preview()
def _sync_wrap_font_controls(self, text: str):
if not hasattr(self, "_wrap_font_spin"):
return
sizes = wolf_codes.detect_font_sizes(text)
if sizes:
self._wrap_font_spin.blockSignals(True)
self._wrap_font_spin.setValue(sizes[-1])
self._wrap_font_spin.blockSignals(False)
shown = ", ".join(f"\\f[{s}]" for s in sizes[:4])
if len(sizes) > 4:
shown += ", …"
self._wrap_font_detect_label.setText(f"In text: {shown}")
else:
self._wrap_font_detect_label.setText("No \\f[N] in text — Apply adds \\f at start")
def _apply_wrap_font_size(self):
if not self._wrap_text_editor.isEnabled():
QMessageBox.information(self, "Fix wrapping", "Select a search result first.")
return
text = self._wrap_text_editor.toPlainText()
size = self._wrap_font_spin.value()
new_text, count = wolf_codes.apply_font_size(text, size)
if new_text == text:
self._wrap_status_label.setText("No font size change applied.")
return
self._wrap_text_editor.setPlainText(new_text)
if self._current_wrap_hit_id and self._current_wrap_path and self._current_wrap_doc:
line = wolf_ws.locate_line(self._current_wrap_doc, self._current_wrap_hit_id)
if line is not None:
line["text"] = new_text
wolf_ws.save_document(self._current_wrap_path, self._current_wrap_doc)
self._sync_wrap_font_controls(new_text)
self._update_wrap_preview()
self._wrap_status_label.setText(
f"Set \\f[{size}] on {count} code(s). Save or inject when ready."
)
self._log(f"✅ Applied \\f[{size}] to wrap editor text ({count} change(s)).")
def _save_wrap_row_text(self):
if not self._current_wrap_hit_id or not self._current_wrap_path or not self._current_wrap_doc:
QMessageBox.information(self, "Fix wrapping", "Select a search result first.")
@ -3144,10 +3203,10 @@ class WolfWorkflowTab(QWidget):
def _wrap_current_sheet(self):
hit = self._current_wrap_hit
if not hit or hit.kind != "db":
if not hit or hit.kind not in ("db", "names"):
QMessageBox.information(
self, "Fix wrapping",
"Select a database sheet result first (not map dialogue).",
"Select a database sheet or names.json category result first.",
)
return
tdir, fdir = self._translated_and_files_dirs()
@ -3159,10 +3218,13 @@ class WolfWorkflowTab(QWidget):
return
doc = json.loads(path.read_text(encoding="utf-8-sig"))
width = self._wrap_width_spin.value()
count = wolf_ws.wrap_overflow_in_sheet(path, doc, hit.sheet_name, width)
count = wolf_ws.wrap_overflow_in_sheet(
path, doc, hit.sheet_name, width, kind=hit.kind,
)
self._remember_sheet_width(hit.sheet_name, width, hit.json_file)
self._refresh_wrap_sheets_list()
msg = f"Wrapped {count} overflowing line(s) in {hit.sheet_name} at width {width}."
label = "category" if hit.kind == "names" else "sheet"
msg = f"Wrapped {count} overflowing line(s) in {label} {hit.sheet_name} at width {width}."
self._wrap_status_label.setText(msg + f" Inject {hit.json_file}.")
self._log(f"{msg}")
@ -3183,22 +3245,28 @@ class WolfWorkflowTab(QWidget):
summaries = wolf_ws.sheet_overflow_summaries(tdir, width, files_dir=fdir)
self._wrap_sheets_list.clear()
if not summaries:
item = QListWidgetItem("No database JSON — translate DB sheets first.")
item = QListWidgetItem("No database or names.json data found.")
item.setFlags(Qt.ItemIsEnabled)
self._wrap_sheets_list.addItem(item)
return
for s in sorted(summaries, key=lambda x: (-x.overflow_count, x.sheet_name)):
for s in sorted(
summaries,
key=lambda x: (x.kind != "names", -x.overflow_count, x.sheet_name),
):
kind_tag = "names" if s.kind == "names" else "DB"
flag = f"{s.overflow_count} overflow" if s.overflow_count else " ok"
item = QListWidgetItem(
f"{s.sheet_name} ({s.line_count} lines){flag}{s.json_file}"
f"[{kind_tag}] {s.sheet_name} ({s.line_count} lines){flag}{s.json_file}"
)
item.setData(Qt.UserRole, s.sheet_name)
item.setData(Qt.UserRole + 1, s.json_file)
item.setData(Qt.UserRole + 2, s.kind)
self._wrap_sheets_list.addItem(item)
def _on_wrap_sheet_double_clicked(self, item: QListWidgetItem):
sheet = item.data(Qt.UserRole)
jf = item.data(Qt.UserRole + 1)
kind = item.data(Qt.UserRole + 2) or "db"
if not sheet or not jf:
return
tdir, fdir = self._translated_and_files_dirs()
@ -3209,6 +3277,26 @@ class WolfWorkflowTab(QWidget):
if not path.is_file():
return
doc = json.loads(path.read_text(encoding="utf-8-sig"))
if kind == "names":
for idx, entry in enumerate(doc.get("names") or []):
if not isinstance(entry, dict):
continue
if str(entry.get("note") or "") != sheet:
continue
text = entry.get("text")
if isinstance(text, str) and wolf_selwrap.line_needs_wrap(text, width):
hit = wolf_ws.WrapHit(
json_file=str(jf),
kind="names",
sheet_name=str(sheet),
row=idx,
field_name=str(entry.get("source") or "")[:80] or f"entry {idx}",
text=str(text),
max_line_len=dazedwrap.max_line_visible_length(str(text)),
)
self._show_wrap_hit_in_editor(hit, width)
return
return
for group in doc.get("groups") or []:
if str(group.get("typeName") or "") != sheet:
continue
@ -3224,15 +3312,18 @@ class WolfWorkflowTab(QWidget):
text=str(text),
max_line_len=dazedwrap.max_line_visible_length(str(text)),
)
self._wrap_search_edit.setText(str(text)[:40])
self._wrap_results_list.clear()
item_r = QListWidgetItem(hit.summary(width))
item_r.setData(Qt.UserRole, hit.hit_id)
item_r.setData(Qt.UserRole + 1, hit)
self._wrap_results_list.addItem(item_r)
self._wrap_results_list.setCurrentRow(0)
self._show_wrap_hit_in_editor(hit, width)
return
def _show_wrap_hit_in_editor(self, hit: wolf_ws.WrapHit, width: int):
self._wrap_search_edit.setText(str(hit.text)[:40])
self._wrap_results_list.clear()
item_r = QListWidgetItem(hit.summary(width))
item_r.setData(Qt.UserRole, hit.hit_id)
item_r.setData(Qt.UserRole + 1, hit)
self._wrap_results_list.addItem(item_r)
self._wrap_results_list.setCurrentRow(0)
def _apply_wolf_wrap_config(self):
enabled = self._wolf_wrap_cb.isChecked() if hasattr(self, "_wolf_wrap_cb") else False
width = self._wolf_wrap_width_spin.value() if hasattr(self, "_wolf_wrap_width_spin") else 55

View file

@ -59,6 +59,20 @@ class WolfCodesRepairTests(unittest.TestCase):
"Fortune-teller\nHa!\\^",
)
def test_apply_font_size_replaces_existing_codes(self):
text = r"Go to the \c[21]\f[20]tavern\c[19]\f[18]!"
new_text, count = wolf_codes.apply_font_size(text, 16)
self.assertEqual(count, 2)
self.assertIn(r"\f[16]", new_text)
self.assertNotIn(r"\f[20]", new_text)
self.assertNotIn(r"\f[18]", new_text)
def test_apply_font_size_prepends_when_missing(self):
text = "Plain text without font codes"
new_text, count = wolf_codes.apply_font_size(text, 18)
self.assertEqual(count, 1)
self.assertTrue(new_text.startswith(r"\f[18]"))
if __name__ == "__main__":
unittest.main()

View file

@ -214,5 +214,54 @@ class TestWrapPreview(unittest.TestCase):
self.assertLessEqual(info["longest"], 30 + 10)
class TestNamesCategoryWrap(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.translated = Path(self.tmp.name) / "translated"
self.translated.mkdir()
self.category = "├■街の噂MOB"
fixture = {
"kind": "names",
"count": 2,
"names": [
{
"source": "短い",
"text": "Short line.",
"note": self.category,
},
{
"source": "長い",
"text": (
"This is a very long English rumor that should definitely "
"wrap when the width is set to thirty-four characters."
),
"note": self.category,
},
],
}
self.path = self.translated / "names.json"
self.path.write_text(json.dumps(fixture, ensure_ascii=False, indent=4), encoding="utf-8")
def tearDown(self):
self.tmp.cleanup()
def test_names_categories_in_sheet_summaries(self):
summaries = ws.sheet_overflow_summaries(self.translated, width=34)
names_rows = [s for s in summaries if s.kind == "names" and s.sheet_name == self.category]
self.assertEqual(len(names_rows), 1)
self.assertEqual(names_rows[0].line_count, 2)
self.assertEqual(names_rows[0].overflow_count, 1)
def test_wrap_all_in_names_category(self):
doc = json.loads(self.path.read_text(encoding="utf-8"))
changed = ws.wrap_overflow_in_sheet(
self.path, doc, self.category, 34, kind="names",
)
self.assertEqual(changed, 1)
doc2 = json.loads(self.path.read_text(encoding="utf-8"))
long_entry = doc2["names"][1]["text"]
self.assertIn("\n", long_entry)
if __name__ == "__main__":
unittest.main()

View file

@ -29,6 +29,40 @@ WOLF_INLINE_CODE_RE = re.compile(
# Placeholder transport (same convention as util.translation.protect_script_codes).
_PLACEHOLDER_PREFIX = "__WOLF_CODE_"
# Wolf font-size codes: ``\f[18]``, ``\f[20]``, etc.
WOLF_FONT_SIZE_RE = re.compile(r"\\f\[(\d+)\]")
def detect_font_sizes(text: str) -> list[int]:
"""Return sorted unique ``\\f[N]`` sizes in *text*."""
if not isinstance(text, str) or not text:
return []
sizes: set[int] = set()
for match in WOLF_FONT_SIZE_RE.finditer(text):
try:
sizes.add(int(match.group(1)))
except (TypeError, ValueError):
continue
return sorted(sizes)
def apply_font_size(text: str, size: int) -> tuple[str, int]:
"""Replace every ``\\f[N]`` in *text* with ``\\f[size]``.
Returns ``(new_text, replacements)``. When no ``\\f[N]`` codes exist, prepends
``\\f[size]`` at the start of the string (after an optional opening quote).
"""
if not isinstance(text, str) or not text.strip() or size <= 0:
return text, 0
size = int(size)
matches = list(WOLF_FONT_SIZE_RE.finditer(text))
if matches:
new_text = WOLF_FONT_SIZE_RE.sub(rf"\\f[{size}]", text)
return new_text, len(matches)
if text.startswith('"'):
return rf'\f[{size}]' + text, 1
return rf"\f[{size}]{text}", 1
def _tokenize(rest: str) -> list[tuple[str, str]]:
"""Split *rest* into ``('lit', ...)`` and ``('code', ...)`` tokens."""

View file

@ -92,9 +92,12 @@ class SheetOverflowSummary:
line_count: int
overflow_count: int
tier: str = ""
kind: str = "db"
@property
def key(self) -> str:
if self.kind == "names":
return f"{self.json_file}|names|{self.sheet_name}"
return group_key(self.json_file, self.sheet_name)
@ -510,8 +513,13 @@ def wrap_overflow_in_sheet(
doc: dict[str, Any],
sheet_name: str,
width: int,
*,
kind: str | None = None,
) -> int:
"""Wrap every overflowing line in one DB sheet; return count changed."""
"""Wrap every overflowing line in one DB sheet or names.json category."""
doc_kind = kind or doc.get("kind")
if doc_kind == "names":
return _wrap_overflow_in_names_category(path, doc, sheet_name, width)
if doc.get("kind") != "db":
return 0
changed = 0
@ -531,6 +539,62 @@ def wrap_overflow_in_sheet(
return changed
def _wrap_overflow_in_names_category(
path: Path,
doc: dict[str, Any],
category: str,
width: int,
) -> int:
"""Wrap overflowing ``names[]`` entries sharing one ``note`` (category)."""
if doc.get("kind") != "names":
return 0
changed = 0
for entry in doc.get("names") or []:
if not isinstance(entry, dict):
continue
if str(entry.get("note") or "") != category:
continue
text = entry.get("text")
if not isinstance(text, str) or not text.strip():
continue
if not line_needs_wrap(text, width):
continue
if wrap_line_in_doc(entry, width):
changed += 1
if changed:
save_document(path, doc)
return changed
def _names_category_summaries(
doc: dict[str, Any],
*,
json_file: str,
width: int,
) -> list[SheetOverflowSummary]:
counts: dict[str, int] = {}
overflow: dict[str, int] = {}
for entry in doc.get("names") or []:
if not isinstance(entry, dict):
continue
note = str(entry.get("note") or "names.json")
counts[note] = counts.get(note, 0) + 1
text = entry.get("text")
if isinstance(text, str) and line_needs_wrap(text, width):
overflow[note] = overflow.get(note, 0) + 1
return [
SheetOverflowSummary(
json_file=json_file,
sheet_name=note,
line_count=counts[note],
overflow_count=overflow.get(note, 0),
tier="names",
kind="names",
)
for note in sorted(counts)
]
def sheet_overflow_summaries(
translated_dir: str | Path,
width: int,
@ -569,15 +633,27 @@ def sheet_overflow_summaries(
tier=group.tier,
)
)
if summaries:
return summaries
return [
SheetOverflowSummary(
json_file=g.json_file,
sheet_name=g.type_name,
line_count=g.line_count,
overflow_count=0,
tier=g.tier,
)
for g in dist.groups
]
if not summaries:
summaries = [
SheetOverflowSummary(
json_file=g.json_file,
sheet_name=g.type_name,
line_count=g.line_count,
overflow_count=0,
tier=g.tier,
)
for g in dist.groups
]
seen_names: set[Path] = set()
for search_base in _iter_search_dirs(base, Path(files_dir) if files_dir else None):
names_path = search_base / "names.json"
if not names_path.is_file() or names_path in seen_names:
continue
seen_names.add(names_path)
doc = _load_json(names_path)
if doc and doc.get("kind") == "names":
summaries.extend(
_names_category_summaries(doc, json_file="names.json", width=width)
)
return summaries