feat(rpgmaker): integrate images into the guided workflow

- Add image readiness checks and navigation from workflow Step 6
- Keep long Image Manager folder lists bounded and scrollable
- Update workflow guidance, button labels, and regression coverage
- Disable code 357 and 108 translation by default
This commit is contained in:
DazedAnon 2026-07-26 11:22:09 -05:00
parent 979d0bd374
commit 56453e922b
7 changed files with 525 additions and 31 deletions

View file

@ -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`.
- `<game>/vocab.txt` exists for the copied AI skill.
- Editable PNGs are under `.dazedtl/images/<game-relative>/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`.

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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] = {
"• <b>Export ALL</b> - everything under <code>translated/</code>"
),
6: (
"<b>Step 6 - Playtest</b><br><br>"
"<b>Step 6 - Images (MV/MZ)</b><br><br>"
"Use the existing <b>Images</b> page for bitmap UI translation:<br>"
"• Confirm this step reports the correct game root, image tree, encryption key, and "
"<code>vocab.txt</code><br>"
"• Open Images and decrypt the images you want to edit<br>"
"• Click <b>Copy skill</b>, paste it into Codex/Cursor/Copilot, and let the agent edit "
"only the generated <code>.dazedtl/images/.../img</code> copies<br>"
"• Review the results, then use <b>Patch selected</b> or <b>Patch all</b><br><br>"
"Editable PNGs must preserve the same <code>img/...</code> hierarchy as the game. "
"This step warns when PNGs were placed elsewhere in the workspace."
),
7: (
"<b>Step 7 - Playtest</b><br><br>"
"Install playtest plugins into the MV/MZ game:<br>"
"• <b>TL Inspector</b> - inspect translated text in-game<br>"
"• <b>Forge</b> - additional playtest helpers<br><br>"
@ -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.<br>"
"2. Click <b>Copy skill</b> in the Image Manager and paste it into your coding "
"agent.<br>"
"3. Review the translated PNGs in the Image Manager.<br>"
"4. Highlight finished images and click <b>Patch selected</b>, or clear highlights "
"and click <b>Patch all</b>."
)
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 <game root>/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(
"<span style='color:#e9a12a'>⚠ Image Manager supports RPG Maker MV/MZ "
"projects only; this project is RPG Maker Ace.</span>"
)
if button is not None:
button.setEnabled(False)
return
game_root = self.folder_edit.text().strip()
if not game_root:
label.setText(
"<span style='color:#e9a12a'>⚠ Complete Step 0 and select the actual game "
"root first.</span>"
)
if button is not None:
button.setEnabled(False)
return
import html
report = _inspect_image_workflow(game_root)
if not report["ok"]:
label.setText(
"<span style='color:#f48771'>✗ The Step 0 folder is not ready for image "
f"management: {html.escape(report['error'])}</span><br>"
"Select the folder that directly contains <code>img/</code>, or contains "
"<code>www/img/</code>."
)
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"<span style='color:#8fbc8f'>✓ Project root:</span> <code>{root}</code>",
f"<span style='color:#8fbc8f'>✓ Runtime images:</span> "
f"{report['runtime']:,} ({report['encrypted']:,} encrypted)",
]
if report["key_ok"] is False:
lines.append(
"<span style='color:#f48771'>✗ Encryption key:</span> missing or invalid in "
"<code>System.json</code>; encrypted images cannot be decrypted."
)
elif report["key_ok"] is True:
lines.append("<span style='color:#8fbc8f'>✓ Encryption key:</span> ready")
else:
lines.append("<span style='color:#8fbc8f'>✓ Encryption key:</span> not required")
if Path(report["vocab"]).is_file():
lines.append(
f"<span style='color:#8fbc8f'>✓ Glossary:</span> <code>{vocab}</code>"
)
else:
lines.append(
"<span style='color:#e9a12a'>⚠ Glossary:</span> "
f"<code>{vocab}</code> is missing. Copy it before using the AI skill."
)
if report["editable"]:
lines.append(
f"<span style='color:#8fbc8f'>✓ Editable PNGs:</span> "
f"{report['editable']:,} under <code>{editable_root}</code>"
)
else:
lines.append(
"<span style='color:#9d9d9d'>• Editable PNGs:</span> none yet. Open Images "
"and decrypt the images you want to translate."
)
if report["misplaced"]:
lines.append(
f"<span style='color:#e9a12a'>⚠ Workspace layout:</span> "
f"{report['misplaced']:,} PNG(s) are outside <code>{editable_root}</code> and "
"will not appear in the Image Manager."
)
else:
lines.append(
"<span style='color:#8fbc8f'>✓ Workspace layout:</span> editable PNGs use "
"the expected game-relative hierarchy."
)
label.setText("<br>".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 67 - 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}")

View file

@ -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.

View file

@ -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:</span> 1", workflow._image_workflow_status.text())
self.assertIn("Glossary:</span>", 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__":