diff --git a/gui/wolf_workflow_tab.py b/gui/wolf_workflow_tab.py index eb5f7b6..f68543c 100644 --- a/gui/wolf_workflow_tab.py +++ b/gui/wolf_workflow_tab.py @@ -3015,7 +3015,7 @@ class WolfWorkflowTab(QWidget): scope = ( wolf_ws.scope_stats(doc, hit, width) if doc is not None - else wolf_ws.ScopeStats(label=wolf_ws.scope_label(hit), total=0, overflow=0) + else wolf_ws.ScopeStats(label=wolf_ws.scope_label(hit, doc), total=0, overflow=0) ) overflow_note = ( f" · {scope.overflow} of {scope.total} lines overflow at width {width}" @@ -3380,7 +3380,7 @@ class WolfWorkflowTab(QWidget): width=width, font=font, ) - scope = wolf_ws.scope_label(hit) + scope = wolf_ws.scope_label(hit, self._current_wrap_doc) font_note = ( "fonts unchanged" if font is None @@ -3427,7 +3427,7 @@ class WolfWorkflowTab(QWidget): self._remember_sheet_width( hit.sheet_name, width, hit.json_file, max_lines=max_lines or None ) - scope = wolf_ws.scope_label(hit) + scope = wolf_ws.scope_label(hit, self._current_wrap_doc) if max_lines > 0: msg = ( f"Relayout {count} line(s) in {scope} " diff --git a/tests/test_wolf_wrap_search.py b/tests/test_wolf_wrap_search.py index beb7100..931f7c2 100644 --- a/tests/test_wolf_wrap_search.py +++ b/tests/test_wolf_wrap_search.py @@ -243,25 +243,34 @@ class TestManualFontAndWrap(unittest.TestCase): class TestEventScopeWrap(unittest.TestCase): - def test_wrap_all_in_map_file(self): + def test_wrap_all_in_map_scene_only(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "Map001.mps.json" + long_a = ( + "This is a very long line of dialogue that " + "should wrap at thirty four visible chars." + ) + long_b = ( + "Another very long line of dialogue that also " + "should wrap at thirty four visible chars." + ) doc = { "kind": "map", "file": "Map001.mps", "scenes": [ { "event": 1, + "name": "Intro", "lines": [ - { - "text": ( - "This is a very long line of dialogue that " - "should wrap at thirty four visible chars." - ), - }, + {"text": long_a}, {"text": "Short line."}, ], - } + }, + { + "event": 2, + "name": "Other", + "lines": [{"text": long_b}], + }, ], } path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8") @@ -270,15 +279,22 @@ class TestEventScopeWrap(unittest.TestCase): kind="map", sheet_name="Map001.mps", row=1, - field_name="scene 0 line 0", + field_name="UI", text="very long line", max_line_len=80, scene_index=0, line_index=0, map_file="Map001.mps", ) - self.assertEqual(ws.scope_stats(doc, hit, 34).overflow, 1) + stats = ws.scope_stats(doc, hit, 34) + self.assertEqual(stats.total, 2) + self.assertEqual(stats.overflow, 1) + self.assertIn("event 1", stats.label) + self.assertIn("Intro", stats.label) self.assertEqual(ws.wrap_overflow_in_scope(path, doc, hit, 34), 1) + # Scene 2 must stay untouched. + self.assertEqual(doc["scenes"][1]["lines"][0]["text"], long_b) + self.assertNotEqual(doc["scenes"][0]["lines"][0]["text"], long_a) class TestFitTextToBox(unittest.TestCase): diff --git a/util/wolfdawn/wrap_search.py b/util/wolfdawn/wrap_search.py index ce91e7c..877af81 100644 --- a/util/wolfdawn/wrap_search.py +++ b/util/wolfdawn/wrap_search.py @@ -820,18 +820,16 @@ def wrap_overflow_manual_in_scope( font: int | None = None, ) -> int: """Manual wrap (+ optional proportional font) for every line in *hit*'s group.""" - if isinstance(hit, WrapHit): - kind, sheet_name = hit.kind, hit.sheet_name - else: - kind = str(hit.get("kind") or doc.get("kind") or "") - sheet_name = str(hit.get("sheet_name") or "") + kind, sheet_name = _hit_kind_and_sheet(hit, doc) if kind == "names": return _wrap_manual_in_names_category(path, doc, sheet_name, width, font=font) if kind == "db": return _wrap_manual_in_db_sheet(path, doc, sheet_name, width, font=font) if kind in ("map", "common"): - return _wrap_manual_in_event_file(path, doc, width, font=font) + return _wrap_manual_in_event_scene( + path, doc, _hit_scene_index(hit), width, font=font + ) if kind == "gamedat": return _wrap_manual_in_gamedat(path, doc, width, font=font) return 0 @@ -900,20 +898,24 @@ def _wrap_manual_in_db_sheet( return changed -def _wrap_manual_in_event_file( +def _wrap_manual_in_event_scene( path: Path, doc: dict[str, Any], + scene_index: int | None, width: int, *, font: int | None = None, ) -> int: + """Manual-wrap overflowing lines in one map/CommonEvent scene.""" if doc.get("kind") not in ("map", "common"): return 0 + scenes = doc.get("scenes") or [] + if scene_index is None or not (0 <= scene_index < len(scenes)): + return 0 changed = 0 - for scene in doc.get("scenes") or []: - for line in scene.get("lines") or []: - if _apply_manual_to_line_dict(line, width, font=font): - changed += 1 + for line in scenes[scene_index].get("lines") or []: + if _apply_manual_to_line_dict(line, width, font=font): + changed += 1 if changed: save_document(path, doc) return changed @@ -946,13 +948,66 @@ class ScopeStats: overflow: int -def scope_label(hit: WrapHit | dict[str, Any]) -> str: - """Human-readable group name for status messages.""" +def _hit_kind_and_sheet(hit: WrapHit | dict[str, Any], doc: dict[str, Any] | None = None) -> tuple[str, str]: if isinstance(hit, WrapHit): - kind, sheet, jf = hit.kind, hit.sheet_name, hit.json_file + return hit.kind, hit.sheet_name + kind = str(hit.get("kind") or (doc or {}).get("kind") or "") + sheet = str(hit.get("sheet_name") or "") + return kind, sheet + + +def _hit_scene_index(hit: WrapHit | dict[str, Any]) -> int | None: + if isinstance(hit, WrapHit): + return hit.scene_index + raw = hit.get("scene_index") + if raw is None: + return None + try: + return int(raw) + except (TypeError, ValueError): + return None + + +def _scene_group_label(doc: dict[str, Any] | None, hit: WrapHit | dict[str, Any]) -> str: + """Label for one map/CommonEvent scene (the wrap-group unit).""" + kind, sheet = _hit_kind_and_sheet(hit, doc) + si = _hit_scene_index(hit) + scene: dict[str, Any] | None = None + if doc is not None and si is not None: + scenes = doc.get("scenes") or [] + if 0 <= si < len(scenes) and isinstance(scenes[si], dict): + scene = scenes[si] + event_id = None + name = "" + if scene is not None: + event_id = scene.get("event") + name = str(scene.get("name") or "").strip() + elif isinstance(hit, WrapHit) and hit.row is not None: + event_id = hit.row + elif isinstance(hit, dict) and hit.get("row") is not None: + event_id = hit.get("row") + + parts: list[str] = [] + if kind == "common": + parts.append("CommonEvent") + elif sheet: + parts.append(sheet) + if event_id is not None: + parts.append(f"event {event_id}") + if name: + parts.append(name) + elif si is not None and event_id is None: + parts.append(f"scene {si}") + return " · ".join(parts) if parts else (sheet or "event") + + +def scope_label(hit: WrapHit | dict[str, Any], doc: dict[str, Any] | None = None) -> str: + """Human-readable group name for status messages.""" + kind, sheet = _hit_kind_and_sheet(hit, doc) + jf = "" + if isinstance(hit, WrapHit): + jf = hit.json_file else: - kind = str(hit.get("kind") or "") - sheet = str(hit.get("sheet_name") or "") jf = str(hit.get("json_file") or "") if kind == "db": return f"sheet {sheet}" @@ -960,10 +1015,8 @@ def scope_label(hit: WrapHit | dict[str, Any]) -> str: return f"category {sheet}" if kind == "gamedat": return "Game.dat" - if kind == "common": - return "CommonEvent" - if kind == "map": - return sheet or jf + if kind in ("map", "common"): + return _scene_group_label(doc, hit) return sheet or jf @@ -973,11 +1026,7 @@ def scope_stats( width: int, ) -> ScopeStats: """Count lines and overflows in the same group as *hit*.""" - if isinstance(hit, WrapHit): - kind, sheet_name = hit.kind, hit.sheet_name - else: - kind = str(hit.get("kind") or doc.get("kind") or "") - sheet_name = str(hit.get("sheet_name") or "") + kind, sheet_name = _hit_kind_and_sheet(hit, doc) total = 0 overflow = 0 @@ -1007,8 +1056,15 @@ def scope_stats( if line_needs_wrap(text, width): overflow += 1 elif kind in ("map", "common"): - for scene in doc.get("scenes") or []: - for line in scene.get("lines") or []: + si = _hit_scene_index(hit) + scenes = doc.get("scenes") or [] + if si is not None and 0 <= si < len(scenes): + scene_iter = [scenes[si]] + else: + # No scene on the hit: fall back to whole file (legacy). + scene_iter = scenes + for scene in scene_iter: + for line in (scene.get("lines") or []): text = line.get("text") if not isinstance(text, str) or not text.strip(): continue @@ -1026,7 +1082,7 @@ def scope_stats( if line_needs_wrap(text, width): overflow += 1 - return ScopeStats(label=scope_label(hit), total=total, overflow=overflow) + return ScopeStats(label=scope_label(hit, doc), total=total, overflow=overflow) def _line_needs_relayout(text: str, width: int, max_lines: int | None) -> bool: @@ -1040,26 +1096,29 @@ def _line_needs_relayout(text: str, width: int, max_lines: int | None) -> bool: return count_soft_lines(wrapped) > limit or wrapped != text -def _wrap_overflow_in_event_file( +def _wrap_overflow_in_event_scene( path: Path, doc: dict[str, Any], + scene_index: int | None, width: int, *, max_lines: int | None = None, ) -> int: - """Wrap overflowing dialogue lines in one map or CommonEvent JSON file.""" + """Wrap overflowing dialogue lines in one map or CommonEvent scene.""" if doc.get("kind") not in ("map", "common"): return 0 + scenes = doc.get("scenes") or [] + if scene_index is None or not (0 <= scene_index < len(scenes)): + return 0 changed = 0 - for scene in doc.get("scenes") or []: - for line in scene.get("lines") or []: - text = line.get("text") - if not isinstance(text, str) or not text.strip(): - continue - if not _line_needs_relayout(text, width, max_lines): - continue - if apply_relayout_to_line_dict(line, width, max_lines=max_lines): - changed += 1 + for line in scenes[scene_index].get("lines") or []: + text = line.get("text") + if not isinstance(text, str) or not text.strip(): + continue + if not _line_needs_relayout(text, width, max_lines): + continue + if apply_relayout_to_line_dict(line, width, max_lines=max_lines): + changed += 1 if changed: save_document(path, doc) return changed @@ -1117,7 +1176,9 @@ def wrap_overflow_in_scope( path, doc, sheet_name, width, kind="db", max_lines=max_lines ) if kind in ("map", "common"): - return _wrap_overflow_in_event_file(path, doc, width, max_lines=max_lines) + return _wrap_overflow_in_event_scene( + path, doc, _hit_scene_index(hit), width, max_lines=max_lines + ) if kind == "gamedat": return _wrap_overflow_in_gamedat(path, doc, width, max_lines=max_lines) return 0