diff --git a/data/help/03-workflow-rpg.md b/data/help/03-workflow-rpg.md index 850ab7b..1e8ff29 100644 --- a/data/help/03-workflow-rpg.md +++ b/data/help/03-workflow-rpg.md @@ -14,7 +14,8 @@ This page is the overview. | **3 TL Phase 1** | Phase 0 DB → Phase 1 dialogue → Phase 1b code 111 cache | | **4 TL Phase 2** | Risky codes (122, plugins, etc.) after you audit | | **5 Export** | MV/MZ: `plugins.js` helpers. Ace: Ruby `.rb` scripts. Then export / pack | -| **6 Playtest** | TL Inspector / Forge (MV/MZ; hidden for Ace) | +| **6 Images** | Check image setup, then decrypt → AI translate → review → patch (MV/MZ) | +| **7 Playtest** | TL Inspector / Forge (MV/MZ; hidden for Ace) | ## Example: first RPG Maker run @@ -42,6 +43,24 @@ Project Setup's `speakers` block recommends ENABLE / SKIP with evidence. - Use **Normal** mode when you want live iteration on a handful of files. - Phase 2 can break games if you translate logic keys - audit with the Plugin Prompt first. +## Images (Step 6, MV/MZ) + +Step 6 keeps the Image Manager on the separate **Images** page, but verifies that you are ready +to use it: + +- Step 0 points to the actual game root containing `img/` or `www/img/`. +- Encrypted projects have a valid key in `System.json`. +- `/vocab.txt` exists for the copied AI skill. +- Editable PNGs are under `.dazedtl/images//img/...`. +- No PNGs were accidentally placed beside that expected `img/` tree. + +Click **Open Images**, decrypt the images you want to translate, then click **Copy skill** in the +Image Manager and paste it into Codex, Cursor, Copilot, or a similar coding agent. Review the +resulting PNGs before using **Patch selected** or **Patch all**. + +Do not edit the runtime images directly. The Image Manager maintains editable copies and patches +the reviewed results back into the game. + ## Ace note (Ruby scripts, not plugins.js) Ace has no `plugins.js`. UI / menu strings often live in **Ruby scripts** packed inside `Data/Scripts.rvdata2`. diff --git a/data/help/05-other-tabs.md b/data/help/05-other-tabs.md index 31803cc..1ce374c 100644 --- a/data/help/05-other-tabs.md +++ b/data/help/05-other-tabs.md @@ -16,6 +16,18 @@ Workflow jumps here when you start a phase. **Example:** check only `Map003.json`, module = RPG Maker MV/MZ, click Translate, then export from Workflow Step 5 (or copy from `translated/` yourself). +## Images + +MV/MZ bitmap UI workflow. Workflow Step 6 checks the project setup and opens this page. + +1. Decrypt selected images (or all images) into `.dazedtl/images/.../img/`. +2. Click **Copy skill** and paste the scoped bitmap-localization instructions into your coding + agent. +3. Review the edited PNGs here. +4. Patch highlighted images, or clear highlights to patch every editable PNG. + +The runtime game images remain untouched until you deliberately patch the reviewed copies. + ## Batches History for Anthropic Message Batch jobs. @@ -32,6 +44,7 @@ Edit tool-level markdown: - `data/skills/wrap_config.md` - RPG Maker wrap-width analysis prompt - `data/skills/plugin_translation.md` - MV/MZ plugin translation prompt - `data/skills/ace_script_translation.md` - VX Ace Ruby script translation prompt +- `data/skills/image_translation.md` - scoped bitmap UI translation skill copied from Images - `data/skills/risky_codes.md` - RPG Maker optional event-code audit prompt - `data/skills/wolf_speakers.md` - WOLF speaker-format audit prompt - `data/translation_contexts.json` - per-call history templates diff --git a/data/help/06-examples.md b/data/help/06-examples.md index 96a4e5f..8370e08 100644 --- a/data/help/06-examples.md +++ b/data/help/06-examples.md @@ -63,7 +63,8 @@ When the sample map looks good: 1. Step 4 - copy the Plugin / risky-codes prompt, audit the game in your IDE, enable only codes that hold player-visible text, then run Phase 2 carefully. 2. Step 5 - for MV/MZ, copy vocab + the plugins.js prompt and edit `plugins.js` in the IDE (player-facing strings only). For **Ace**, edit `ace_json/scripts/*.rb` the same way, then Export so **RV2JSON** packs `Scripts.rvdata2`. -3. Step 6 (MV/MZ) - install TL Inspector / Forge if you want in-game inspection helpers. +3. Step 6 (MV/MZ) - check image setup, then decrypt / translate / patch any bitmap UI text. +4. Step 7 (MV/MZ) - install TL Inspector / Forge if you want in-game inspection helpers. ### What “done enough” looks like diff --git a/gui/rpgmaker_image_manager.py b/gui/rpgmaker_image_manager.py index 9e67dc9..1e2d327 100644 --- a/gui/rpgmaker_image_manager.py +++ b/gui/rpgmaker_image_manager.py @@ -5,6 +5,7 @@ from __future__ import annotations from pathlib import Path from PyQt5.QtCore import ( + QPoint, Qt, QSize, QThread, @@ -23,6 +24,7 @@ from PyQt5.QtWidgets import ( QLineEdit, QListWidget, QListWidgetItem, + QListView, QMessageBox, QPushButton, QSizePolicy, @@ -55,6 +57,87 @@ _ASSET_ID_ROLE = Qt.UserRole + 1 _PAGE_SIZE = 1000 +class _BoundedComboBox(QComboBox): + """Keep large combo popups on-screen and scrollable.""" + + _MAX_VISIBLE_ROWS = 14 + + def __init__(self, parent=None): + super().__init__(parent) + view = QListView(self) + view.setVerticalScrollMode(QAbstractItemView.ScrollPerItem) + view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + view.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + view.setTextElideMode(Qt.ElideMiddle) + view.setUniformItemSizes(True) + view.setStyleSheet( + "QListView {" + " background: #404040; color: #f2f2f2;" + " border: 1px solid #5a5a5a; outline: none; padding: 3px;" + "}" + "QListView::item { min-height: 30px; padding: 3px 8px; }" + "QListView::item:selected { background: #087dcc; color: white; }" + "QScrollBar:vertical { background: #303030; width: 12px; margin: 0; }" + "QScrollBar::handle:vertical {" + " background: #777; min-height: 28px; border-radius: 5px;" + "}" + "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {" + " height: 0;" + "}" + ) + self.setView(view) + self.setMaxVisibleItems(self._MAX_VISIBLE_ROWS) + + def showPopup(self) -> None: + self.view().setVerticalScrollBarPolicy( + Qt.ScrollBarAlwaysOn + if self.count() > self._MAX_VISIBLE_ROWS + else Qt.ScrollBarAsNeeded + ) + super().showPopup() + view = self.view() + popup = view.window() + screen = QApplication.screenAt(self.mapToGlobal(self.rect().center())) + screen = screen or QApplication.primaryScreen() + if screen is None: + return + + available = screen.availableGeometry() + row_height = view.sizeHintForRow(0) + if row_height <= 0: + row_height = self.fontMetrics().height() + 10 + visible_rows = min(max(1, self.count()), self._MAX_VISIBLE_ROWS) + height_cap = min( + row_height * visible_rows + view.frameWidth() * 2 + 8, + max(row_height * 4, int(available.height() * 0.6)), + ) + popup_height = min(popup.height(), height_cap) + popup_width = min(max(popup.width(), self.width()), available.width()) + + combo_top_left = self.mapToGlobal(QPoint(0, 0)) + below_y = combo_top_left.y() + self.height() + above_y = combo_top_left.y() - popup_height + if below_y + popup_height <= available.bottom() + 1: + popup_y = below_y + elif above_y >= available.top(): + popup_y = above_y + else: + popup_y = max( + available.top(), + min(popup.y(), available.bottom() - popup_height + 1), + ) + popup_x = max( + available.left(), + min(combo_top_left.x(), available.right() - popup_width + 1), + ) + popup.setGeometry(popup_x, popup_y, popup_width, popup_height) + if self.currentIndex() >= 0: + view.scrollTo( + self.model().index(self.currentIndex(), self.modelColumn()), + QAbstractItemView.PositionAtCenter, + ) + + class _UserSelectionList(QListWidget): """Report user selection changes without reacting to list rebuilding.""" @@ -228,7 +311,7 @@ class RPGMakerImageManager(QWidget): self.search_edit.setPlaceholderText("Filter by any part of the folder or filename…") self.search_edit.textChanged.connect(self._apply_filters) filters.addWidget(self.search_edit, 2) - self.folder_combo = QComboBox() + self.folder_combo = _BoundedComboBox() self.folder_combo.addItem("All folders", "") self.folder_combo.currentIndexChanged.connect(self._apply_filters) filters.addWidget(self.folder_combo, 1) diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 3bf0cbf..92f77a2 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -9,7 +9,8 @@ Provides a guided, step-by-step interface: Step 3 – Translation: Phase 0 (DB), Phase 1 (dialogue), Phase 1b (111 cache) Step 4 – Translation Phase 2 (risky codes) Step 5 – Plugins.js prompt helpers + export translated/ to the game - Step 6 – Install TL Inspector and/or Forge playtest plugins + Step 6 – Prepare and translate editable bitmap UI images + Step 7 – Install TL Inspector and/or Forge playtest plugins """ from __future__ import annotations @@ -497,7 +498,19 @@ _STEP_HELP: dict[int, str] = { "• Export ALL - everything under translated/" ), 6: ( - "Step 6 - Playtest

" + "Step 6 - Images (MV/MZ)

" + "Use the existing Images page for bitmap UI translation:
" + "• Confirm this step reports the correct game root, image tree, encryption key, and " + "vocab.txt
" + "• Open Images and decrypt the images you want to edit
" + "• Click Copy skill, paste it into Codex/Cursor/Copilot, and let the agent edit " + "only the generated .dazedtl/images/.../img copies
" + "• Review the results, then use Patch selected or Patch all

" + "Editable PNGs must preserve the same img/... hierarchy as the game. " + "This step warns when PNGs were placed elsewhere in the workspace." + ), + 7: ( + "Step 7 - Playtest

" "Install playtest plugins into the MV/MZ game:
" "• TL Inspector - inspect translated text in-game
" "• Forge - additional playtest helpers

" @@ -634,6 +647,67 @@ def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton: return btn +def _inspect_image_workflow(game_root: str | Path) -> dict: + """Return lightweight MV/MZ image-workflow readiness details.""" + root = Path(game_root).expanduser().resolve() + report = { + "root": root, + "ok": False, + "error": "", + "runtime": 0, + "encrypted": 0, + "editable": 0, + "misplaced": 0, + "vocab": root / "vocab.txt", + "editable_root": None, + "key_ok": None, + } + try: + from util.rpgmaker_images import ( + editable_workspace_root, + read_encryption_key, + resolve_content_root, + scan_image_assets, + ) + + content_root = resolve_content_root(root) + workspace = editable_workspace_root(root) + expected_root = workspace / content_root.relative_to(root) / "img" + assets = scan_image_assets(root) + encrypted = sum(asset.has_encrypted for asset in assets) + report.update( + { + "ok": True, + "runtime": sum( + asset.has_encrypted or asset.has_runtime_plain for asset in assets + ), + "encrypted": encrypted, + "editable": sum(asset.has_plain for asset in assets), + "editable_root": expected_root, + } + ) + if encrypted: + try: + read_encryption_key(root) + report["key_ok"] = True + except Exception: + report["key_ok"] = False + + if workspace.is_dir(): + misplaced = 0 + for path in workspace.rglob("*"): + if not path.is_file() or path.suffix.casefold() != ".png": + continue + try: + path.relative_to(expected_root) + except ValueError: + misplaced += 1 + report["misplaced"] = misplaced + except Exception as exc: + report["error"] = str(exc) + return report + + # ───────────────────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────────────────── @@ -752,7 +826,8 @@ class WorkflowTab(QWidget): ("3 TL Phase 1", self._build_step4_translation), ("4 TL Phase 2", self._build_step5_tl_phase2), ("5 Export", self._build_step5_finish), - ("6 Playtest", self._build_step8_playtest), + ("6 Images", self._build_step6_images), + ("7 Playtest", self._build_step8_playtest), ] self._step_labels = [label for label, _ in _tab_defs] @@ -924,6 +999,7 @@ class WorkflowTab(QWidget): "Phase1", "Phase2", "Export", + "Images", "Playtest", ) name = short_names[idx] if 0 <= idx < len(short_names) else str(idx) @@ -988,6 +1064,8 @@ class WorkflowTab(QWidget): if index == 4: self._populate_p2_checkboxes() if index == 6: + self._refresh_image_workflow_status() + if index == 7: self._refresh_playtest_status() self._load_playtest_settings() @@ -2083,7 +2161,7 @@ class WorkflowTab(QWidget): ) self._step6_vocab_btn.clicked.connect(self._copy_vocab_to_game) - self._step6_copy_btn = _make_btn("📋 Copy Prompt for Copilot", "#555") + self._step6_copy_btn = _make_btn("TL Plugins Skill", "#555") self._step6_copy_btn.setToolTip( "Copy a prompt that audits plugins.js and enabled plugin sources, asks what " "needs translation, then edits approved player-visible strings in place." @@ -2115,11 +2193,93 @@ class WorkflowTab(QWidget): inner.addLayout(_labeled_row(export_lbl, export_active_btn, export_all_btn)) layout.addWidget(box, 0, Qt.AlignLeft) - # ── Step 6: Playtest (TL Inspector) ───────────────────────────────────── + # ── Step 6: Editable images ───────────────────────────────────────────── + + def _build_step6_images(self, layout: QVBoxLayout): + self._add_step_header(layout, "Step 6 — Images", 6) + + intro = QLabel( + "Translate text embedded in RPG Maker MV/MZ images without moving the Image Manager. " + "This step verifies the project setup, then sends you to the Images page." + ) + intro.setWordWrap(True) + intro.setStyleSheet("color:#b8b8b8;font-size:13px;padding-bottom:4px;") + layout.addWidget(intro) + + status_box = QWidget() + status_box.setObjectName("tbox") + status_box.setStyleSheet(self._task_box_style()) + status_layout = QVBoxLayout(status_box) + status_layout.setContentsMargins(14, 12, 14, 12) + status_layout.setSpacing(8) + + status_title = QLabel("Setup check") + status_title.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;") + status_layout.addWidget(status_title) + + self._image_workflow_status = QLabel("Open this step to check image readiness.") + self._image_workflow_status.setWordWrap(True) + self._image_workflow_status.setTextFormat(Qt.RichText) + self._image_workflow_status.setTextInteractionFlags(Qt.TextSelectableByMouse) + self._image_workflow_status.setStyleSheet( + "color:#c8c8c8;font-size:12px;line-height:1.4;" + ) + status_layout.addWidget(self._image_workflow_status) + + refresh_btn = _make_btn("↻ Refresh check", "#555") + refresh_btn.clicked.connect(self._refresh_image_workflow_status) + status_layout.addWidget(refresh_btn, 0, Qt.AlignLeft) + layout.addWidget(status_box) + + flow_box = QWidget() + flow_box.setObjectName("tbox") + flow_box.setStyleSheet(self._task_box_style()) + flow_layout = QVBoxLayout(flow_box) + flow_layout.setContentsMargins(14, 12, 14, 12) + flow_layout.setSpacing(8) + + flow_title = QLabel("Image translation flow") + flow_title.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;") + flow_layout.addWidget(flow_title) + + flow_text = QLabel( + "1. Open Images and decrypt the source images into editable PNG copies.
" + "2. Click Copy skill in the Image Manager and paste it into your coding " + "agent.
" + "3. Review the translated PNGs in the Image Manager.
" + "4. Highlight finished images and click Patch selected, or clear highlights " + "and click Patch all." + ) + flow_text.setWordWrap(True) + flow_text.setTextFormat(Qt.RichText) + flow_text.setStyleSheet("color:#c8c8c8;font-size:12px;") + flow_layout.addWidget(flow_text) + + actions = QHBoxLayout() + actions.setSpacing(8) + copy_vocab_btn = _make_btn("📄 Copy vocab.txt → Game", "#555") + copy_vocab_btn.setToolTip( + "Copy the current glossary to /vocab.txt for the image translation skill." + ) + copy_vocab_btn.clicked.connect(self._copy_vocab_to_game) + actions.addWidget(copy_vocab_btn) + + self._open_images_btn = _make_btn("🖼 Open Images", "#3a5a7a") + self._open_images_btn.setToolTip( + "Open the existing Image Manager using the Step 0 game folder." + ) + self._open_images_btn.clicked.connect(self._open_image_manager) + actions.addWidget(self._open_images_btn) + actions.addStretch() + flow_layout.addLayout(actions) + layout.addWidget(flow_box) + layout.addStretch() + + # ── Step 7: Playtest (TL Inspector) ───────────────────────────────────── def _build_step8_playtest(self, layout: QVBoxLayout): self._step8_section_label = self._add_step_header( - layout, "Step 6 — Playtest Tools", 6 + layout, "Step 7 — Playtest Tools", 7 ) settings_box = QWidget() @@ -2354,6 +2514,117 @@ class WorkflowTab(QWidget): self._populate_tli_editor_combo() self._load_playtest_settings() + def _refresh_image_workflow_status(self): + """Check that Step 0 points at an MV/MZ root ready for image work.""" + label = getattr(self, "_image_workflow_status", None) + button = getattr(self, "_open_images_btn", None) + if label is None: + return + if self._ace_encrypted or self._ace_rvdata_dir or self._ace_json_dir: + label.setText( + "⚠ Image Manager supports RPG Maker MV/MZ " + "projects only; this project is RPG Maker Ace." + ) + if button is not None: + button.setEnabled(False) + return + + game_root = self.folder_edit.text().strip() + if not game_root: + label.setText( + "⚠ Complete Step 0 and select the actual game " + "root first." + ) + if button is not None: + button.setEnabled(False) + return + + import html + + report = _inspect_image_workflow(game_root) + if not report["ok"]: + label.setText( + "✗ The Step 0 folder is not ready for image " + f"management: {html.escape(report['error'])}
" + "Select the folder that directly contains img/, or contains " + "www/img/." + ) + if button is not None: + button.setEnabled(False) + return + + root = html.escape(str(report["root"])) + editable_root = html.escape(str(report["editable_root"])) + vocab = html.escape(str(report["vocab"])) + lines = [ + f"✓ Project root: {root}", + f"✓ Runtime images: " + f"{report['runtime']:,} ({report['encrypted']:,} encrypted)", + ] + if report["key_ok"] is False: + lines.append( + "✗ Encryption key: missing or invalid in " + "System.json; encrypted images cannot be decrypted." + ) + elif report["key_ok"] is True: + lines.append("✓ Encryption key: ready") + else: + lines.append("✓ Encryption key: not required") + + if Path(report["vocab"]).is_file(): + lines.append( + f"✓ Glossary: {vocab}" + ) + else: + lines.append( + "⚠ Glossary: " + f"{vocab} is missing. Copy it before using the AI skill." + ) + + if report["editable"]: + lines.append( + f"✓ Editable PNGs: " + f"{report['editable']:,} under {editable_root}" + ) + else: + lines.append( + "• Editable PNGs: none yet. Open Images " + "and decrypt the images you want to translate." + ) + + if report["misplaced"]: + lines.append( + f"⚠ Workspace layout: " + f"{report['misplaced']:,} PNG(s) are outside {editable_root} and " + "will not appear in the Image Manager." + ) + else: + lines.append( + "✓ Workspace layout: editable PNGs use " + "the expected game-relative hierarchy." + ) + label.setText("
".join(lines)) + if button is not None: + button.setEnabled(True) + + def _open_image_manager(self): + """Open the Images page with the current Step 0 project root.""" + game_root = self.folder_edit.text().strip() + report = _inspect_image_workflow(game_root) if game_root else {"ok": False} + if not report.get("ok"): + QMessageBox.warning( + self, + "Images Setup", + "Select a valid RPG Maker MV/MZ game root in Step 0 first.", + ) + return + self._save_setting("last_game_folder", str(report["root"])) + parent = self.parent_window + if hasattr(parent, "switch_page"): + parent.switch_page(getattr(parent, "PAGE_IMAGES", 2)) + return + self._log("⚠ Could not open the Images page from this window.") + def _populate_tli_editor_combo(self, select: str | None = None): """Fill editor dropdown with auto-detect, found editors, and custom.""" from util.tl_inspector.config import detect_editors @@ -2811,28 +3082,29 @@ class WorkflowTab(QWidget): "Copy a prompt that audits plugins.js and enabled plugin sources, asks what " "needs translation, then edits approved player-visible strings in place." ) - # Step 6 - TL Inspector (MV/MZ only; hidden for Ace) - show_playtest = not is_ace - playtest_idx = 6 - if hasattr(self, "_step_tabs") and self._step_tabs.count() > playtest_idx: - if hasattr(self._step_tabs, "setTabVisible"): - self._step_tabs.setTabVisible(playtest_idx, show_playtest) - else: - self._step_tabs.setTabEnabled(playtest_idx, show_playtest) - if is_ace and self._step_tabs.currentIndex() == playtest_idx: - self._goto_step(5) - if hasattr(self, "_step_buttons") and len(self._step_buttons) > playtest_idx: - self._step_buttons[playtest_idx].setVisible(show_playtest) - self._step_buttons[playtest_idx].setEnabled(show_playtest) + # Steps 6–7 - Image Manager and playtest tools are MV/MZ only. + show_mvmz_tools = not is_ace + tool_indices = (6, 7) + for tool_idx in tool_indices: + if hasattr(self, "_step_tabs") and self._step_tabs.count() > tool_idx: + if hasattr(self._step_tabs, "setTabVisible"): + self._step_tabs.setTabVisible(tool_idx, show_mvmz_tools) + else: + self._step_tabs.setTabEnabled(tool_idx, show_mvmz_tools) + if hasattr(self, "_step_buttons") and len(self._step_buttons) > tool_idx: + self._step_buttons[tool_idx].setVisible(show_mvmz_tools) + self._step_buttons[tool_idx].setEnabled(show_mvmz_tools) + if is_ace and self._step_tabs.currentIndex() in tool_indices: + self._goto_step(5) self._refresh_step_strip() box = getattr(self, "_step8_playtest_box", None) install_both_btn = getattr(self, "_install_both_btn", None) if box is not None: - box.setVisible(not is_ace) - box.setEnabled(not is_ace) + box.setVisible(show_mvmz_tools) + box.setEnabled(show_mvmz_tools) if install_both_btn is not None: - install_both_btn.setVisible(not is_ace) - if not is_ace: + install_both_btn.setVisible(show_mvmz_tools) + if show_mvmz_tools: self._refresh_playtest_status() def _detect_folder(self): @@ -3396,7 +3668,7 @@ class WorkflowTab(QWidget): src = VOCAB_PATH if not src.exists(): - self._log("⚠ vocab.txt not found — save it in Step 3 first.") + self._log("⚠ vocab.txt not found — save it in Step 2 first.") return import shutil @@ -3404,6 +3676,7 @@ class WorkflowTab(QWidget): try: shutil.copy2(src, dst) self._log(f"✅ vocab.txt copied to {dst}") + self._refresh_image_workflow_status() except Exception as exc: self._log(f"❌ Could not copy vocab.txt: {exc}") diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index eae793a..210bf61 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -163,14 +163,14 @@ CODE122_VAR_MAX = 2000 # Plugins / Scripts CODE355655 = False -CODE357 = True +CODE357 = False CODE657 = False CODE356 = False CODE320 = False CODE324 = False CODE325 = False CODE111 = False -CODE108 = True +CODE108 = False # ─── Plugin Manager ────────────────────────────────────────────────────────── # All known code-357 headerMapping entries. Enable entries via ENABLED_PLUGINS_357. diff --git a/tests/test_rpgmaker_image_manager.py b/tests/test_rpgmaker_image_manager.py index aa8e7e9..ac57778 100644 --- a/tests/test_rpgmaker_image_manager.py +++ b/tests/test_rpgmaker_image_manager.py @@ -9,9 +9,10 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") from PIL import Image from PyQt5.QtCore import QSettings, Qt from PyQt5.QtTest import QTest -from PyQt5.QtWidgets import QApplication, QAbstractItemView, QMessageBox +from PyQt5.QtWidgets import QApplication, QAbstractItemView, QMainWindow, QMessageBox from gui.rpgmaker_image_manager import RPGMakerImageManager, _PAGE_SIZE +from gui.workflow_tab import _inspect_image_workflow class RPGMakerImageManagerSelectionTests(unittest.TestCase): @@ -251,6 +252,37 @@ class RPGMakerImageManagerSelectionTests(unittest.TestCase): self.manager.open_workspace_button.geometry().top(), ) + def test_folder_popup_is_bounded_and_scrolls_long_lists(self): + combo = self.manager.folder_combo + combo.blockSignals(True) + combo.clear() + combo.addItem("All folders", "") + for index in range(80): + combo.addItem(f"img/pictures/group_{index:03d}/nested_folder", str(index)) + combo.setCurrentIndex(combo.count() - 1) + combo.blockSignals(False) + + combo.showPopup() + self.app.processEvents() + try: + view = combo.view() + popup = view.window() + screen = QApplication.screenAt(combo.mapToGlobal(combo.rect().center())) + screen = screen or QApplication.primaryScreen() + available = screen.availableGeometry() + + self.assertLessEqual(popup.height(), int(available.height() * 0.6)) + self.assertGreater(view.verticalScrollBar().maximum(), 0) + self.assertTrue(view.verticalScrollBar().isVisible()) + self.assertGreaterEqual(popup.geometry().top(), available.top()) + self.assertLessEqual(popup.geometry().bottom(), available.bottom()) + current = combo.model().index(combo.currentIndex(), combo.modelColumn()) + current_rect = view.visualRect(current) + self.assertLess(current_rect.top(), view.viewport().height()) + self.assertGreaterEqual(current_rect.bottom(), 0) + finally: + combo.hidePopup() + def test_translation_skill_is_enabled_only_for_editable_images(self): self.assertFalse(self.manager.copy_translation_button.isEnabled()) @@ -292,6 +324,23 @@ class RPGMakerImageManagerSelectionTests(unittest.TestCase): self.manager.status_label.text(), ) + def test_workflow_readiness_detects_editable_and_misplaced_pngs(self): + asset = self.manager.assets[0] + asset.plain_path.parent.mkdir(parents=True, exist_ok=True) + asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes()) + self.game_root.joinpath("vocab.txt").write_text("Menu (Menu)\n", encoding="utf-8") + misplaced = self.game_root / ".dazedtl" / "images" / "img (2)" / "pictures" + misplaced.mkdir(parents=True) + misplaced.joinpath("menu.png").write_bytes(asset.runtime_plain_path.read_bytes()) + + report = _inspect_image_workflow(self.game_root) + + self.assertTrue(report["ok"]) + self.assertEqual(report["runtime"], 4) + self.assertEqual(report["editable"], 1) + self.assertEqual(report["misplaced"], 1) + self.assertTrue(report["vocab"].is_file()) + def test_open_folder_uses_highlighted_images_editable_parent(self): self._click(0) asset_id = self.manager.image_list.currentItem().data(Qt.UserRole + 1) @@ -350,7 +399,63 @@ class RPGMakerImageManagerNavigationTests(unittest.TestCase): self.assertIn("PAGE_IMAGES = 2", main_source) self.assertIn("self.image_manager_tab = RPGMakerImageManager", main_source) self.assertIn('create_nav_button("🖼", "Images")', main_source) - self.assertNotIn("Open Image Manager", workflow_source) + self.assertIn('(\"6 Images\", self._build_step6_images)', workflow_source) + self.assertIn('self._open_images_btn = _make_btn("🖼 Open Images"', workflow_source) + + +class RPGMakerWorkflowImageStepTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app = QApplication.instance() or QApplication([]) + + def test_images_step_checks_setup_and_opens_existing_manager(self): + from gui.workflow_tab import WorkflowTab + + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + game_root = root / "Game" + image_root = game_root / "img" / "pictures" + data_root = game_root / "data" + image_root.mkdir(parents=True) + data_root.mkdir(parents=True) + Image.new("RGBA", (12, 12), "purple").save(image_root / "menu.png") + data_root.joinpath("System.json").write_text("{}", encoding="utf-8") + game_root.joinpath("vocab.txt").write_text("Menu (Menu)\n", encoding="utf-8") + + settings = QSettings(str(root / "settings.ini"), QSettings.IniFormat) + + class Host(QMainWindow): + PAGE_IMAGES = 2 + + def __init__(self): + super().__init__() + self.switched_to = None + + def switch_page(self, index): + self.switched_to = index + + host = Host() + with patch("gui.workflow_tab.QSettings", return_value=settings): + workflow = WorkflowTab(host) + try: + workflow.folder_edit.setText(str(game_root)) + workflow._goto_step(6) + workflow._refresh_image_workflow_status() + + self.assertEqual(workflow._step_tabs.count(), 8) + self.assertEqual(workflow._step_tabs.currentIndex(), 6) + self.assertIn("Runtime images: 1", workflow._image_workflow_status.text()) + self.assertIn("Glossary:", workflow._image_workflow_status.text()) + + workflow._open_image_manager() + + self.assertEqual(host.switched_to, host.PAGE_IMAGES) + self.assertEqual( + settings.value("workflow/last_game_folder"), str(game_root.resolve()) + ) + finally: + workflow.close() + host.close() if __name__ == "__main__":