font and width
This commit is contained in:
parent
0a79b74703
commit
2b4a0e257f
5 changed files with 294 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue