""" RPG Maker MV/MZ Tab - Configuration for RPG Maker specific settings """ import json from pathlib import Path from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QCheckBox, QPushButton, QGroupBox, QLabel, QMessageBox, QScrollArea, QTextEdit, QSpinBox, QFrame, QGridLayout ) from PyQt5.QtCore import Qt, pyqtSignal try: from .config_integration import ConfigIntegration except ImportError: # Fallback if config_integration is not available ConfigIntegration = None def create_section_label(text): """Create a section label for grouping checkboxes.""" label = QLabel(text) label.setStyleSheet(""" QLabel { font-size: 12px; font-weight: bold; color: #007acc; padding: 5px 0px 3px 0px; background-color: transparent; } """) return label class RPGMakerTab(QWidget): """RPG Maker MV/MZ & Ace configuration tab. This widget now supports both MV/MZ and Ace engines. Pass engine="ACE" to target rpgmakerace.py; otherwise it will default to MV/MZ (rpgmakermvmz.py). """ # Default configuration values for RPG Maker MV/MZ DEFAULT_CONFIG = { # General settings "FIRSTLINESPEAKERS": False, "FACENAME101": False, "NAMES": False, "BRFLAG": False, "FIXTEXTWRAP": True, "IGNORETLTEXT": False, # Main dialogue codes (enabled by default) "CODE401": True, # Show Text "CODE405": True, # Show Text (line 4+) "CODE102": True, # Show Choices # Optional codes (disabled by default) "CODE101": False, # Show Text (face) "CODE408": False, # Show Text (line continuation) # Variable codes (disabled by default) "CODE122": False, # Control Variables # Other codes (all disabled by default) "CODE103": False, # Input Number "CODE104": False, # Select Item "CODE111": False, # Conditional Branch "CODE117": False, # Common Event "CODE119": False, # Set Movement Route "CODE121": False, # Control Switches "CODE125": False, # Change Gold "CODE126": False, # Change Items "CODE127": False, # Change Weapons "CODE128": False, # Change Armors "CODE129": False, # Change Party Member "CODE132": False, # Change Battle BGM "CODE133": False, # Change Victory ME "CODE134": False, # Change Save Access "CODE135": False, # Change Menu Access "CODE136": False, # Change Encounter "CODE137": False, # Change Formation Access "CODE138": False, # Change Window Color "CODE139": False, # Change Defeat ME "CODE140": False, # Change Vehicle BGM "CODE201": False, # Transfer Player "CODE202": False, # Set Vehicle Location "CODE203": False, # Set Event Location "CODE204": False, # Scroll Map "CODE205": False, # Set Move Route "CODE206": False, # Get On/Off Vehicle "CODE211": False, # Change Transparency "CODE212": False, # Show Animation "CODE213": False, # Show Balloon Icon "CODE214": False, # Erase Event "CODE216": False, # Change Player Followers "CODE217": False, # Gather Followers "CODE221": False, # Fadeout Screen "CODE222": False, # Fadein Screen "CODE223": False, # Tint Screen "CODE224": False, # Flash Screen "CODE225": False, # Shake Screen "CODE230": False, # Wait "CODE231": False, # Show Picture "CODE232": False, # Move Picture "CODE233": False, # Rotate Picture "CODE234": False, # Tint Picture "CODE235": False, # Erase Picture "CODE236": False, # Set Weather Effect "CODE241": False, # Play BGM "CODE242": False, # Fadeout BGM "CODE243": False, # Save BGM "CODE244": False, # Resume BGM "CODE245": False, # Play BGS "CODE246": False, # Fadeout BGS "CODE249": False, # Play ME "CODE250": False, # Play SE "CODE251": False, # Stop SE "CODE261": False, # Show Movie "CODE281": False, # Change Map Name Display "CODE282": False, # Change Tileset "CODE283": False, # Change Battle Background "CODE284": False, # Change Parallax "CODE285": False, # Get Location Info "CODE301": False, # Battle Processing "CODE302": False, # Shop Processing "CODE303": False, # Name Input Processing "CODE311": False, # Change HP "CODE312": False, # Change MP "CODE313": False, # Change State "CODE314": False, # Recover All "CODE315": False, # Change EXP "CODE316": False, # Change Level "CODE317": False, # Change Parameters "CODE318": False, # Change Skills "CODE319": False, # Change Equipment "CODE320": False, # Change Name "CODE321": False, # Change Class "CODE322": False, # Change Actor Graphic "CODE323": False, # Change Vehicle Graphic "CODE324": False, # Change Nickname "CODE325": False, # Change Profile "CODE326": False, # Change TP "CODE331": False, # Change Enemy HP "CODE332": False, # Change Enemy MP "CODE333": False, # Change Enemy State "CODE334": False, # Enemy Recover All "CODE335": False, # Enemy Appear "CODE336": False, # Enemy Transform "CODE337": False, # Show Battle Animation "CODE339": False, # Force Action "CODE340": False, # Abort Battle "CODE351": False, # Open Menu Screen "CODE352": False, # Open Save Screen "CODE353": False, # Game Over "CODE354": False, # Return to Title Screen "CODE355": False, # Script "CODE356": False, # Plugin Command } config_changed = pyqtSignal() def __init__(self, engine: str = "MVMZ"): super().__init__() self.engine = engine.upper() self.config_integration = ConfigIntegration() if ConfigIntegration else None self.init_ui() self.connect_auto_apply() # Connect auto-apply before setting defaults self.reset_to_defaults() def init_ui(self): """Initialize the user interface with compact two-column layout.""" main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) main_layout.setSpacing(10) # Title and description title = "RPG Maker MV/MZ" if self.engine != "ACE" else "RPG Maker Ace" title_label = QLabel(f"{title} Translation Settings") title_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #007acc;") main_layout.addWidget(title_label) description_label = QLabel( "Enable event codes to translate. Only enable what you need to reduce translation time and costs." ) description_label.setWordWrap(True) description_label.setStyleSheet("color: #888888; font-size: 11px; margin-bottom: 5px;") main_layout.addWidget(description_label) # Two-column layout for checkboxes columns_layout = QHBoxLayout() columns_layout.setSpacing(30) # LEFT COLUMN left_column = QVBoxLayout() left_column.setSpacing(3) # General Configuration left_column.addWidget(create_section_label("⚙️ General Options")) self.first_line_speakers_cb = QCheckBox("First Line Speakers") self.first_line_speakers_cb.setToolTip("Enable if first line of CODE401 contains speaker name") left_column.addWidget(self.first_line_speakers_cb) self.facename101_cb = QCheckBox("Face Name in CODE101") self.facename101_cb.setToolTip("Enable to translate face names in CODE101") left_column.addWidget(self.facename101_cb) self.names_cb = QCheckBox("Translate Names") self.names_cb.setToolTip("Enable to translate character names") left_column.addWidget(self.names_cb) self.brflag_cb = QCheckBox("Line Break Flag") self.brflag_cb.setToolTip("Enable line break handling") left_column.addWidget(self.brflag_cb) self.fixtextwrap_cb = QCheckBox("Fix Text Wrap") self.fixtextwrap_cb.setToolTip("Automatically fix text wrapping issues") left_column.addWidget(self.fixtextwrap_cb) self.ignoretltext_cb = QCheckBox("Ignore TL Text") self.ignoretltext_cb.setToolTip("Ignore already translated text") left_column.addWidget(self.ignoretltext_cb) left_column.addSpacing(8) # Main Dialogue Codes left_column.addWidget(create_section_label("💬 Main Dialogue (Recommended)")) self.code401_cb = QCheckBox("CODE401 - Show Text") left_column.addWidget(self.code401_cb) self.code405_cb = QCheckBox("CODE405 - Show Text (line 4+)") left_column.addWidget(self.code405_cb) self.code102_cb = QCheckBox("CODE102 - Show Choices") left_column.addWidget(self.code102_cb) left_column.addSpacing(8) # Optional Dialogue Codes left_column.addWidget(create_section_label("📝 Optional Dialogue")) self.code101_cb = QCheckBox("CODE101 - Show Text (face)") left_column.addWidget(self.code101_cb) self.code408_cb = QCheckBox("CODE408 - Show Text (continuation)") self.code408_cb.setToolTip("Can significantly increase costs") left_column.addWidget(self.code408_cb) self.code122_cb = QCheckBox("CODE122 - Control Variables") left_column.addWidget(self.code122_cb) left_column.addStretch() # RIGHT COLUMN right_column = QVBoxLayout() right_column.setSpacing(3) # Event Codes (compact grid layout) right_column.addWidget(create_section_label("🎮 Event Codes")) # Create a grid for better space usage event_grid = QGridLayout() event_grid.setSpacing(2) event_grid.setHorizontalSpacing(15) # Event codes in a more compact format event_codes = [ ("CODE103", "Input Number"), ("CODE104", "Select Item"), ("CODE111", "Conditional Branch"), ("CODE117", "Common Event"), ("CODE119", "Set Movement"), ("CODE121", "Control Switches"), ("CODE125", "Change Gold"), ("CODE126", "Change Items"), ("CODE127", "Change Weapons"), ("CODE128", "Change Armors"), ("CODE129", "Change Party"), ("CODE201", "Transfer Player"), ("CODE202", "Set Vehicle Loc"), ("CODE203", "Set Event Loc"), ("CODE211", "Change Transparency"), ("CODE212", "Show Animation"), ("CODE213", "Show Balloon"), ("CODE214", "Erase Event"), ("CODE221", "Fadeout Screen"), ("CODE222", "Fadein Screen"), ("CODE231", "Show Picture"), ("CODE232", "Move Picture"), ("CODE241", "Play BGM"), ("CODE242", "Fadeout BGM"), ] self.event_checkboxes = {} for idx, (code, label) in enumerate(event_codes): cb = QCheckBox(f"{code} - {label}") cb.setStyleSheet("QCheckBox { font-size: 10px; }") event_grid.addWidget(cb, idx // 2, idx % 2) self.event_checkboxes[code] = cb right_column.addLayout(event_grid) right_column.addStretch() # Add both columns columns_layout.addLayout(left_column, 1) columns_layout.addLayout(right_column, 1) main_layout.addLayout(columns_layout) # Bottom buttons button_layout = QHBoxLayout() button_layout.setSpacing(10) self.reset_button = QPushButton("🔄 Reset to Defaults") self.reset_button.clicked.connect(self.reset_to_defaults_with_message) self.reset_button.setMaximumWidth(200) self.apply_button = QPushButton("✓ Apply Settings") self.apply_button.clicked.connect(self.apply_to_module) self.apply_button.setMaximumWidth(200) button_layout.addWidget(self.reset_button) button_layout.addWidget(self.apply_button) button_layout.addStretch() main_layout.addLayout(button_layout) self.setLayout(main_layout) def reset_to_defaults(self): """Reset all settings to default values.""" # Temporarily disconnect signals to avoid multiple apply calls self.disconnect_auto_apply() # Apply default values from the DEFAULT_CONFIG constant self.first_line_speakers_cb.setChecked(self.DEFAULT_CONFIG["FIRSTLINESPEAKERS"]) self.facename101_cb.setChecked(self.DEFAULT_CONFIG["FACENAME101"]) self.names_cb.setChecked(self.DEFAULT_CONFIG["NAMES"]) self.brflag_cb.setChecked(self.DEFAULT_CONFIG["BRFLAG"]) self.fixtextwrap_cb.setChecked(self.DEFAULT_CONFIG["FIXTEXTWRAP"]) self.ignoretltext_cb.setChecked(self.DEFAULT_CONFIG["IGNORETLTEXT"]) # Main dialogue codes self.code401_cb.setChecked(self.DEFAULT_CONFIG["CODE401"]) self.code405_cb.setChecked(self.DEFAULT_CONFIG["CODE405"]) self.code102_cb.setChecked(self.DEFAULT_CONFIG["CODE102"]) # Optional codes self.code101_cb.setChecked(self.DEFAULT_CONFIG["CODE101"]) self.code408_cb.setChecked(self.DEFAULT_CONFIG["CODE408"]) # Variable codes self.code122_cb.setChecked(self.DEFAULT_CONFIG["CODE122"]) # Event codes for code, cb in self.event_checkboxes.items(): cb.setChecked(self.DEFAULT_CONFIG.get(code, False)) # Reconnect signals and apply changes once self.connect_auto_apply() self.apply_to_module(show_messages=False) def reset_to_defaults_with_message(self): """Reset to defaults and show confirmation message (for button clicks).""" self.reset_to_defaults() QMessageBox.information( self, "Reset Complete", "All settings have been reset to their default values and applied to the module." ) def disconnect_auto_apply(self): """Disconnect all checkboxes from auto-apply to prevent multiple calls.""" try: # General settings checkboxes self.first_line_speakers_cb.stateChanged.disconnect() self.facename101_cb.stateChanged.disconnect() self.names_cb.stateChanged.disconnect() self.brflag_cb.stateChanged.disconnect() self.fixtextwrap_cb.stateChanged.disconnect() self.ignoretltext_cb.stateChanged.disconnect() # Main dialogue codes self.code401_cb.stateChanged.disconnect() self.code405_cb.stateChanged.disconnect() self.code102_cb.stateChanged.disconnect() # Optional codes self.code101_cb.stateChanged.disconnect() self.code408_cb.stateChanged.disconnect() # Variable codes self.code122_cb.stateChanged.disconnect() # Event codes for cb in self.event_checkboxes.values(): cb.stateChanged.disconnect() except TypeError: # Ignore if signals are not connected pass def connect_auto_apply(self): """Connect all checkboxes to auto-apply changes when modified.""" # General settings checkboxes self.first_line_speakers_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.facename101_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.names_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.brflag_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.fixtextwrap_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.ignoretltext_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) # Main dialogue codes self.code401_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.code405_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.code102_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) # Optional codes self.code101_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.code408_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) # Variable codes self.code122_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) # Event codes for cb in self.event_checkboxes.values(): cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) def get_config(self): """Get current configuration as dictionary.""" config = { # General settings "FIRSTLINESPEAKERS": self.first_line_speakers_cb.isChecked(), "FACENAME101": self.facename101_cb.isChecked(), "NAMES": self.names_cb.isChecked(), "BRFLAG": self.brflag_cb.isChecked(), "FIXTEXTWRAP": self.fixtextwrap_cb.isChecked(), "IGNORETLTEXT": self.ignoretltext_cb.isChecked(), # Main dialogue codes "CODE401": self.code401_cb.isChecked(), "CODE405": self.code405_cb.isChecked(), "CODE102": self.code102_cb.isChecked(), # Optional codes "CODE101": self.code101_cb.isChecked(), "CODE408": self.code408_cb.isChecked(), # Variable codes "CODE122": self.code122_cb.isChecked(), } # Event codes for code, cb in self.event_checkboxes.items(): config[code] = cb.isChecked() return config def set_config(self, config): """Set configuration from dictionary.""" # General settings self.first_line_speakers_cb.setChecked(config.get("FIRSTLINESPEAKERS", False)) self.facename101_cb.setChecked(config.get("FACENAME101", False)) self.names_cb.setChecked(config.get("NAMES", False)) self.brflag_cb.setChecked(config.get("BRFLAG", False)) self.fixtextwrap_cb.setChecked(config.get("FIXTEXTWRAP", True)) self.ignoretltext_cb.setChecked(config.get("IGNORETLTEXT", False)) # Main dialogue codes self.code401_cb.setChecked(config.get("CODE401", True)) self.code405_cb.setChecked(config.get("CODE405", True)) self.code102_cb.setChecked(config.get("CODE102", True)) # Optional codes self.code101_cb.setChecked(config.get("CODE101", False)) self.code408_cb.setChecked(config.get("CODE408", False)) # Variable codes self.code122_cb.setChecked(config.get("CODE122", False)) # Event codes for code, cb in self.event_checkboxes.items(): cb.setChecked(config.get(code, False)) def load_from_file(self, file_path): """Load configuration from file.""" try: with open(file_path, 'r', encoding='utf-8') as f: config = json.load(f) self.set_config(config) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to load RPG Maker config:\n{str(e)}") def validate(self): """Validate current configuration.""" warnings = [] errors = [] # Check if any main dialogue codes are enabled main_codes_enabled = ( self.code401_cb.isChecked() or self.code405_cb.isChecked() or self.code102_cb.isChecked() ) if not main_codes_enabled: warnings.append("No main dialogue codes are enabled. You may not get any translated content.") # Check for high-cost options if self.code408_cb.isChecked(): warnings.append("CODE 408 (Comments) is enabled. This can significantly increase translation costs!") # Check for conflicting options if self.brflag_cb.isChecked() and self.fixtextwrap_cb.isChecked(): warnings.append("Both BR tags and text wrapping are enabled. This might cause formatting issues.") return len(errors) == 0, warnings, errors def validate_and_show_report(self): """Validate and show a detailed report.""" is_valid, warnings, errors = self.validate() report = [] if errors: report.append("Errors:") for error in errors: report.append(f"• {error}") report.append("") if warnings: report.append("Warnings:") for warning in warnings: report.append(f"• {warning}") report.append("") # Add enabled codes summary enabled_codes = [] config = self.get_config() for key, value in config.items(): if key.startswith("CODE") and value: enabled_codes.append(key) if enabled_codes: report.append("Enabled Codes:") report.append(", ".join(enabled_codes)) else: report.append("No codes enabled!") if not report: report.append("Configuration is valid with no issues.") msg_box = QMessageBox(self) msg_box.setWindowTitle("Validation Report") msg_box.setTextFormat(Qt.RichText) msg_box.setText("
".join(report)) if errors: msg_box.setIcon(QMessageBox.Critical) elif warnings: msg_box.setIcon(QMessageBox.Warning) else: msg_box.setIcon(QMessageBox.Information) msg_box.exec_() return is_valid def apply_to_module(self, show_messages=False): """Apply current configuration to the correct RPG Maker module (MV/MZ or Ace).""" try: config = self.get_config() # Select module filename based on engine module_filename = "rpgmakermvmz.py" if self.engine != "ACE" else "rpgmakerace.py" module_path = Path(__file__).parent.parent / "modules" / module_filename if not module_path.exists(): if show_messages: QMessageBox.critical( self, "Error", "modules/rpgmakermvmz.py not found!\n\n" "Make sure you're running the GUI from the correct directory." ) return # Read the current file with open(module_path, 'r', encoding='utf-8') as f: content = f.read() # Update each configuration value for key, value in config.items(): # Convert boolean to Python boolean string value_str = str(value) # Find and replace the line with this configuration import re pattern = rf'^{key}\s*=\s*.*$' replacement = f'{key} = {value_str}' content = re.sub(pattern, replacement, content, flags=re.MULTILINE) # Write the updated content back with open(module_path, 'w', encoding='utf-8') as f: f.write(content) # Emit signal for other components (silent) self.config_changed.emit() if show_messages: QMessageBox.information( self, "Success", f"Configuration has been applied to modules/{module_filename}\n\n" "The module will now use these settings when running translations." ) except Exception as e: if show_messages: QMessageBox.critical(self, "Error", f"Failed to apply configuration:\n{str(e)}") # Silent failure for auto-apply - just emit signal anyway self.config_changed.emit() def load_from_module(self): """Load configuration from the actual module file based on engine.""" if not self.config_integration: return try: module_filename = "rpgmakermvmz.py" if self.engine != "ACE" else "rpgmakerace.py" module_path = Path("modules") / module_filename config = self.config_integration.read_current_config(module_path) if config: self.set_config(config) QMessageBox.information(self, "Success", f"Configuration loaded from {module_filename}") else: QMessageBox.warning(self, "Warning", f"No configuration found in {module_filename}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to load from module:\n{str(e)}")