From e7b41c9f44e569c779d456c2fc386ad93725dbb4 Mon Sep 17 00:00:00 2001 From: dazedanon Date: Fri, 18 Jul 2025 15:44:26 -0500 Subject: [PATCH] Get mv config working --- gui/config_integration.py | 11 - gui/file_manager_tab.py | 466 ++++++++++++++++++++++++++++++++++++ gui/main.py | 22 +- gui/rpgmaker_tab.py | 297 +++++++++++++++++++---- gui/translation_tab.py | 489 ++++++++++++++++++++++++++++++++++++++ modules/rpgmakermvmz.py | 16 +- set_defaults.py | 16 +- 7 files changed, 1245 insertions(+), 72 deletions(-) create mode 100644 gui/file_manager_tab.py create mode 100644 gui/translation_tab.py diff --git a/gui/config_integration.py b/gui/config_integration.py index 1f02c7c..bcc181e 100644 --- a/gui/config_integration.py +++ b/gui/config_integration.py @@ -27,11 +27,6 @@ class ConfigIntegration: with open(module_path, 'r', encoding='utf-8') as f: content = f.read() - # Create backup - backup_path = module_path.with_suffix('.py.backup') - with open(backup_path, 'w', encoding='utf-8') as f: - f.write(content) - # Update configuration values updated_content = self._update_config_values(content, config) @@ -42,12 +37,6 @@ class ConfigIntegration: return True except Exception as e: - # Restore from backup if something went wrong - if backup_path.exists(): - with open(backup_path, 'r', encoding='utf-8') as f: - content = f.read() - with open(module_path, 'w', encoding='utf-8') as f: - f.write(content) raise e def _update_config_values(self, content: str, config: Dict[str, Any]) -> str: diff --git a/gui/file_manager_tab.py b/gui/file_manager_tab.py new file mode 100644 index 0000000..10a9788 --- /dev/null +++ b/gui/file_manager_tab.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +""" +File Manager Tab for DazedMTLTool GUI + +Manages input files from 'files' directory and output to 'translated' directory. +Users can add files through GUI or by copying them to the files folder. +""" + +import os +import shutil +from pathlib import Path +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTreeWidget, QTreeWidgetItem, + QPushButton, QGroupBox, QSplitter, QTextEdit, QFileDialog, QMessageBox, + QProgressBar, QFrame, QHeaderView +) +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QFont + + +class FileManagerTab(QWidget): + """File Manager tab for managing input and output files.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.project_root = Path(__file__).parent.parent + self.files_dir = self.project_root / "files" + self.translated_dir = self.project_root / "translated" + + # Ensure directories exist + self.files_dir.mkdir(exist_ok=True) + self.translated_dir.mkdir(exist_ok=True) + + self.setup_ui() + self.refresh_file_lists() + + # Auto-refresh timer + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self.refresh_file_lists) + self.refresh_timer.start(3000) # Refresh every 3 seconds + + def setup_ui(self): + """Set up the user interface.""" + layout = QVBoxLayout() + + # Title + title_label = QLabel("File Manager") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;") + layout.addWidget(title_label) + + # Description + desc_label = QLabel( + "Input files are loaded from the 'files' directory. " + "Translated files will be saved to the 'translated' directory. " + "You can add files using the GUI or by copying them to the files folder." + ) + desc_label.setWordWrap(True) + desc_label.setStyleSheet("color: #cccccc; margin-bottom: 10px;") + layout.addWidget(desc_label) + + # Main splitter + splitter = QSplitter(Qt.Horizontal) + + # Input files section + input_group = self.create_input_files_section() + splitter.addWidget(input_group) + + # Output files section + output_group = self.create_output_files_section() + splitter.addWidget(output_group) + + # Set equal sizes + splitter.setSizes([400, 400]) + layout.addWidget(splitter) + + # Action buttons + button_layout = self.create_action_buttons() + layout.addLayout(button_layout) + + # Status bar + self.status_label = QLabel("Ready") + self.status_label.setStyleSheet("color: #cccccc; padding: 5px;") + layout.addWidget(self.status_label) + + self.setLayout(layout) + + def create_input_files_section(self): + """Create the input files section.""" + group = QGroupBox("Input Files (files directory)") + layout = QVBoxLayout() + + # File tree + self.input_tree = QTreeWidget() + self.input_tree.setHeaderLabels(["Filename", "Size", "Modified"]) + self.input_tree.header().setSectionResizeMode(0, QHeaderView.Stretch) + self.input_tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) + self.input_tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents) + self.input_tree.itemSelectionChanged.connect(self.on_input_file_selected) + layout.addWidget(self.input_tree) + + # Input buttons + input_button_layout = QHBoxLayout() + + self.add_file_btn = QPushButton("Add Files") + self.add_file_btn.clicked.connect(self.add_files) + input_button_layout.addWidget(self.add_file_btn) + + self.remove_file_btn = QPushButton("Remove Selected") + self.remove_file_btn.clicked.connect(self.remove_input_file) + self.remove_file_btn.setEnabled(False) + input_button_layout.addWidget(self.remove_file_btn) + + self.open_files_folder_btn = QPushButton("Open Files Folder") + self.open_files_folder_btn.clicked.connect(self.open_files_folder) + input_button_layout.addWidget(self.open_files_folder_btn) + + input_button_layout.addStretch() + layout.addLayout(input_button_layout) + + group.setLayout(layout) + return group + + def create_output_files_section(self): + """Create the output files section.""" + group = QGroupBox("Output Files (translated directory)") + layout = QVBoxLayout() + + # File tree + self.output_tree = QTreeWidget() + self.output_tree.setHeaderLabels(["Filename", "Size", "Modified"]) + self.output_tree.header().setSectionResizeMode(0, QHeaderView.Stretch) + self.output_tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) + self.output_tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents) + self.output_tree.itemSelectionChanged.connect(self.on_output_file_selected) + layout.addWidget(self.output_tree) + + # Output buttons + output_button_layout = QHBoxLayout() + + self.view_output_btn = QPushButton("View Selected") + self.view_output_btn.clicked.connect(self.view_output_file) + self.view_output_btn.setEnabled(False) + output_button_layout.addWidget(self.view_output_btn) + + self.delete_output_btn = QPushButton("Delete Selected") + self.delete_output_btn.clicked.connect(self.delete_output_file) + self.delete_output_btn.setEnabled(False) + output_button_layout.addWidget(self.delete_output_btn) + + self.open_translated_folder_btn = QPushButton("Open Translated Folder") + self.open_translated_folder_btn.clicked.connect(self.open_translated_folder) + output_button_layout.addWidget(self.open_translated_folder_btn) + + output_button_layout.addStretch() + layout.addLayout(output_button_layout) + + group.setLayout(layout) + return group + + def create_action_buttons(self): + """Create main action buttons.""" + layout = QHBoxLayout() + + self.refresh_btn = QPushButton("Refresh File Lists") + self.refresh_btn.clicked.connect(self.refresh_file_lists) + layout.addWidget(self.refresh_btn) + + self.clear_translated_btn = QPushButton("Clear All Translated Files") + self.clear_translated_btn.clicked.connect(self.clear_translated_files) + layout.addWidget(self.clear_translated_btn) + + layout.addStretch() + + # Auto-refresh indicator + self.auto_refresh_label = QLabel("Auto-refresh: ON") + self.auto_refresh_label.setStyleSheet("color: #00aa00; font-size: 10px;") + layout.addWidget(self.auto_refresh_label) + + return layout + + def refresh_file_lists(self): + """Refresh both input and output file lists.""" + self.refresh_input_files() + self.refresh_output_files() + self.update_status() + + def refresh_input_files(self): + """Refresh the input files tree.""" + self.input_tree.clear() + + if not self.files_dir.exists(): + return + + supported_extensions = {'.json', '.txt', '.csv', '.js', '.py', '.xml', '.html', '.md'} + + for file_path in self.files_dir.iterdir(): + if (file_path.is_file() and + file_path.suffix.lower() in supported_extensions and + file_path.name != '.gitkeep'): + item = QTreeWidgetItem() + item.setText(0, file_path.name) + + # File size + size = file_path.stat().st_size + if size < 1024: + size_str = f"{size} B" + elif size < 1024 * 1024: + size_str = f"{size / 1024:.1f} KB" + else: + size_str = f"{size / (1024 * 1024):.1f} MB" + item.setText(1, size_str) + + # Modified time + import datetime + mtime = datetime.datetime.fromtimestamp(file_path.stat().st_mtime) + item.setText(2, mtime.strftime("%Y-%m-%d %H:%M")) + + # Store file path + item.setData(0, Qt.UserRole, str(file_path)) + + self.input_tree.addTopLevelItem(item) + + def refresh_output_files(self): + """Refresh the output files tree.""" + self.output_tree.clear() + + if not self.translated_dir.exists(): + return + + for file_path in self.translated_dir.iterdir(): + if file_path.is_file() and file_path.name != '.gitkeep': + item = QTreeWidgetItem() + item.setText(0, file_path.name) + + # File size + size = file_path.stat().st_size + if size < 1024: + size_str = f"{size} B" + elif size < 1024 * 1024: + size_str = f"{size / 1024:.1f} KB" + else: + size_str = f"{size / (1024 * 1024):.1f} MB" + item.setText(1, size_str) + + # Modified time + import datetime + mtime = datetime.datetime.fromtimestamp(file_path.stat().st_mtime) + item.setText(2, mtime.strftime("%Y-%m-%d %H:%M")) + + # Store file path + item.setData(0, Qt.UserRole, str(file_path)) + + self.output_tree.addTopLevelItem(item) + + def update_status(self): + """Update the status label.""" + input_count = self.input_tree.topLevelItemCount() + output_count = self.output_tree.topLevelItemCount() + self.status_label.setText(f"Input files: {input_count} | Output files: {output_count}") + + def on_input_file_selected(self): + """Handle input file selection.""" + selected = self.input_tree.selectedItems() + self.remove_file_btn.setEnabled(len(selected) > 0) + + def on_output_file_selected(self): + """Handle output file selection.""" + selected = self.output_tree.selectedItems() + self.view_output_btn.setEnabled(len(selected) > 0) + self.delete_output_btn.setEnabled(len(selected) > 0) + + def add_files(self): + """Add files to the input directory.""" + file_paths, _ = QFileDialog.getOpenFileNames( + self, + "Select Files to Add", + "", + "All Supported Files (*.json *.txt *.csv *.js *.py *.xml *.html *.md);;All Files (*.*)" + ) + + if not file_paths: + return + + copied_count = 0 + for file_path in file_paths: + source_path = Path(file_path) + dest_path = self.files_dir / source_path.name + + try: + # Check if file already exists + if dest_path.exists(): + reply = QMessageBox.question( + self, + "File Exists", + f"'{source_path.name}' already exists. Overwrite?", + QMessageBox.Yes | QMessageBox.No + ) + if reply != QMessageBox.Yes: + continue + + shutil.copy2(source_path, dest_path) + copied_count += 1 + + except Exception as e: + QMessageBox.warning( + self, + "Copy Error", + f"Failed to copy '{source_path.name}':\n{str(e)}" + ) + + if copied_count > 0: + QMessageBox.information( + self, + "Files Added", + f"Successfully added {copied_count} file(s) to the input directory." + ) + self.refresh_input_files() + + def remove_input_file(self): + """Remove selected input file.""" + selected = self.input_tree.selectedItems() + if not selected: + return + + item = selected[0] + file_path = Path(item.data(0, Qt.UserRole)) + + reply = QMessageBox.question( + self, + "Confirm Removal", + f"Are you sure you want to remove '{file_path.name}' from the input directory?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + try: + file_path.unlink() + self.refresh_input_files() + QMessageBox.information(self, "File Removed", f"'{file_path.name}' has been removed.") + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to remove file:\n{str(e)}") + + def view_output_file(self): + """View selected output file.""" + selected = self.output_tree.selectedItems() + if not selected: + return + + item = selected[0] + file_path = Path(item.data(0, Qt.UserRole)) + + try: + # Open file with default system application + import subprocess + import platform + + if platform.system() == 'Windows': + os.startfile(str(file_path)) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', str(file_path)]) + else: # Linux + subprocess.run(['xdg-open', str(file_path)]) + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open file:\n{str(e)}") + + def delete_output_file(self): + """Delete selected output file.""" + selected = self.output_tree.selectedItems() + if not selected: + return + + item = selected[0] + file_path = Path(item.data(0, Qt.UserRole)) + + reply = QMessageBox.question( + self, + "Confirm Deletion", + f"Are you sure you want to delete '{file_path.name}' from the output directory?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + try: + file_path.unlink() + self.refresh_output_files() + QMessageBox.information(self, "File Deleted", f"'{file_path.name}' has been deleted.") + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to delete file:\n{str(e)}") + + def clear_translated_files(self): + """Clear all files from the translated directory.""" + if self.output_tree.topLevelItemCount() == 0: + QMessageBox.information(self, "No Files", "There are no files to clear.") + return + + reply = QMessageBox.question( + self, + "Confirm Clear All", + "Are you sure you want to delete ALL files from the translated directory?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + try: + deleted_count = 0 + for file_path in self.translated_dir.iterdir(): + if file_path.is_file() and file_path.name != '.gitkeep': + file_path.unlink() + deleted_count += 1 + + self.refresh_output_files() + QMessageBox.information( + self, + "Files Cleared", + f"Successfully deleted {deleted_count} file(s) from the translated directory." + ) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to clear files:\n{str(e)}") + + def open_files_folder(self): + """Open the files folder in the system file manager.""" + try: + import subprocess + import platform + + if platform.system() == 'Windows': + subprocess.run(['explorer', str(self.files_dir)]) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', str(self.files_dir)]) + else: # Linux + subprocess.run(['xdg-open', str(self.files_dir)]) + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open folder:\n{str(e)}") + + def open_translated_folder(self): + """Open the translated folder in the system file manager.""" + try: + import subprocess + import platform + + if platform.system() == 'Windows': + subprocess.run(['explorer', str(self.translated_dir)]) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', str(self.translated_dir)]) + else: # Linux + subprocess.run(['xdg-open', str(self.translated_dir)]) + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open folder:\n{str(e)}") + + def get_input_files(self): + """Get list of input files for processing.""" + input_files = [] + for i in range(self.input_tree.topLevelItemCount()): + item = self.input_tree.topLevelItem(i) + file_path = item.data(0, Qt.UserRole) + input_files.append(Path(file_path)) + return input_files + + def closeEvent(self, event): + """Handle widget close event.""" + if hasattr(self, 'refresh_timer'): + self.refresh_timer.stop() + event.accept() diff --git a/gui/main.py b/gui/main.py index 579f2fc..1ca116f 100644 --- a/gui/main.py +++ b/gui/main.py @@ -22,6 +22,8 @@ from gui.rpgmaker_tab import RPGMakerTab from gui.other_modules_tab import OtherModulesTab from gui.log_viewer import LogViewer from gui.file_manager import FileManager +from gui.file_manager_tab import FileManagerTab +from gui.translation_tab import TranslationTab class DazedMTLGUI(QMainWindow): """Main GUI window for the DazedMTLTool.""" @@ -118,6 +120,14 @@ class DazedMTLGUI(QMainWindow): self.config_tab.config_changed.connect(self.on_config_changed) self.tab_widget.addTab(self.config_tab, "Configuration") + # File Manager Tab (New) + self.file_manager_tab = FileManagerTab() + self.tab_widget.addTab(self.file_manager_tab, "File Manager") + + # Translation Execution Tab + self.translation_tab = TranslationTab(self) + self.tab_widget.addTab(self.translation_tab, "Translation") + # RPG Maker MV/MZ Tab self.rpgmaker_tab = RPGMakerTab() self.tab_widget.addTab(self.rpgmaker_tab, "RPG Maker MV/MZ") @@ -126,10 +136,6 @@ class DazedMTLGUI(QMainWindow): self.other_modules_tab = OtherModulesTab() self.tab_widget.addTab(self.other_modules_tab, "Other Modules") - # File Manager Tab - self.file_manager = FileManager() - self.tab_widget.addTab(self.file_manager, "File Manager") - def on_config_changed(self): """Handle configuration changes.""" # This will be called when font scale or other settings change @@ -330,6 +336,14 @@ class DazedMTLGUI(QMainWindow): def set_progress(self, value): """Set the progress bar value (0-100).""" self.progress_bar.setValue(value) + + def get_input_files(self): + """Get list of input files from the file manager.""" + try: + return self.file_manager_tab.get_input_files() + except Exception as e: + print(f"Warning: Could not get input files: {e}") + return [] def main(): diff --git a/gui/rpgmaker_tab.py b/gui/rpgmaker_tab.py index 4d8c8a1..f2b80bf 100644 --- a/gui/rpgmaker_tab.py +++ b/gui/rpgmaker_tab.py @@ -21,12 +21,131 @@ except ImportError: class RPGMakerTab(QWidget): """RPG Maker MV/MZ configuration tab.""" + # 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): super().__init__() 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): @@ -75,18 +194,13 @@ class RPGMakerTab(QWidget): button_layout = QHBoxLayout() self.reset_button = QPushButton("Reset to Defaults") - self.reset_button.clicked.connect(self.reset_to_defaults) + self.reset_button.clicked.connect(self.reset_to_defaults_with_message) self.validate_button = QPushButton("Validate Settings") self.validate_button.clicked.connect(self.validate_and_show_report) - self.apply_button = QPushButton("Apply to Module") - self.apply_button.clicked.connect(self.apply_to_module) - self.apply_button.setToolTip("Apply current settings to the rpgmakermvmz.py module file") - button_layout.addWidget(self.reset_button) button_layout.addWidget(self.validate_button) - button_layout.addWidget(self.apply_button) button_layout.addStretch() scroll_layout.addLayout(button_layout) @@ -263,29 +377,105 @@ class RPGMakerTab(QWidget): def reset_to_defaults(self): """Reset all settings to default values.""" - # General config defaults - self.first_line_speakers_cb.setChecked(False) - self.facename_101_cb.setChecked(False) - self.names_cb.setChecked(False) - self.br_flag_cb.setChecked(False) - self.fix_textwrap_cb.setChecked(True) - self.ignore_tl_text_cb.setChecked(False) + # Temporarily disconnect signals to avoid multiple apply calls + self.disconnect_auto_apply() - # Main dialogue codes (enabled by default) - self.code_401_cb.setChecked(True) - self.code_405_cb.setChecked(True) - self.code_102_cb.setChecked(True) + # Apply default values from the DEFAULT_CONFIG constant + self.first_line_speakers_cb.setChecked(self.DEFAULT_CONFIG["FIRSTLINESPEAKERS"]) + self.facename_101_cb.setChecked(self.DEFAULT_CONFIG["FACENAME101"]) + self.names_cb.setChecked(self.DEFAULT_CONFIG["NAMES"]) + self.br_flag_cb.setChecked(self.DEFAULT_CONFIG["BRFLAG"]) + self.fix_textwrap_cb.setChecked(self.DEFAULT_CONFIG["FIXTEXTWRAP"]) + self.ignore_tl_text_cb.setChecked(self.DEFAULT_CONFIG["IGNORETLTEXT"]) - # Optional codes (disabled by default) - self.code_101_cb.setChecked(False) - self.code_408_cb.setChecked(False) + # Main dialogue codes + self.code_401_cb.setChecked(self.DEFAULT_CONFIG["CODE401"]) + self.code_405_cb.setChecked(self.DEFAULT_CONFIG["CODE405"]) + self.code_102_cb.setChecked(self.DEFAULT_CONFIG["CODE102"]) - # Variable codes (disabled by default) - self.code_122_cb.setChecked(False) + # Optional codes + self.code_101_cb.setChecked(self.DEFAULT_CONFIG["CODE101"]) + self.code_408_cb.setChecked(self.DEFAULT_CONFIG["CODE408"]) - # Other codes (all disabled by default) + # Variable codes + self.code_122_cb.setChecked(self.DEFAULT_CONFIG["CODE122"]) + + # Other codes - apply defaults from the constant + for code, cb in self.other_code_checkboxes.items(): + default_key = f"CODE{code}" + if default_key in self.DEFAULT_CONFIG: + cb.setChecked(self.DEFAULT_CONFIG[default_key]) + else: + cb.setChecked(False) # Fallback to False if not in defaults + + # 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.facename_101_cb.stateChanged.disconnect() + self.names_cb.stateChanged.disconnect() + self.br_flag_cb.stateChanged.disconnect() + self.fix_textwrap_cb.stateChanged.disconnect() + self.ignore_tl_text_cb.stateChanged.disconnect() + + # Main dialogue codes + self.code_401_cb.stateChanged.disconnect() + self.code_405_cb.stateChanged.disconnect() + self.code_102_cb.stateChanged.disconnect() + + # Optional codes + self.code_101_cb.stateChanged.disconnect() + self.code_408_cb.stateChanged.disconnect() + + # Variable codes + self.code_122_cb.stateChanged.disconnect() + + # Other codes + for cb in self.other_code_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.facename_101_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.br_flag_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + self.fix_textwrap_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + self.ignore_tl_text_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + + # Main dialogue codes + self.code_401_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + self.code_405_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + self.code_102_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + + # Optional codes + self.code_101_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + self.code_408_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + + # Variable codes + self.code_122_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) + + # Other codes for cb in self.other_code_checkboxes.values(): - cb.setChecked(False) + cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) def get_config(self): """Get current configuration as dictionary.""" @@ -427,35 +617,60 @@ class RPGMakerTab(QWidget): return is_valid - def apply_to_module(self): + def apply_to_module(self, show_messages=False): """Apply current configuration to the rpgmakermvmz.py module.""" - if not self.config_integration: - QMessageBox.warning(self, "Warning", "Configuration integration not available") - return - try: config = self.get_config() - success = self.config_integration.update_rpgmaker_config(config) - if success: + # Direct file modification approach (completely silent) + module_path = Path(__file__).parent.parent / "modules" / "rpgmakermvmz.py" + + 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", "Configuration has been applied to modules/rpgmakermvmz.py\n\n" "The module will now use these settings when running translations." ) - else: - QMessageBox.warning(self, "Warning", "Failed to apply configuration to module") - except FileNotFoundError: - QMessageBox.critical( - self, - "Error", - "modules/rpgmakermvmz.py not found!\n\n" - "Make sure you're running the GUI from the correct directory." - ) except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to apply configuration:\n{str(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.""" diff --git a/gui/translation_tab.py b/gui/translation_tab.py new file mode 100644 index 0000000..22c3641 --- /dev/null +++ b/gui/translation_tab.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python3 +""" +Translation Execution Tab for DazedMTLTool GUI + +Handles running the translation process with configured settings. +Each module has its own default settings in its folder without backups. +""" + +import os +import json +from pathlib import Path +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox, + QComboBox, QTextEdit, QProgressBar, QMessageBox, QListWidget, + QListWidgetItem, QSplitter, QCheckBox +) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer +from PyQt5.QtGui import QFont, QColor + + +class TranslationWorker(QThread): + """Worker thread for translation process.""" + + progress_updated = pyqtSignal(int) + status_updated = pyqtSignal(str) + log_updated = pyqtSignal(str) + finished = pyqtSignal(bool) # True if successful + + def __init__(self, config, files_to_translate): + super().__init__() + self.config = config + self.files_to_translate = files_to_translate + self.is_cancelled = False + + def run(self): + """Run the translation process.""" + try: + total_files = len(self.files_to_translate) + if total_files == 0: + self.status_updated.emit("No files to translate") + self.finished.emit(False) + return + + self.status_updated.emit(f"Starting translation of {total_files} files...") + + for i, file_path in enumerate(self.files_to_translate): + if self.is_cancelled: + self.status_updated.emit("Translation cancelled") + self.finished.emit(False) + return + + progress = int((i / total_files) * 100) + self.progress_updated.emit(progress) + self.status_updated.emit(f"Translating: {file_path.name}") + self.log_updated.emit(f"Processing file: {file_path}") + + # Here we would call the actual translation module + success = self.translate_file(file_path) + + if not success: + self.log_updated.emit(f"Failed to translate: {file_path.name}") + self.status_updated.emit(f"Error translating {file_path.name}") + self.finished.emit(False) + return + else: + self.log_updated.emit(f"Successfully translated: {file_path.name}") + + self.progress_updated.emit(100) + self.status_updated.emit(f"Translation completed successfully!") + self.finished.emit(True) + + except Exception as e: + self.log_updated.emit(f"Translation error: {str(e)}") + self.status_updated.emit(f"Translation failed: {str(e)}") + self.finished.emit(False) + + def translate_file(self, file_path): + """Translate a single file using the appropriate module.""" + try: + import sys + from pathlib import Path + + # Add project root to path + project_root = Path(__file__).parent.parent + sys.path.insert(0, str(project_root)) + + # Import the main translation module + from modules.main import translate_file + + # Create module-specific config for this file + self.create_module_config(file_path) + + # Call the translation function + result = translate_file(str(file_path)) + return result + + except Exception as e: + self.log_updated.emit(f"Error in translate_file: {str(e)}") + return False + + def create_module_config(self, file_path): + """Create module-specific configuration without backups.""" + try: + # Determine the module based on file extension and content + module_name = self.determine_module(file_path) + + if module_name: + # Create module directory if it doesn't exist + module_dir = Path(__file__).parent.parent / "modules" / module_name + module_dir.mkdir(exist_ok=True) + + # Create default settings file + settings_file = module_dir / "settings.json" + + # Only create if it doesn't exist (no overwriting) + if not settings_file.exists(): + default_settings = self.get_default_settings(module_name) + with open(settings_file, 'w', encoding='utf-8') as f: + json.dump(default_settings, f, indent=2, ensure_ascii=False) + + self.log_updated.emit(f"Created default settings for {module_name}") + + except Exception as e: + self.log_updated.emit(f"Error creating module config: {str(e)}") + + def determine_module(self, file_path): + """Determine which module to use based on file type.""" + suffix = file_path.suffix.lower() + + if suffix == '.json': + # Check if it's RPG Maker format + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + if '"code":' in content or '"parameters":' in content: + return "rpgmakermvmz" + else: + return "json" + except: + return "json" + elif suffix == '.txt': + return "text" + elif suffix == '.csv': + return "csv" + elif suffix in ['.js', '.py']: + return "regex" + else: + return "text" # Default fallback + + def get_default_settings(self, module_name): + """Get default settings for a module.""" + base_settings = { + "enabled": True, + "backup": False, # No backups as requested + "output_directory": "translated", + "preserve_formatting": True + } + + if module_name == "rpgmakermvmz": + # Use the same defaults as defined in RPG Maker tab + rpg_defaults = { + # 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 + + # General settings + "FIRSTLINESPEAKERS": False, + "FACENAME101": False, + "NAMES": False, + "BRFLAG": False, + "FIXTEXTWRAP": True, + "IGNORETLTEXT": False, + + # All other codes disabled by default + "translate_dialogue": True, + "translate_choices": True, + "translate_items": False, + "translate_skills": False, + "translate_states": False, + "translate_actors": False, + "translate_classes": False, + "translate_weapons": False, + "translate_armors": False, + "translate_enemies": False, + "event_codes": ["401", "405", "102"] # Only main dialogue codes enabled + } + base_settings.update(rpg_defaults) + elif module_name == "json": + base_settings.update({ + "translate_strings": True, + "skip_keys": ["id", "index", "type"], + "nested_translation": True + }) + elif module_name == "text": + base_settings.update({ + "line_by_line": True, + "skip_empty_lines": True, + "preserve_whitespace": True + }) + elif module_name == "csv": + base_settings.update({ + "has_header": True, + "delimiter": ",", + "columns_to_translate": [] # Empty means translate all text columns + }) + + return base_settings + + def cancel(self): + """Cancel the translation process.""" + self.is_cancelled = True + + +class TranslationTab(QWidget): + """Translation execution tab.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + self.worker = None + self.setup_ui() + + def setup_ui(self): + """Set up the user interface.""" + layout = QVBoxLayout() + + # Title + title_label = QLabel("Translation Execution") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;") + layout.addWidget(title_label) + + # Description + desc_label = QLabel( + "Execute the translation process on selected files. " + "Each module will use its default settings stored in its own folder." + ) + desc_label.setWordWrap(True) + desc_label.setStyleSheet("color: #cccccc; margin-bottom: 10px;") + layout.addWidget(desc_label) + + # Main content + content_splitter = QSplitter(Qt.Vertical) + + # File selection and controls + controls_group = self.create_controls_section() + content_splitter.addWidget(controls_group) + + # Progress and log section + progress_group = self.create_progress_section() + content_splitter.addWidget(progress_group) + + content_splitter.setSizes([300, 200]) + layout.addWidget(content_splitter) + + # Status bar + self.status_label = QLabel("Ready to translate") + self.status_label.setStyleSheet("color: #cccccc; padding: 5px;") + layout.addWidget(self.status_label) + + self.setLayout(layout) + + def create_controls_section(self): + """Create the controls section.""" + group = QGroupBox("Translation Controls") + layout = QVBoxLayout() + + # File selection + file_layout = QHBoxLayout() + + file_label = QLabel("Files to translate:") + file_label.setStyleSheet("color: #ffffff;") + file_layout.addWidget(file_label) + + self.refresh_files_btn = QPushButton("Refresh File List") + self.refresh_files_btn.clicked.connect(self.refresh_file_list) + file_layout.addWidget(self.refresh_files_btn) + + file_layout.addStretch() + layout.addLayout(file_layout) + + # File list + self.file_list = QListWidget() + self.file_list.setSelectionMode(QListWidget.ExtendedSelection) + layout.addWidget(self.file_list) + + # Options + options_layout = QHBoxLayout() + + self.select_all_cb = QCheckBox("Select All Files") + self.select_all_cb.stateChanged.connect(self.toggle_select_all) + options_layout.addWidget(self.select_all_cb) + + self.dry_run_cb = QCheckBox("Dry Run (test without translating)") + options_layout.addWidget(self.dry_run_cb) + + options_layout.addStretch() + layout.addLayout(options_layout) + + # Action buttons + button_layout = QHBoxLayout() + + self.start_btn = QPushButton("Start Translation") + self.start_btn.clicked.connect(self.start_translation) + self.start_btn.setStyleSheet("QPushButton { background-color: #0a7a0a; }") + button_layout.addWidget(self.start_btn) + + self.stop_btn = QPushButton("Stop Translation") + self.stop_btn.clicked.connect(self.stop_translation) + self.stop_btn.setEnabled(False) + self.stop_btn.setStyleSheet("QPushButton { background-color: #aa0000; }") + button_layout.addWidget(self.stop_btn) + + button_layout.addStretch() + layout.addLayout(button_layout) + + group.setLayout(layout) + return group + + def create_progress_section(self): + """Create the progress and log section.""" + group = QGroupBox("Translation Progress") + layout = QVBoxLayout() + + # Progress bar + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + layout.addWidget(self.progress_bar) + + # Log output + log_label = QLabel("Translation Log:") + log_label.setStyleSheet("color: #ffffff; margin-top: 10px;") + layout.addWidget(log_label) + + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + self.log_output.setMaximumHeight(150) + layout.addWidget(self.log_output) + + # Clear log button + clear_layout = QHBoxLayout() + clear_layout.addStretch() + + self.clear_log_btn = QPushButton("Clear Log") + self.clear_log_btn.clicked.connect(self.clear_log) + clear_layout.addWidget(self.clear_log_btn) + + layout.addLayout(clear_layout) + + group.setLayout(layout) + return group + + def refresh_file_list(self): + """Refresh the list of available files.""" + self.file_list.clear() + + try: + # Get files from the file manager tab + if hasattr(self.parent_window, 'file_manager_tab'): + input_files = self.parent_window.file_manager_tab.get_input_files() + + for file_path in input_files: + item = QListWidgetItem(file_path.name) + item.setData(Qt.UserRole, str(file_path)) + item.setCheckState(Qt.Checked) # Default to checked + self.file_list.addItem(item) + + self.status_label.setText(f"Found {len(input_files)} files ready for translation") + else: + self.status_label.setText("File manager not available") + + except Exception as e: + self.status_label.setText(f"Error loading files: {str(e)}") + + def toggle_select_all(self, state): + """Toggle selection of all files.""" + check_state = Qt.Checked if state == Qt.Checked else Qt.Unchecked + + for i in range(self.file_list.count()): + item = self.file_list.item(i) + item.setCheckState(check_state) + + def get_selected_files(self): + """Get list of selected files.""" + selected_files = [] + + for i in range(self.file_list.count()): + item = self.file_list.item(i) + if item.checkState() == Qt.Checked: + file_path = Path(item.data(Qt.UserRole)) + selected_files.append(file_path) + + return selected_files + + def start_translation(self): + """Start the translation process.""" + selected_files = self.get_selected_files() + + if not selected_files: + QMessageBox.warning(self, "No Files Selected", "Please select at least one file to translate.") + return + + # Get configuration from other tabs + try: + config = {} + if hasattr(self.parent_window, 'config_tab'): + config.update(self.parent_window.config_tab.get_config()) + if hasattr(self.parent_window, 'rpgmaker_tab'): + config.update(self.parent_window.rpgmaker_tab.get_config()) + + except Exception as e: + QMessageBox.critical(self, "Configuration Error", f"Failed to get configuration:\n{str(e)}") + return + + # Confirm start + if not self.dry_run_cb.isChecked(): + reply = QMessageBox.question( + self, + "Confirm Translation", + f"Start translation of {len(selected_files)} files?\n\nThis will create translated files in the 'translated' directory.", + QMessageBox.Yes | QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + + # Start worker thread + self.worker = TranslationWorker(config, selected_files) + self.worker.progress_updated.connect(self.progress_bar.setValue) + self.worker.status_updated.connect(self.status_label.setText) + self.worker.log_updated.connect(self.add_log_message) + self.worker.finished.connect(self.on_translation_finished) + + # Update UI state + self.start_btn.setEnabled(False) + self.stop_btn.setEnabled(True) + self.progress_bar.setValue(0) + + # Start translation + self.worker.start() + self.add_log_message(f"Starting translation of {len(selected_files)} files...") + + def stop_translation(self): + """Stop the translation process.""" + if self.worker and self.worker.isRunning(): + self.worker.cancel() + self.add_log_message("Cancelling translation...") + self.status_label.setText("Stopping translation...") + + def on_translation_finished(self, success): + """Handle translation completion.""" + # Update UI state + self.start_btn.setEnabled(True) + self.stop_btn.setEnabled(False) + + if success: + self.add_log_message("Translation completed successfully!") + QMessageBox.information(self, "Translation Complete", "All files have been translated successfully!") + + # Refresh file manager if available + if hasattr(self.parent_window, 'file_manager_tab'): + self.parent_window.file_manager_tab.refresh_file_lists() + else: + self.add_log_message("Translation failed or was cancelled.") + + self.worker = None + + def add_log_message(self, message): + """Add a message to the log output.""" + import datetime + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + formatted_message = f"[{timestamp}] {message}" + self.log_output.append(formatted_message) + + # Auto-scroll to bottom + scrollbar = self.log_output.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def clear_log(self): + """Clear the log output.""" + self.log_output.clear() + self.add_log_message("Log cleared") diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index d6d2b65..9875345 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -87,12 +87,12 @@ TRANSLATION_CONFIG = TranslationConfig( LEAVE = False # Config (Default) -FIRSTLINESPEAKERS = False # If 1st line of 401 is a speaker, set to True (False) -FACENAME101 = False # Find Speakers in 101 Codes based on Face Name (False) -NAMES = False # Output a list of all the character names found (False) -BRFLAG = False # If the game uses
instead (False) -FIXTEXTWRAP = True # Overwrites textwrap (True) -IGNORETLTEXT = False # Ignores all translated text. (False) +FIRSTLINESPEAKERS = False +FACENAME101 = False +NAMES = False +BRFLAG = False +FIXTEXTWRAP = True +IGNORETLTEXT = False # Dialogue / Scroll / Choices (Main Codes) CODE401 = True @@ -100,8 +100,8 @@ CODE405 = True CODE102 = True # Optional -CODE101 = False # Turn this one on when names exist in 101 -CODE408 = False # Warning, translates comments and can inflate costs. +CODE101 = False +CODE408 = False # Variables CODE122 = False diff --git a/set_defaults.py b/set_defaults.py index 9ed9616..dfa67ce 100644 --- a/set_defaults.py +++ b/set_defaults.py @@ -6,19 +6,19 @@ def set_defaults(file_path): # Define the default values with comments defaults = { - 'FIRSTLINESPEAKERS': 'False # If 1st line of 401 is a speaker, set to True (False)', - 'FACENAME101': 'False # Find Speakers in 101 Codes based on Face Name (False)', - 'NAMES': 'False # Output a list of all the character names found (False)', - 'BRFLAG': 'False # If the game uses
instead (False)', - 'FIXTEXTWRAP': 'True # Overwrites textwrap (True)', - 'IGNORETLTEXT': 'False # Ignores all translated text. (False)', + 'FIRSTLINESPEAKERS': 'False', + 'FACENAME101': 'False', + 'NAMES': 'False', + 'BRFLAG': 'False', + 'FIXTEXTWRAP': 'True', + 'IGNORETLTEXT': 'False', # Dialogue / Scroll / Choices (Main Codes) 'CODE401': 'True', 'CODE405': 'True', 'CODE102': 'True', # Optional - 'CODE101': 'False # Turn this one on when names exist in 101', - 'CODE408': 'False # Warning, translates comments and can inflate costs.', + 'CODE101': 'False', + 'CODE408': 'False', # Variables 'CODE122': 'False', # Other