diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..22a61ff --- /dev/null +++ b/gui/__init__.py @@ -0,0 +1,23 @@ +""" +DazedMTLTool GUI Package +""" + +__version__ = "1.0.0" +__author__ = "DazedTranslations" + +# Package imports +from .main import DazedMTLGUI +from .config_tab import ConfigTab +from .rpgmaker_tab import RPGMakerTab +from .other_modules_tab import OtherModulesTab +from .log_viewer import LogViewer +from .file_manager import FileManager + +__all__ = [ + "DazedMTLGUI", + "ConfigTab", + "RPGMakerTab", + "OtherModulesTab", + "LogViewer", + "FileManager" +] diff --git a/gui/config_integration.py b/gui/config_integration.py new file mode 100644 index 0000000..1f02c7c --- /dev/null +++ b/gui/config_integration.py @@ -0,0 +1,220 @@ +""" +Configuration Integration Helper +Updates the actual module files with GUI configuration settings +""" + +import re +import os +from pathlib import Path +from typing import Dict, Any + + +class ConfigIntegration: + """Helper class to integrate GUI configuration with module files.""" + + def __init__(self): + self.modules_dir = Path("modules") + + def update_rpgmaker_config(self, config: Dict[str, Any]) -> bool: + """Update rpgmakermvmz.py with configuration from GUI.""" + module_path = self.modules_dir / "rpgmakermvmz.py" + + if not module_path.exists(): + raise FileNotFoundError(f"Module file not found: {module_path}") + + try: + # Read the current module file + 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) + + # Write back to file + with open(module_path, 'w', encoding='utf-8') as f: + f.write(updated_content) + + 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: + """Update configuration values in the module content.""" + lines = content.split('\n') + updated_lines = [] + + # Track which configurations we found and updated + found_configs = set() + + for line in lines: + updated_line = line + + # Look for configuration assignments + for config_key, config_value in config.items(): + # Match lines like: CONFIG_NAME = value # comment + pattern = rf'^({re.escape(config_key)})\s*=\s*.*?(#.*)?$' + match = re.match(pattern, line.strip()) + + if match: + # Preserve the comment if it exists + comment = match.group(2) if match.group(2) else "" + if comment: + comment = " " + comment + + # Create the new line with proper formatting + updated_line = f"{config_key} = {config_value}{comment}" + found_configs.add(config_key) + break + + updated_lines.append(updated_line) + + # Check if all configurations were found + missing_configs = set(config.keys()) - found_configs + if missing_configs: + print(f"Warning: Could not find these configurations in module: {missing_configs}") + + return '\n'.join(updated_lines) + + def update_env_file(self, config: Dict[str, Any], env_path: Path = None) -> bool: + """Update .env file with configuration.""" + if env_path is None: + env_path = Path(".env") + + try: + from dotenv import set_key + + for key, value in config.items(): + set_key(env_path, key, str(value)) + + return True + + except Exception as e: + print(f"Error updating .env file: {e}") + return False + + def read_current_config(self, module_path: Path = None) -> Dict[str, Any]: + """Read current configuration from module file.""" + if module_path is None: + module_path = self.modules_dir / "rpgmakermvmz.py" + + if not module_path.exists(): + return {} + + config = {} + + try: + with open(module_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Find configuration lines + config_patterns = [ + r'^(FIRSTLINESPEAKERS|FACENAME101|NAMES|BRFLAG|FIXTEXTWRAP|IGNORETLTEXT)\s*=\s*(True|False)', + r'^(CODE\d+)\s*=\s*(True|False)' + ] + + for line in content.split('\n'): + line = line.strip() + for pattern in config_patterns: + match = re.match(pattern, line) + if match: + key = match.group(1) + value = match.group(2) == 'True' + config[key] = value + + except Exception as e: + print(f"Error reading configuration from {module_path}: {e}") + + return config + + def validate_module_syntax(self, module_path: Path = None) -> bool: + """Validate that the module file has correct Python syntax.""" + if module_path is None: + module_path = self.modules_dir / "rpgmakermvmz.py" + + try: + with open(module_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Try to compile the code + compile(content, str(module_path), 'exec') + return True + + except SyntaxError as e: + print(f"Syntax error in {module_path}: {e}") + return False + except Exception as e: + print(f"Error validating {module_path}: {e}") + return False + + def create_config_template(self) -> Dict[str, Any]: + """Create a template configuration with default values.""" + return { + # General config + "FIRSTLINESPEAKERS": False, + "FACENAME101": False, + "NAMES": False, + "BRFLAG": False, + "FIXTEXTWRAP": True, + "IGNORETLTEXT": False, + + # Main dialogue codes + "CODE401": True, + "CODE405": True, + "CODE102": True, + + # Optional codes + "CODE101": False, + "CODE408": False, + + # Variable codes + "CODE122": False, + + # Other codes + "CODE355655": False, + "CODE357": False, + "CODE657": False, + "CODE356": False, + "CODE320": False, + "CODE324": False, + "CODE111": False, + "CODE108": False + } + + def get_config_descriptions(self) -> Dict[str, str]: + """Get descriptions for all configuration options.""" + return { + "FIRSTLINESPEAKERS": "If 1st line of 401 is a speaker, set to True", + "FACENAME101": "Find Speakers in 101 Codes based on Face Name", + "NAMES": "Output a list of all the character names found", + "BRFLAG": "If the game uses
instead of newlines", + "FIXTEXTWRAP": "Overwrites textwrap for better formatting", + "IGNORETLTEXT": "Ignores all translated text", + + "CODE401": "Show Text - Main dialogue content", + "CODE405": "Show Text (Scrolling) - Longer dialogue", + "CODE102": "Show Choices - Player choice options", + "CODE101": "Character Names - Turn on when names exist in 101", + "CODE408": "Comments - WARNING: Can inflate costs significantly", + "CODE122": "Control Variables - Text stored in variables", + + "CODE355655": "Scripts - Text within script commands", + "CODE357": "Picture Text - Text displayed on pictures", + "CODE657": "Picture Text Extended - Extended picture text", + "CODE356": "Plugin Commands - Plugin command parameters", + "CODE320": "Change Name Input - Name input prompts", + "CODE324": "Change Nickname - Nickname changes", + "CODE111": "Conditional Branch - Conditional text", + "CODE108": "Comments - Comment blocks" + } diff --git a/gui/config_tab.py b/gui/config_tab.py new file mode 100644 index 0000000..fc8996b --- /dev/null +++ b/gui/config_tab.py @@ -0,0 +1,503 @@ +""" +Configuration Tab - Handles environment variables and global settings +""" + +import os +from pathlib import Path +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, + QSpinBox, QDoubleSpinBox, QComboBox, QPushButton, QGroupBox, + QLabel, QFileDialog, QMessageBox, QScrollArea, QTextEdit, + QCheckBox, QApplication +) +from PyQt5.QtCore import Qt, pyqtSignal +from dotenv import load_dotenv, set_key + + +class ConfigTab(QWidget): + """Configuration tab for managing environment variables and global settings.""" + + config_changed = pyqtSignal() + + def __init__(self): + super().__init__() + self.env_file_path = Path(".env") + self.init_ui() + self.load_from_env() + + def init_ui(self): + """Initialize the user interface.""" + main_layout = QVBoxLayout() + + # Create scroll area + scroll_area = QScrollArea() + scroll_content = QWidget() + scroll_layout = QVBoxLayout() + + # API Configuration Group + api_group = self.create_api_group() + scroll_layout.addWidget(api_group) + + # Translation Settings Group + translation_group = self.create_translation_group() + scroll_layout.addWidget(translation_group) + + # Performance Settings Group + performance_group = self.create_performance_group() + scroll_layout.addWidget(performance_group) + + # Text Formatting Group + formatting_group = self.create_formatting_group() + scroll_layout.addWidget(formatting_group) + + # Custom API Settings Group + custom_api_group = self.create_custom_api_group() + scroll_layout.addWidget(custom_api_group) + + # UI Settings Group + ui_group = self.create_ui_settings_group() + scroll_layout.addWidget(ui_group) + + # Buttons + button_layout = QHBoxLayout() + + self.save_button = QPushButton("Save Configuration") + self.save_button.clicked.connect(self.save_to_env) + + self.load_button = QPushButton("Load from File") + self.load_button.clicked.connect(self.load_from_file_dialog) + + self.reset_button = QPushButton("Reset to Defaults") + self.reset_button.clicked.connect(self.reset_to_defaults) + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.load_button) + button_layout.addWidget(self.reset_button) + button_layout.addStretch() + + scroll_layout.addLayout(button_layout) + + scroll_content.setLayout(scroll_layout) + scroll_area.setWidget(scroll_content) + scroll_area.setWidgetResizable(True) + + main_layout.addWidget(scroll_area) + self.setLayout(main_layout) + + def create_api_group(self): + """Create API configuration group.""" + group = QGroupBox("API Configuration") + layout = QFormLayout() + + # API URL + self.api_url_edit = QLineEdit() + self.api_url_edit.setPlaceholderText("Leave blank for OpenAI API") + layout.addRow("API URL:", self.api_url_edit) + + # API Key + self.api_key_edit = QLineEdit() + self.api_key_edit.setEchoMode(QLineEdit.Password) + self.api_key_edit.setPlaceholderText("Enter your API key") + layout.addRow("API Key:", self.api_key_edit) + + # Organization + self.organization_edit = QLineEdit() + self.organization_edit.setPlaceholderText("Organization key") + layout.addRow("Organization:", self.organization_edit) + + # Model + self.model_combo = QComboBox() + self.model_combo.setEditable(True) + self.model_combo.addItems([ + "gpt-3.5-turbo", + "gpt-3.5-turbo-1106", + "gpt-4", + "gpt-4-1106-preview", + "gpt-4-turbo", + "claude-3-sonnet-20240229", + "claude-3-opus-20240229" + ]) + layout.addRow("Model:", self.model_combo) + + group.setLayout(layout) + return group + + def create_translation_group(self): + """Create translation settings group.""" + group = QGroupBox("Translation Settings") + layout = QFormLayout() + + # Language + self.language_combo = QComboBox() + self.language_combo.addItems([ + "English", "Spanish", "French", "German", "Italian", + "Portuguese", "Russian", "Chinese", "Korean", "Japanese" + ]) + layout.addRow("Target Language:", self.language_combo) + + # Timeout + self.timeout_spin = QSpinBox() + self.timeout_spin.setRange(30, 300) + self.timeout_spin.setValue(120) + self.timeout_spin.setSuffix(" seconds") + layout.addRow("Timeout:", self.timeout_spin) + + group.setLayout(layout) + return group + + def create_performance_group(self): + """Create performance settings group.""" + group = QGroupBox("Performance Settings") + layout = QFormLayout() + + # File Threads + self.file_threads_spin = QSpinBox() + self.file_threads_spin.setRange(1, 10) + self.file_threads_spin.setValue(1) + layout.addRow("File Threads:", self.file_threads_spin) + + # Threads per File + self.threads_spin = QSpinBox() + self.threads_spin.setRange(1, 20) + self.threads_spin.setValue(1) + layout.addRow("Threads per File:", self.threads_spin) + + # Batch Size + self.batch_size_spin = QSpinBox() + self.batch_size_spin.setRange(1, 100) + self.batch_size_spin.setValue(10) + layout.addRow("Batch Size:", self.batch_size_spin) + + # Frequency Penalty + self.frequency_penalty_spin = QDoubleSpinBox() + self.frequency_penalty_spin.setRange(0.0, 2.0) + self.frequency_penalty_spin.setSingleStep(0.1) + self.frequency_penalty_spin.setValue(0.2) + layout.addRow("Frequency Penalty:", self.frequency_penalty_spin) + + group.setLayout(layout) + return group + + def create_formatting_group(self): + """Create text formatting settings group.""" + group = QGroupBox("Text Formatting") + layout = QFormLayout() + + # Dialogue Width + self.width_spin = QSpinBox() + self.width_spin.setRange(20, 200) + self.width_spin.setValue(60) + self.width_spin.setSuffix(" characters") + layout.addRow("Dialogue Width:", self.width_spin) + + # List Width + self.list_width_spin = QSpinBox() + self.list_width_spin.setRange(20, 200) + self.list_width_spin.setValue(100) + self.list_width_spin.setSuffix(" characters") + layout.addRow("List Width:", self.list_width_spin) + + # Note Width + self.note_width_spin = QSpinBox() + self.note_width_spin.setRange(20, 200) + self.note_width_spin.setValue(75) + self.note_width_spin.setSuffix(" characters") + layout.addRow("Note Width:", self.note_width_spin) + + group.setLayout(layout) + return group + + def create_custom_api_group(self): + """Create custom API settings group.""" + group = QGroupBox("Custom API Pricing") + layout = QFormLayout() + + # Input Cost + self.input_cost_spin = QDoubleSpinBox() + self.input_cost_spin.setRange(0.0, 1.0) + self.input_cost_spin.setDecimals(4) + self.input_cost_spin.setSingleStep(0.0001) + self.input_cost_spin.setValue(0.002) + self.input_cost_spin.setSuffix(" per 1K tokens") + layout.addRow("Input Cost:", self.input_cost_spin) + + # Output Cost + self.output_cost_spin = QDoubleSpinBox() + self.output_cost_spin.setRange(0.0, 1.0) + self.output_cost_spin.setDecimals(4) + self.output_cost_spin.setSingleStep(0.0001) + self.output_cost_spin.setValue(0.002) + self.output_cost_spin.setSuffix(" per 1K tokens") + layout.addRow("Output Cost:", self.output_cost_spin) + + group.setLayout(layout) + return group + + def create_ui_settings_group(self): + """Create UI settings group.""" + group = QGroupBox("User Interface Settings") + layout = QFormLayout() + + # Font Scale + self.font_scale_spin = QDoubleSpinBox() + self.font_scale_spin.setRange(0.5, 3.0) + self.font_scale_spin.setSingleStep(0.1) + self.font_scale_spin.setValue(1.0) + self.font_scale_spin.setDecimals(1) + self.font_scale_spin.setSuffix("x") + self.font_scale_spin.setToolTip("Scale factor for all fonts in the GUI (1.0 = normal, 1.5 = 150% larger)") + self.font_scale_spin.valueChanged.connect(self.on_font_scale_changed) + + # Font scale presets + font_layout = QHBoxLayout() + font_layout.addWidget(self.font_scale_spin) + + # Quick preset buttons + small_btn = QPushButton("Small") + small_btn.clicked.connect(lambda: self.font_scale_spin.setValue(0.8)) + small_btn.setMaximumWidth(60) + + normal_btn = QPushButton("Normal") + normal_btn.clicked.connect(lambda: self.font_scale_spin.setValue(1.0)) + normal_btn.setMaximumWidth(60) + + large_btn = QPushButton("Large") + large_btn.clicked.connect(lambda: self.font_scale_spin.setValue(1.5)) + large_btn.setMaximumWidth(60) + + xlarge_btn = QPushButton("X-Large") + xlarge_btn.clicked.connect(lambda: self.font_scale_spin.setValue(2.0)) + xlarge_btn.setMaximumWidth(60) + + font_layout.addWidget(small_btn) + font_layout.addWidget(normal_btn) + font_layout.addWidget(large_btn) + font_layout.addWidget(xlarge_btn) + + layout.addRow("Font Scale:", font_layout) + + # DPI Awareness + self.dpi_aware_cb = QCheckBox("High DPI Scaling") + self.dpi_aware_cb.setToolTip("Enable automatic scaling for high DPI displays") + self.dpi_aware_cb.setChecked(True) + layout.addRow("", self.dpi_aware_cb) + + # Theme selection + self.theme_combo = QComboBox() + self.theme_combo.addItems(["Dark", "Light", "System"]) + self.theme_combo.setCurrentText("Dark") + layout.addRow("Theme:", self.theme_combo) + + # Show font tip + self.show_font_tip_cb = QCheckBox("Show font size tip on startup") + self.show_font_tip_cb.setToolTip("Display helpful font sizing information when the GUI starts") + self.show_font_tip_cb.setChecked(True) + layout.addRow("", self.show_font_tip_cb) + + group.setLayout(layout) + return group + + def on_font_scale_changed(self, value): + """Handle font scale change.""" + self.config_changed.emit() + # Apply font scaling immediately + self.apply_font_scaling(value) + + def apply_font_scaling(self, scale_factor): + """Apply font scaling to the entire application.""" + app = QApplication.instance() + if app: + # Get the default font + font = app.font() + base_size = 9 # Base font size + new_size = int(base_size * scale_factor) + font.setPointSize(new_size) + app.setFont(font) + + # Emit signal to notify other components + self.config_changed.emit() + + def load_from_env(self): + """Load configuration from .env file.""" + if self.env_file_path.exists(): + load_dotenv(self.env_file_path) + + # Load API settings + self.api_url_edit.setText(os.getenv("api", "")) + self.api_key_edit.setText(os.getenv("key", "")) + self.organization_edit.setText(os.getenv("organization", "")) + self.model_combo.setCurrentText(os.getenv("model", "gpt-4")) + + # Load translation settings + self.language_combo.setCurrentText(os.getenv("language", "English")) + self.timeout_spin.setValue(int(os.getenv("timeout", "120"))) + + # Load performance settings + self.file_threads_spin.setValue(int(os.getenv("fileThreads", "1"))) + self.threads_spin.setValue(int(os.getenv("threads", "1"))) + self.batch_size_spin.setValue(int(os.getenv("batchsize", "10"))) + self.frequency_penalty_spin.setValue(float(os.getenv("frequency_penalty", "0.2"))) + + # Load formatting settings + self.width_spin.setValue(int(os.getenv("width", "60"))) + self.list_width_spin.setValue(int(os.getenv("listWidth", "100"))) + self.note_width_spin.setValue(int(os.getenv("noteWidth", "75"))) + + # Load custom API settings + self.input_cost_spin.setValue(float(os.getenv("input_cost", "0.002"))) + self.output_cost_spin.setValue(float(os.getenv("output_cost", "0.002"))) + + # Load UI settings + self.font_scale_spin.setValue(float(os.getenv("font_scale", "1.0"))) + self.dpi_aware_cb.setChecked(os.getenv("dpi_aware", "true").lower() == "true") + self.theme_combo.setCurrentText(os.getenv("theme", "Dark")) + self.show_font_tip_cb.setChecked(os.getenv("show_font_tip", "true").lower() == "true") + + # Apply font scaling + self.apply_font_scaling(self.font_scale_spin.value()) + + def save_to_env(self): + """Save configuration to .env file.""" + try: + # Ensure .env file exists + if not self.env_file_path.exists(): + self.env_file_path.touch() + + # Save API settings + set_key(self.env_file_path, "api", self.api_url_edit.text()) + set_key(self.env_file_path, "key", self.api_key_edit.text()) + set_key(self.env_file_path, "organization", self.organization_edit.text()) + set_key(self.env_file_path, "model", self.model_combo.currentText()) + + # Save translation settings + set_key(self.env_file_path, "language", self.language_combo.currentText()) + set_key(self.env_file_path, "timeout", str(self.timeout_spin.value())) + + # Save performance settings + set_key(self.env_file_path, "fileThreads", str(self.file_threads_spin.value())) + set_key(self.env_file_path, "threads", str(self.threads_spin.value())) + set_key(self.env_file_path, "batchsize", str(self.batch_size_spin.value())) + set_key(self.env_file_path, "frequency_penalty", str(self.frequency_penalty_spin.value())) + + # Save formatting settings + set_key(self.env_file_path, "width", str(self.width_spin.value())) + set_key(self.env_file_path, "listWidth", str(self.list_width_spin.value())) + set_key(self.env_file_path, "noteWidth", str(self.note_width_spin.value())) + + # Save custom API settings + set_key(self.env_file_path, "input_cost", str(self.input_cost_spin.value())) + set_key(self.env_file_path, "output_cost", str(self.output_cost_spin.value())) + + # Save UI settings + set_key(self.env_file_path, "font_scale", str(self.font_scale_spin.value())) + set_key(self.env_file_path, "dpi_aware", str(self.dpi_aware_cb.isChecked()).lower()) + set_key(self.env_file_path, "theme", self.theme_combo.currentText()) + set_key(self.env_file_path, "show_font_tip", str(self.show_font_tip_cb.isChecked()).lower()) + + QMessageBox.information(self, "Success", "Configuration saved successfully!") + self.config_changed.emit() + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save configuration:\n{str(e)}") + + def load_from_file_dialog(self): + """Load configuration from a file via dialog.""" + file_path, _ = QFileDialog.getOpenFileName( + self, "Load Configuration", "", "Environment Files (*.env);;All Files (*)" + ) + + if file_path: + self.load_from_file(file_path) + + def load_from_file(self, file_path): + """Load configuration from a specific file.""" + try: + load_dotenv(file_path, override=True) + self.load_from_env() + QMessageBox.information(self, "Success", f"Configuration loaded from {Path(file_path).name}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load configuration:\n{str(e)}") + + def reset_to_defaults(self): + """Reset all settings to default values.""" + # API settings + self.api_url_edit.clear() + self.api_key_edit.clear() + self.organization_edit.clear() + self.model_combo.setCurrentText("gpt-4") + + # Translation settings + self.language_combo.setCurrentText("English") + self.timeout_spin.setValue(120) + + # Performance settings + self.file_threads_spin.setValue(1) + self.threads_spin.setValue(1) + self.batch_size_spin.setValue(10) + self.frequency_penalty_spin.setValue(0.2) + + # Formatting settings + self.width_spin.setValue(60) + self.list_width_spin.setValue(100) + self.note_width_spin.setValue(75) + + # Custom API settings + self.input_cost_spin.setValue(0.002) + self.output_cost_spin.setValue(0.002) + + # UI settings + self.font_scale_spin.setValue(1.0) + self.dpi_aware_cb.setChecked(True) + self.theme_combo.setCurrentText("Dark") + self.show_font_tip_cb.setChecked(True) + + # Apply font scaling + self.apply_font_scaling(1.0) + + def get_config(self): + """Get current configuration as dictionary.""" + return { + "api": self.api_url_edit.text(), + "key": self.api_key_edit.text(), + "organization": self.organization_edit.text(), + "model": self.model_combo.currentText(), + "language": self.language_combo.currentText(), + "timeout": self.timeout_spin.value(), + "fileThreads": self.file_threads_spin.value(), + "threads": self.threads_spin.value(), + "batchsize": self.batch_size_spin.value(), + "frequency_penalty": self.frequency_penalty_spin.value(), + "width": self.width_spin.value(), + "listWidth": self.list_width_spin.value(), + "noteWidth": self.note_width_spin.value(), + "input_cost": self.input_cost_spin.value(), + "output_cost": self.output_cost_spin.value(), + "font_scale": self.font_scale_spin.value(), + "dpi_aware": self.dpi_aware_cb.isChecked(), + "theme": self.theme_combo.currentText(), + "show_font_tip": self.show_font_tip_cb.isChecked() + } + + def validate(self): + """Validate the current configuration.""" + errors = [] + + # Check required fields + if not self.api_key_edit.text().strip(): + errors.append("API Key is required") + + if not self.model_combo.currentText().strip(): + errors.append("Model is required") + + # Check numeric ranges + if self.timeout_spin.value() < 30: + errors.append("Timeout should be at least 30 seconds") + + if self.threads_spin.value() > 10 and "gpt-4" in self.model_combo.currentText().lower(): + errors.append("Too many threads for GPT-4 - recommended: 1-2") + + if errors: + QMessageBox.warning(self, "Validation Errors", "\n".join(errors)) + return False + + return True diff --git a/gui/file_manager.py b/gui/file_manager.py new file mode 100644 index 0000000..868728d --- /dev/null +++ b/gui/file_manager.py @@ -0,0 +1,405 @@ +""" +File Manager - Handle input and output files +""" + +from pathlib import Path +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, + QPushButton, QLabel, QFileDialog, QMessageBox, QGroupBox, + QSplitter, QTextEdit, QProgressBar, QCheckBox +) +from PyQt5.QtCore import Qt, QThread, pyqtSignal +import json + + +class FileManager(QWidget): + """Widget for managing input and output files.""" + + files_changed = pyqtSignal() + + def __init__(self): + super().__init__() + self.input_files = [] + self.output_directory = Path("translated") + self.init_ui() + self.refresh_files() + + def init_ui(self): + """Initialize the user interface.""" + layout = QVBoxLayout() + + # Create splitter for input and output sections + splitter = QSplitter(Qt.Horizontal) + + # Input files section + input_group = self.create_input_section() + splitter.addWidget(input_group) + + # Output files section + output_group = self.create_output_section() + splitter.addWidget(output_group) + + splitter.setSizes([400, 400]) + layout.addWidget(splitter) + + # Progress section + progress_group = self.create_progress_section() + layout.addWidget(progress_group) + + self.setLayout(layout) + + def create_input_section(self): + """Create the input files section.""" + group = QGroupBox("Input Files") + layout = QVBoxLayout() + + # Buttons + button_layout = QHBoxLayout() + + self.add_files_button = QPushButton("Add Files") + self.add_files_button.clicked.connect(self.add_input_files) + + self.add_folder_button = QPushButton("Add Folder") + self.add_folder_button.clicked.connect(self.add_input_folder) + + self.remove_files_button = QPushButton("Remove Selected") + self.remove_files_button.clicked.connect(self.remove_selected_files) + + self.clear_files_button = QPushButton("Clear All") + self.clear_files_button.clicked.connect(self.clear_input_files) + + button_layout.addWidget(self.add_files_button) + button_layout.addWidget(self.add_folder_button) + button_layout.addWidget(self.remove_files_button) + button_layout.addWidget(self.clear_files_button) + + layout.addLayout(button_layout) + + # File tree + self.input_tree = QTreeWidget() + self.input_tree.setHeaderLabels(["File", "Size", "Type"]) + self.input_tree.itemSelectionChanged.connect(self.on_input_selection_changed) + layout.addWidget(self.input_tree) + + # File info + self.input_info = QTextEdit() + self.input_info.setMaximumHeight(100) + self.input_info.setReadOnly(True) + layout.addWidget(self.input_info) + + group.setLayout(layout) + return group + + def create_output_section(self): + """Create the output files section.""" + group = QGroupBox("Output Files") + layout = QVBoxLayout() + + # Output directory selection + dir_layout = QHBoxLayout() + + self.output_dir_label = QLabel(str(self.output_directory)) + self.output_dir_label.setStyleSheet("border: 1px solid #555; padding: 5px;") + + self.browse_output_button = QPushButton("Browse") + self.browse_output_button.clicked.connect(self.browse_output_directory) + + dir_layout.addWidget(QLabel("Output Directory:")) + dir_layout.addWidget(self.output_dir_label) + dir_layout.addWidget(self.browse_output_button) + + layout.addLayout(dir_layout) + + # Output file tree + self.output_tree = QTreeWidget() + self.output_tree.setHeaderLabels(["File", "Size", "Modified"]) + self.output_tree.itemSelectionChanged.connect(self.on_output_selection_changed) + layout.addWidget(self.output_tree) + + # Output file preview + self.output_preview = QTextEdit() + self.output_preview.setMaximumHeight(100) + self.output_preview.setReadOnly(True) + layout.addWidget(self.output_preview) + + # Output buttons + output_button_layout = QHBoxLayout() + + self.open_output_button = QPushButton("Open in Editor") + self.open_output_button.clicked.connect(self.open_output_file) + + self.delete_output_button = QPushButton("Delete Selected") + self.delete_output_button.clicked.connect(self.delete_output_file) + + output_button_layout.addWidget(self.open_output_button) + output_button_layout.addWidget(self.delete_output_button) + output_button_layout.addStretch() + + layout.addLayout(output_button_layout) + + group.setLayout(layout) + return group + + def create_progress_section(self): + """Create the progress monitoring section.""" + group = QGroupBox("Translation Progress") + layout = QVBoxLayout() + + # Progress bar + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + layout.addWidget(self.progress_bar) + + # Status and options + status_layout = QHBoxLayout() + + self.status_label = QLabel("Ready") + self.backup_cb = QCheckBox("Create Backups") + self.backup_cb.setChecked(True) + + status_layout.addWidget(self.status_label) + status_layout.addStretch() + status_layout.addWidget(self.backup_cb) + + layout.addLayout(status_layout) + + group.setLayout(layout) + return group + + def add_input_files(self): + """Add input files via dialog.""" + files, _ = QFileDialog.getOpenFileNames( + self, + "Select Input Files", + "", + "JSON Files (*.json);;All Files (*)" + ) + + for file_path in files: + if file_path not in self.input_files: + self.input_files.append(file_path) + + self.refresh_input_files() + + def add_input_folder(self): + """Add all files from a folder.""" + folder = QFileDialog.getExistingDirectory(self, "Select Input Folder") + + if folder: + folder_path = Path(folder) + for file_path in folder_path.rglob("*.json"): + file_str = str(file_path) + if file_str not in self.input_files: + self.input_files.append(file_str) + + self.refresh_input_files() + + def remove_selected_files(self): + """Remove selected input files.""" + selected_items = self.input_tree.selectedItems() + for item in selected_items: + file_path = item.text(0) + if file_path in self.input_files: + self.input_files.remove(file_path) + + self.refresh_input_files() + + def clear_input_files(self): + """Clear all input files.""" + self.input_files.clear() + self.refresh_input_files() + + def refresh_input_files(self): + """Refresh the input files display.""" + self.input_tree.clear() + + for file_path in self.input_files: + path_obj = Path(file_path) + if path_obj.exists(): + size = path_obj.stat().st_size + file_type = path_obj.suffix + + item = QTreeWidgetItem([ + path_obj.name, + f"{size:,} bytes", + file_type + ]) + item.setToolTip(0, str(path_obj)) + self.input_tree.addTopLevelItem(item) + else: + # File doesn't exist - show in red + item = QTreeWidgetItem([ + f"{path_obj.name} (Missing)", + "0 bytes", + "N/A" + ]) + item.setBackground(0, Qt.red) + self.input_tree.addTopLevelItem(item) + + # Update info + self.update_input_info() + + def refresh_output_files(self): + """Refresh the output files display.""" + self.output_tree.clear() + + if self.output_directory.exists(): + for file_path in self.output_directory.iterdir(): + if file_path.is_file(): + size = file_path.stat().st_size + modified = file_path.stat().st_mtime + + import datetime + modified_str = datetime.datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M") + + item = QTreeWidgetItem([ + file_path.name, + f"{size:,} bytes", + modified_str + ]) + item.setToolTip(0, str(file_path)) + self.output_tree.addTopLevelItem(item) + + def browse_output_directory(self): + """Browse for output directory.""" + directory = QFileDialog.getExistingDirectory( + self, "Select Output Directory", str(self.output_directory) + ) + + if directory: + self.output_directory = Path(directory) + self.output_dir_label.setText(str(self.output_directory)) + self.refresh_output_files() + + def refresh_files(self): + """Refresh both input and output file displays.""" + self.refresh_input_files() + self.refresh_output_files() + + def on_input_selection_changed(self): + """Handle input file selection change.""" + selected_items = self.input_tree.selectedItems() + if selected_items: + item = selected_items[0] + file_name = item.text(0) + + # Find the full path + full_path = None + for file_path in self.input_files: + if Path(file_path).name == file_name: + full_path = file_path + break + + if full_path and Path(full_path).exists(): + try: + with open(full_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Show preview of file structure + if full_path.endswith('.json'): + data = json.loads(content) + if isinstance(data, dict): + keys = list(data.keys())[:10] # Show first 10 keys + preview = f"JSON Object with {len(data)} keys:\n" + preview += "\n".join(keys) + if len(data) > 10: + preview += f"\n... and {len(data) - 10} more" + elif isinstance(data, list): + preview = f"JSON Array with {len(data)} items" + else: + preview = f"JSON: {type(data).__name__}" + else: + preview = content[:500] + "..." if len(content) > 500 else content + + self.input_info.setPlainText(preview) + except Exception as e: + self.input_info.setPlainText(f"Error reading file: {str(e)}") + + def on_output_selection_changed(self): + """Handle output file selection change.""" + selected_items = self.output_tree.selectedItems() + if selected_items: + item = selected_items[0] + file_name = item.text(0) + file_path = self.output_directory / file_name + + if file_path.exists(): + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + preview = content[:500] + "..." if len(content) > 500 else content + self.output_preview.setPlainText(preview) + except Exception as e: + self.output_preview.setPlainText(f"Error reading file: {str(e)}") + + def open_output_file(self): + """Open selected output file in external editor.""" + selected_items = self.output_tree.selectedItems() + if selected_items: + item = selected_items[0] + file_name = item.text(0) + file_path = self.output_directory / file_name + + if file_path.exists(): + import subprocess + try: + subprocess.run(['notepad.exe', str(file_path)], check=True) + except subprocess.CalledProcessError: + QMessageBox.warning(self, "Warning", "Could not open file in external editor") + + def delete_output_file(self): + """Delete selected output file.""" + selected_items = self.output_tree.selectedItems() + if selected_items: + item = selected_items[0] + file_name = item.text(0) + file_path = self.output_directory / file_name + + reply = QMessageBox.question( + self, + "Confirm Deletion", + f"Are you sure you want to delete '{file_name}'?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes and file_path.exists(): + try: + file_path.unlink() + self.refresh_output_files() + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}") + + def update_input_info(self): + """Update the input files information.""" + total_files = len(self.input_files) + total_size = 0 + + for file_path in self.input_files: + path_obj = Path(file_path) + if path_obj.exists(): + total_size += path_obj.stat().st_size + + info = f"Total Files: {total_files}\nTotal Size: {total_size:,} bytes" + + # Count by file type + extensions = {} + for file_path in self.input_files: + ext = Path(file_path).suffix + extensions[ext] = extensions.get(ext, 0) + 1 + + if extensions: + info += "\n\nFile Types:" + for ext, count in sorted(extensions.items()): + info += f"\n{ext or 'No extension'}: {count}" + + self.input_info.setPlainText(info) + + def get_input_files(self): + """Get list of input files.""" + return self.input_files.copy() + + def get_output_directory(self): + """Get output directory path.""" + return self.output_directory diff --git a/gui/log_viewer.py b/gui/log_viewer.py new file mode 100644 index 0000000..8985555 --- /dev/null +++ b/gui/log_viewer.py @@ -0,0 +1,149 @@ +""" +Log Viewer - Real-time log display and monitoring +""" + +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, + QLabel, QCheckBox, QComboBox, QGroupBox, QSplitter +) +from PyQt5.QtCore import Qt, QTimer, pyqtSignal +from PyQt5.QtGui import QTextCursor, QFont +from pathlib import Path +import datetime + + +class LogViewer(QWidget): + """Widget for viewing translation logs and monitoring progress.""" + + log_updated = pyqtSignal(str) + + def __init__(self): + super().__init__() + self.log_files = { + "Translation History": "log/translationHistory.txt", + "Mismatch History": "log/mismatchHistory.txt" + } + self.auto_refresh_timer = QTimer() + self.auto_refresh_timer.timeout.connect(self.refresh_logs) + self.init_ui() + + def init_ui(self): + """Initialize the user interface.""" + layout = QVBoxLayout() + + # Control panel + control_group = QGroupBox("Log Controls") + control_layout = QHBoxLayout() + + # Log file selector + self.log_selector = QComboBox() + self.log_selector.addItems(list(self.log_files.keys())) + self.log_selector.currentTextChanged.connect(self.load_selected_log) + control_layout.addWidget(QLabel("Log File:")) + control_layout.addWidget(self.log_selector) + + # Auto-refresh checkbox + self.auto_refresh_cb = QCheckBox("Auto Refresh") + self.auto_refresh_cb.toggled.connect(self.toggle_auto_refresh) + control_layout.addWidget(self.auto_refresh_cb) + + # Manual refresh button + self.refresh_button = QPushButton("Refresh") + self.refresh_button.clicked.connect(self.refresh_logs) + control_layout.addWidget(self.refresh_button) + + # Clear button + self.clear_button = QPushButton("Clear") + self.clear_button.clicked.connect(self.clear_log) + control_layout.addWidget(self.clear_button) + + control_layout.addStretch() + control_group.setLayout(control_layout) + layout.addWidget(control_group) + + # Log display area + self.log_display = QTextEdit() + self.log_display.setReadOnly(True) + self.log_display.setFont(QFont("Consolas", 10)) + self.log_display.setStyleSheet(""" + QTextEdit { + background-color: #1e1e1e; + color: #ffffff; + border: 1px solid #555555; + } + """) + layout.addWidget(self.log_display) + + # Status bar + self.status_label = QLabel("Ready") + self.status_label.setStyleSheet("color: #cccccc; padding: 5px;") + layout.addWidget(self.status_label) + + self.setLayout(layout) + + # Initial load + self.load_selected_log() + + def toggle_auto_refresh(self, enabled): + """Toggle auto-refresh functionality.""" + if enabled: + self.auto_refresh_timer.start(2000) # Refresh every 2 seconds + self.status_label.setText("Auto-refresh enabled") + else: + self.auto_refresh_timer.stop() + self.status_label.setText("Auto-refresh disabled") + + def load_selected_log(self): + """Load the currently selected log file.""" + selected_log = self.log_selector.currentText() + if selected_log in self.log_files: + self.load_log_file(self.log_files[selected_log]) + + def load_log_file(self, file_path): + """Load content from a specific log file.""" + try: + log_path = Path(file_path) + if log_path.exists(): + with open(log_path, 'r', encoding='utf-8') as f: + content = f.read() + self.log_display.setPlainText(content) + + # Scroll to bottom + cursor = self.log_display.textCursor() + cursor.movePosition(QTextCursor.End) + self.log_display.setTextCursor(cursor) + + # Update status + file_size = log_path.stat().st_size + self.status_label.setText(f"Loaded {log_path.name} ({file_size} bytes)") + else: + self.log_display.setPlainText(f"Log file not found: {file_path}") + self.status_label.setText("Log file not found") + except Exception as e: + self.log_display.setPlainText(f"Error loading log file: {str(e)}") + self.status_label.setText(f"Error: {str(e)}") + + def refresh_logs(self): + """Refresh the current log display.""" + self.load_selected_log() + + def clear_log(self): + """Clear the log display.""" + self.log_display.clear() + self.status_label.setText("Log cleared") + + def append_log_message(self, message): + """Append a message to the log display.""" + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + formatted_message = f"[{timestamp}] {message}" + + self.log_display.append(formatted_message) + + # Scroll to bottom + cursor = self.log_display.textCursor() + cursor.movePosition(QTextCursor.End) + self.log_display.setTextCursor(cursor) + + def get_current_log_content(self): + """Get the current log content.""" + return self.log_display.toPlainText() diff --git a/gui/main.py b/gui/main.py new file mode 100644 index 0000000..579f2fc --- /dev/null +++ b/gui/main.py @@ -0,0 +1,623 @@ +#!/usr/bin/env python3 +""" +DazedMTLTool GUI - Main Application +A PyQt-based graphical user interface for the DazedMTLTool translation system. +""" + +import sys +import os +import json +from pathlib import Path +from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QTabWidget, QVBoxLayout, QHBoxLayout, + QWidget, QPushButton, QLabel, QFileDialog, QMessageBox, QProgressBar, + QTextEdit, QSplitter, QGroupBox, QStatusBar +) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer +from PyQt5.QtGui import QIcon, QFont, QPixmap + +# Import configuration widgets +from gui.config_tab import ConfigTab +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 + +class DazedMTLGUI(QMainWindow): + """Main GUI window for the DazedMTLTool.""" + + def __init__(self): + super().__init__() + self.init_ui() + self.setup_status_bar() + self.setup_font_scaling() + + def setup_font_scaling(self): + """Set up font scaling based on configuration.""" + try: + from dotenv import load_dotenv + import os + + # Load environment variables + load_dotenv() + + # Get font scale setting + font_scale = float(os.getenv("font_scale", "1.0")) + + # Apply font scaling + self.apply_font_scaling(font_scale) + + except Exception as e: + print(f"Warning: Could not apply font scaling: {e}") + + def apply_font_scaling(self, scale_factor): + """Apply font scaling to the entire application.""" + try: + app = QApplication.instance() + if app: + # Get the default font + font = app.font() + base_size = 9 # Base font size + new_size = int(base_size * scale_factor) + font.setPointSize(new_size) + app.setFont(font) + + # Also update window title to show scaling + if scale_factor != 1.0: + self.setWindowTitle(f"DazedMTLTool - Visual Translation Interface (Font: {scale_factor:.1f}x)") + else: + self.setWindowTitle("DazedMTLTool - Visual Translation Interface") + + except Exception as e: + print(f"Warning: Could not apply font scaling: {e}") + + def init_ui(self): + """Initialize the user interface.""" + self.setWindowTitle("DazedMTLTool - Visual Translation Interface") + self.setGeometry(100, 100, 1200, 800) + + # Set window icon if available + icon_path = Path("screens/tool.png") + if icon_path.exists(): + self.setWindowIcon(QIcon(str(icon_path))) + + # Create central widget and main layout + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Create main splitter + main_splitter = QSplitter(Qt.Horizontal) + + # Create tab widget + self.tab_widget = QTabWidget() + self.tab_widget.setTabPosition(QTabWidget.North) + + # Add tabs + self.setup_tabs() + + # Create log viewer + self.log_viewer = LogViewer() + + # Add widgets to splitter + main_splitter.addWidget(self.tab_widget) + main_splitter.addWidget(self.log_viewer) + main_splitter.setSizes([800, 400]) + + # Set main layout + layout = QVBoxLayout() + layout.addWidget(main_splitter) + central_widget.setLayout(layout) + + # Create menu bar + self.create_menu_bar() + + def setup_tabs(self): + """Set up all the tabs in the interface.""" + # Configuration Tab + self.config_tab = ConfigTab() + self.config_tab.config_changed.connect(self.on_config_changed) + self.tab_widget.addTab(self.config_tab, "Configuration") + + # RPG Maker MV/MZ Tab + self.rpgmaker_tab = RPGMakerTab() + self.tab_widget.addTab(self.rpgmaker_tab, "RPG Maker MV/MZ") + + # Other Modules Tab + 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 + try: + config = self.config_tab.get_config() + font_scale = config.get("font_scale", 1.0) + self.apply_font_scaling(font_scale) + except Exception as e: + print(f"Warning: Could not apply configuration changes: {e}") + + def set_font_scale(self, scale_factor): + """Set the font scale and update the configuration.""" + try: + # Apply the scaling immediately + self.apply_font_scaling(scale_factor) + + # Update the configuration tab + self.config_tab.font_scale_spin.setValue(scale_factor) + + # Save to .env file + self.config_tab.save_to_env() + + self.status_label.setText(f"Font scale set to {scale_factor:.1f}x") + + except Exception as e: + QMessageBox.warning(self, "Warning", f"Failed to set font scale: {str(e)}") + + def create_menu_bar(self): + """Create the menu bar.""" + menubar = self.menuBar() + + # File menu + file_menu = menubar.addMenu('File') + + # Load project action + load_action = file_menu.addAction('Load Project') + load_action.triggered.connect(self.load_project) + + # Save project action + save_action = file_menu.addAction('Save Project') + save_action.triggered.connect(self.save_project) + + file_menu.addSeparator() + + # Exit action + exit_action = file_menu.addAction('Exit') + exit_action.triggered.connect(self.close) + + # Tools menu + tools_menu = menubar.addMenu('Tools') + + # Font Size submenu + font_menu = tools_menu.addMenu('Font Size') + + # Font size actions + small_font_action = font_menu.addAction('Small (0.8x)') + small_font_action.triggered.connect(lambda: self.set_font_scale(0.8)) + + normal_font_action = font_menu.addAction('Normal (1.0x)') + normal_font_action.triggered.connect(lambda: self.set_font_scale(1.0)) + + large_font_action = font_menu.addAction('Large (1.5x)') + large_font_action.triggered.connect(lambda: self.set_font_scale(1.5)) + + xlarge_font_action = font_menu.addAction('Extra Large (2.0x)') + xlarge_font_action.triggered.connect(lambda: self.set_font_scale(2.0)) + + tools_menu.addSeparator() + + # Reset to defaults + reset_action = tools_menu.addAction('Reset to Defaults') + reset_action.triggered.connect(self.reset_to_defaults) + + # Validate configuration + validate_action = tools_menu.addAction('Validate Configuration') + validate_action.triggered.connect(self.validate_configuration) + + # Help menu + help_menu = menubar.addMenu('Help') + + # About action + about_action = help_menu.addAction('About') + about_action.triggered.connect(self.show_about) + + def setup_status_bar(self): + """Set up the status bar.""" + self.status_bar = QStatusBar() + self.setStatusBar(self.status_bar) + + # Add status labels + self.status_label = QLabel("Ready") + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + + self.status_bar.addWidget(self.status_label) + self.status_bar.addPermanentWidget(self.progress_bar) + + def load_project(self): + """Load a project configuration.""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "Load Project Configuration", + "", + "JSON Files (*.json);;All Files (*)" + ) + + if file_path: + try: + # Load configuration from file + self.config_tab.load_from_file(file_path) + self.rpgmaker_tab.load_from_file(file_path) + self.status_label.setText(f"Loaded project: {Path(file_path).name}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load project:\n{str(e)}") + + def save_project(self): + """Save the current project configuration.""" + file_path, _ = QFileDialog.getSaveFileName( + self, + "Save Project Configuration", + "", + "JSON Files (*.json);;All Files (*)" + ) + + if file_path: + try: + # Save configuration to file + config_data = {} + config_data.update(self.config_tab.get_config()) + config_data.update(self.rpgmaker_tab.get_config()) + + import json + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=2, ensure_ascii=False) + + self.status_label.setText(f"Saved project: {Path(file_path).name}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save project:\n{str(e)}") + + def reset_to_defaults(self): + """Reset all configurations to default values.""" + reply = QMessageBox.question( + self, + "Reset to Defaults", + "Are you sure you want to reset all settings to their default values?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.config_tab.reset_to_defaults() + self.rpgmaker_tab.reset_to_defaults() + self.status_label.setText("Reset to defaults") + + def validate_configuration(self): + """Validate the current configuration.""" + try: + # Validate configuration settings + config_valid = self.config_tab.validate() + rpgmaker_valid = self.rpgmaker_tab.validate() + + if config_valid and rpgmaker_valid: + QMessageBox.information(self, "Validation", "Configuration is valid!") + self.status_label.setText("Configuration validated") + else: + QMessageBox.warning(self, "Validation", "Configuration has issues. Check the log for details.") + + except Exception as e: + QMessageBox.critical(self, "Validation Error", f"Failed to validate configuration:\n{str(e)}") + + def show_about(self): + """Show the about dialog.""" + QMessageBox.about( + self, + "About DazedMTLTool GUI", + """ +

DazedMTLTool GUI

+

A visual interface for the DazedMTLTool translation system.

+

This tool helps translate visual novels, RPG games, and other text-based content using AI translation services.

+

Features:

+ + """ + ) + + def update_status(self, message): + """Update the status bar message.""" + self.status_label.setText(message) + + def show_progress(self, show=True): + """Show or hide the progress bar.""" + self.progress_bar.setVisible(show) + + def set_progress(self, value): + """Set the progress bar value (0-100).""" + self.progress_bar.setValue(value) + + +def main(): + """Main entry point for the GUI application.""" + try: + # Enable high DPI scaling before creating QApplication + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + app = QApplication(sys.argv) + except Exception as e: + print(f"Failed to create QApplication: {e}") + print("Make sure PyQt5 is properly installed:") + print(" pip install PyQt5>=5.15.0") + return 1 + + # Set application properties + app.setApplicationName("DazedMTLTool") + app.setApplicationVersion("1.0") + app.setOrganizationName("DazedTranslations") + + # Apply dark theme (optional) + app.setStyleSheet(""" + QMainWindow { + background-color: #2b2b2b; + color: #ffffff; + } + QWidget { + background-color: #2b2b2b; + color: #ffffff; + } + QTabWidget::pane { + border: 1px solid #555555; + background-color: #3c3c3c; + } + QTabBar::tab { + background-color: #555555; + color: #ffffff; + padding: 8px 12px; + margin-right: 2px; + border: 1px solid #666666; + } + QTabBar::tab:selected { + background-color: #007acc; + color: #ffffff; + } + QTabBar::tab:hover { + background-color: #666666; + color: #ffffff; + } + QGroupBox { + font-weight: bold; + border: 2px solid #555555; + border-radius: 5px; + margin: 10px 0px; + padding-top: 10px; + color: #ffffff; + background-color: #3c3c3c; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + color: #007acc; + background-color: transparent; + } + QPushButton { + background-color: #0078d4; + color: white; + border: none; + padding: 6px 12px; + border-radius: 3px; + font-weight: bold; + } + QPushButton:hover { + background-color: #106ebe; + } + QPushButton:pressed { + background-color: #005a9e; + } + QCheckBox { + color: #ffffff; + spacing: 5px; + background-color: transparent; + } + QCheckBox::indicator { + width: 16px; + height: 16px; + } + QCheckBox::indicator:unchecked { + background-color: #404040; + border: 1px solid #555555; + } + QCheckBox::indicator:checked { + background-color: #0078d4; + border: 1px solid #0078d4; + } + QLabel { + color: #ffffff; + background-color: transparent; + } + QLineEdit { + background-color: #404040; + color: #ffffff; + border: 1px solid #555555; + padding: 4px; + border-radius: 2px; + } + QLineEdit:focus { + border: 2px solid #007acc; + } + QSpinBox, QDoubleSpinBox { + background-color: #404040; + color: #ffffff; + border: 1px solid #555555; + padding: 4px; + border-radius: 2px; + } + QSpinBox:focus, QDoubleSpinBox:focus { + border: 2px solid #007acc; + } + QComboBox { + background-color: #404040; + color: #ffffff; + border: 1px solid #555555; + padding: 4px; + border-radius: 2px; + } + QComboBox:focus { + border: 2px solid #007acc; + } + QComboBox::drop-down { + border: none; + background-color: #555555; + } + QComboBox::down-arrow { + image: none; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #ffffff; + } + QComboBox QAbstractItemView { + background-color: #404040; + color: #ffffff; + selection-background-color: #007acc; + border: 1px solid #555555; + } + QTextEdit { + background-color: #1e1e1e; + color: #ffffff; + border: 1px solid #555555; + selection-background-color: #007acc; + } + QScrollArea { + background-color: #2b2b2b; + border: none; + } + QScrollBar:vertical { + background-color: #404040; + width: 12px; + border-radius: 6px; + } + QScrollBar::handle:vertical { + background-color: #666666; + border-radius: 6px; + margin: 2px; + } + QScrollBar::handle:vertical:hover { + background-color: #007acc; + } + QScrollBar:horizontal { + background-color: #404040; + height: 12px; + border-radius: 6px; + } + QScrollBar::handle:horizontal { + background-color: #666666; + border-radius: 6px; + margin: 2px; + } + QScrollBar::handle:horizontal:hover { + background-color: #007acc; + } + QTreeWidget { + background-color: #1e1e1e; + color: #ffffff; + border: 1px solid #555555; + selection-background-color: #007acc; + } + QTreeWidget::item { + padding: 4px; + border-bottom: 1px solid #333333; + } + QTreeWidget::item:selected { + background-color: #007acc; + color: #ffffff; + } + QTreeWidget::item:hover { + background-color: #404040; + } + QHeaderView::section { + background-color: #555555; + color: #ffffff; + padding: 6px; + border: 1px solid #666666; + } + QStatusBar { + background-color: #2b2b2b; + color: #ffffff; + border-top: 1px solid #555555; + } + QProgressBar { + background-color: #404040; + border: 1px solid #555555; + border-radius: 3px; + text-align: center; + color: #ffffff; + } + QProgressBar::chunk { + background-color: #007acc; + border-radius: 2px; + } + QMenuBar { + background-color: #2b2b2b; + color: #ffffff; + border-bottom: 1px solid #555555; + } + QMenuBar::item { + padding: 6px 12px; + background-color: transparent; + } + QMenuBar::item:selected { + background-color: #007acc; + } + QMenu { + background-color: #404040; + color: #ffffff; + border: 1px solid #555555; + } + QMenu::item { + padding: 6px 12px; + } + QMenu::item:selected { + background-color: #007acc; + } + QFrame[frameShape="4"] { + color: #555555; + } + QFrame[frameShape="5"] { + color: #555555; + } + """) + + try: + # Create and show the main window + window = DazedMTLGUI() + window.show() + + # Show font adjustment tip on first run (optional) + try: + from dotenv import load_dotenv + load_dotenv() + show_font_tip = os.getenv("show_font_tip", "true").lower() == "true" + + if show_font_tip: + QMessageBox.information( + window, + "Font Size Adjustment", + "💡 Font too small?\n\n" + "• Use Tools → Font Size menu for quick adjustment\n" + "• Or go to Configuration tab → UI Settings\n" + "• Try 'Large (1.5x)' or 'Extra Large (2.0x)' for high DPI displays\n\n" + "This tip can be disabled in the Configuration tab." + ) + + # Set flag to not show again + from dotenv import set_key + set_key(".env", "show_font_tip", "false") + + except Exception: + pass # Ignore if .env operations fail + + # Start the application + return app.exec_() + except Exception as e: + print(f"Error starting GUI: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gui/other_modules_tab.py b/gui/other_modules_tab.py new file mode 100644 index 0000000..cbedd78 --- /dev/null +++ b/gui/other_modules_tab.py @@ -0,0 +1,182 @@ +""" +Other Modules Tab - Configuration for other translation modules +""" + +from pathlib import Path +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, + QGroupBox, QLabel, QScrollArea, QListWidget, QListWidgetItem, + QMessageBox, QTextEdit, QFrame +) +from PyQt5.QtCore import Qt, pyqtSignal + + +class OtherModulesTab(QWidget): + """Tab for configuring other translation modules.""" + + config_changed = pyqtSignal() + + def __init__(self): + super().__init__() + self.modules_info = self.get_modules_info() + self.init_ui() + + def init_ui(self): + """Initialize the user interface.""" + main_layout = QVBoxLayout() + + # Create scroll area + scroll_area = QScrollArea() + scroll_content = QWidget() + scroll_layout = QVBoxLayout() + + # Title + title_label = QLabel("Other Translation Modules") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;") + scroll_layout.addWidget(title_label) + + # Description + description_label = QLabel( + "Configure settings for other game engines and file formats supported by DazedMTLTool." + ) + description_label.setWordWrap(True) + description_label.setStyleSheet("color: #cccccc; margin-bottom: 10px;") + scroll_layout.addWidget(description_label) + + # Create module groups + for category, modules in self.modules_info.items(): + group = self.create_module_group(category, modules) + scroll_layout.addWidget(group) + + # Buttons + button_layout = QHBoxLayout() + + self.refresh_button = QPushButton("Refresh Modules") + self.refresh_button.clicked.connect(self.refresh_modules) + + self.enable_all_button = QPushButton("Enable All") + self.enable_all_button.clicked.connect(self.enable_all_modules) + + self.disable_all_button = QPushButton("Disable All") + self.disable_all_button.clicked.connect(self.disable_all_modules) + + button_layout.addWidget(self.refresh_button) + button_layout.addWidget(self.enable_all_button) + button_layout.addWidget(self.disable_all_button) + button_layout.addStretch() + + scroll_layout.addLayout(button_layout) + + scroll_content.setLayout(scroll_layout) + scroll_area.setWidget(scroll_content) + scroll_area.setWidgetResizable(True) + + main_layout.addWidget(scroll_area) + self.setLayout(main_layout) + + def get_modules_info(self): + """Get information about available modules.""" + return { + "Visual Novel Engines": { + "kirikiri": "Kirikiri engine games (.ks, .tjs files)", + "renpy": "Ren'Py visual novels (.rpy files)", + "tyrano": "TyranoBuilder games", + "nscript": "NScripter games", + "lune": "Lune engine games" + }, + "RPG Engines": { + "rpgmakerace": "RPG Maker Ace (legacy)", + "rpgmakerplugin": "RPG Maker plugins", + "unity": "Unity engine games", + "wolf": "Wolf RPG Editor games", + "wolf2": "Wolf RPG Editor 2 games" + }, + "File Formats": { + "json": "Generic JSON files", + "csv": "CSV/Excel files", + "text": "Plain text files", + "regex": "Custom regex patterns", + "images": "Image text extraction" + } + } + + def create_module_group(self, category, modules): + """Create a group for a category of modules.""" + group = QGroupBox(category) + layout = QVBoxLayout() + + for module_name, description in modules.items(): + # Check if module file exists + module_path = Path(f"modules/{module_name}.py") + exists = module_path.exists() + + checkbox = QCheckBox(f"{module_name.upper()}") + checkbox.setEnabled(exists) + checkbox.setToolTip(description) + + if not exists: + checkbox.setText(f"{module_name.upper()} (Not Found)") + checkbox.setStyleSheet("color: #ff6b6b;") + + # Add description label + desc_label = QLabel(description) + desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 20px;") + desc_label.setWordWrap(True) + + layout.addWidget(checkbox) + layout.addWidget(desc_label) + + # Store reference for later use + setattr(self, f"{module_name}_cb", checkbox) + + group.setLayout(layout) + return group + + def refresh_modules(self): + """Refresh the modules list.""" + # Clear current layout and rebuild + self.modules_info = self.get_modules_info() + + # Find all Python files in modules directory + modules_dir = Path("modules") + if modules_dir.exists(): + found_modules = [f.stem for f in modules_dir.glob("*.py") if f.stem != "__init__"] + + info_text = f"Found {len(found_modules)} module files:\n" + info_text += ", ".join(sorted(found_modules)) + + QMessageBox.information(self, "Modules Refreshed", info_text) + else: + QMessageBox.warning(self, "Warning", "Modules directory not found!") + + def enable_all_modules(self): + """Enable all available modules.""" + for category, modules in self.modules_info.items(): + for module_name in modules.keys(): + checkbox = getattr(self, f"{module_name}_cb", None) + if checkbox and checkbox.isEnabled(): + checkbox.setChecked(True) + + def disable_all_modules(self): + """Disable all modules.""" + for category, modules in self.modules_info.items(): + for module_name in modules.keys(): + checkbox = getattr(self, f"{module_name}_cb", None) + if checkbox: + checkbox.setChecked(False) + + def get_enabled_modules(self): + """Get list of enabled modules.""" + enabled = [] + for category, modules in self.modules_info.items(): + for module_name in modules.keys(): + checkbox = getattr(self, f"{module_name}_cb", None) + if checkbox and checkbox.isChecked(): + enabled.append(module_name) + return enabled + + def get_config(self): + """Get current configuration.""" + return { + "enabled_modules": self.get_enabled_modules() + } diff --git a/gui/rpgmaker_tab.py b/gui/rpgmaker_tab.py new file mode 100644 index 0000000..4d8c8a1 --- /dev/null +++ b/gui/rpgmaker_tab.py @@ -0,0 +1,473 @@ +""" +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 +) +from PyQt5.QtCore import Qt, pyqtSignal + +try: + from .config_integration import ConfigIntegration +except ImportError: + # Fallback if config_integration is not available + ConfigIntegration = None + + +class RPGMakerTab(QWidget): + """RPG Maker MV/MZ configuration tab.""" + + config_changed = pyqtSignal() + + def __init__(self): + super().__init__() + self.config_integration = ConfigIntegration() if ConfigIntegration else None + self.init_ui() + self.reset_to_defaults() + + def init_ui(self): + """Initialize the user interface.""" + main_layout = QVBoxLayout() + + # Create scroll area + scroll_area = QScrollArea() + scroll_content = QWidget() + scroll_layout = QVBoxLayout() + + # Title and description + title_label = QLabel("RPG Maker MV/MZ Translation Settings") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;") + scroll_layout.addWidget(title_label) + + description_label = QLabel( + "Configure which RPG Maker event codes to translate and additional options. " + "Each code represents a specific type of game content." + ) + description_label.setWordWrap(True) + description_label.setStyleSheet("color: #cccccc; margin-bottom: 10px;") + scroll_layout.addWidget(description_label) + + # General Config Group + general_group = self.create_general_config_group() + scroll_layout.addWidget(general_group) + + # Main Dialogue Codes Group + dialogue_group = self.create_dialogue_codes_group() + scroll_layout.addWidget(dialogue_group) + + # Optional Codes Group + optional_group = self.create_optional_codes_group() + scroll_layout.addWidget(optional_group) + + # Variable Codes Group + variable_group = self.create_variable_codes_group() + scroll_layout.addWidget(variable_group) + + # Other Codes Group + other_group = self.create_other_codes_group() + scroll_layout.addWidget(other_group) + + # Buttons + button_layout = QHBoxLayout() + + self.reset_button = QPushButton("Reset to Defaults") + self.reset_button.clicked.connect(self.reset_to_defaults) + + 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) + + # Add separator + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Sunken) + scroll_layout.addWidget(separator) + + # Add note about performance + note_text = QTextEdit() + note_text.setMaximumHeight(100) + note_text.setReadOnly(True) + note_text.setHtml(""" + Performance Notes:
+ • Enable only the codes you need to reduce translation time and costs
+ • CODE408 can significantly increase costs as it translates comments
+ • Main dialogue codes (401, 405, 102) are typically required for story content
+ • Test with a small file first to verify settings work correctly + """) + scroll_layout.addWidget(note_text) + + scroll_content.setLayout(scroll_layout) + scroll_area.setWidget(scroll_content) + scroll_area.setWidgetResizable(True) + + main_layout.addWidget(scroll_area) + self.setLayout(main_layout) + + def create_general_config_group(self): + """Create general configuration group.""" + group = QGroupBox("General Configuration") + layout = QVBoxLayout() + + # FIRSTLINESPEAKERS + self.first_line_speakers_cb = QCheckBox("First Line Speakers") + self.first_line_speakers_cb.setToolTip( + "If the first line of event code 401 contains a speaker name, enable this option" + ) + layout.addWidget(self.first_line_speakers_cb) + + # FACENAME101 + self.facename_101_cb = QCheckBox("Find Speakers in 101 Codes based on Face Name") + self.facename_101_cb.setToolTip( + "Use character face names to identify speakers in event code 101" + ) + layout.addWidget(self.facename_101_cb) + + # NAMES + self.names_cb = QCheckBox("Output Character Names List") + self.names_cb.setToolTip( + "Generate a list of all character names found during translation" + ) + layout.addWidget(self.names_cb) + + # BRFLAG + self.br_flag_cb = QCheckBox("Use
Tags") + self.br_flag_cb.setToolTip( + "Use
HTML tags instead of newlines for line breaks" + ) + layout.addWidget(self.br_flag_cb) + + # FIXTEXTWRAP + self.fix_textwrap_cb = QCheckBox("Fix Text Wrapping") + self.fix_textwrap_cb.setToolTip( + "Automatically fix text wrapping for better display" + ) + layout.addWidget(self.fix_textwrap_cb) + + # IGNORETLTEXT + self.ignore_tl_text_cb = QCheckBox("Ignore Already Translated Text") + self.ignore_tl_text_cb.setToolTip( + "Skip text that appears to already be translated" + ) + layout.addWidget(self.ignore_tl_text_cb) + + group.setLayout(layout) + return group + + def create_dialogue_codes_group(self): + """Create dialogue codes group.""" + group = QGroupBox("Main Dialogue Codes (Recommended)") + layout = QVBoxLayout() + + # CODE401 + self.code_401_cb = QCheckBox("CODE 401 - Show Text") + self.code_401_cb.setToolTip( + "Translate dialogue text displayed in message windows. Essential for story content." + ) + layout.addWidget(self.code_401_cb) + + # CODE405 + self.code_405_cb = QCheckBox("CODE 405 - Show Text (Scrolling)") + self.code_405_cb.setToolTip( + "Translate scrolling text messages. Used for longer dialogue passages." + ) + layout.addWidget(self.code_405_cb) + + # CODE102 + self.code_102_cb = QCheckBox("CODE 102 - Show Choices") + self.code_102_cb.setToolTip( + "Translate choice options presented to the player." + ) + layout.addWidget(self.code_102_cb) + + group.setLayout(layout) + return group + + def create_optional_codes_group(self): + """Create optional codes group.""" + group = QGroupBox("Optional Codes") + layout = QVBoxLayout() + + # CODE101 + self.code_101_cb = QCheckBox("CODE 101 - Character Names") + self.code_101_cb.setToolTip( + "Translate character names. Enable only when names are stored in code 101." + ) + layout.addWidget(self.code_101_cb) + + # CODE408 + self.code_408_cb = QCheckBox("CODE 408 - Comments (Warning: High Cost)") + self.code_408_cb.setToolTip( + "Translate comment text. WARNING: This can significantly increase translation costs!" + ) + self.code_408_cb.setStyleSheet("QCheckBox { color: #ff6b6b; }") + layout.addWidget(self.code_408_cb) + + group.setLayout(layout) + return group + + def create_variable_codes_group(self): + """Create variable codes group.""" + group = QGroupBox("Variable Codes") + layout = QVBoxLayout() + + # CODE122 + self.code_122_cb = QCheckBox("CODE 122 - Control Variables") + self.code_122_cb.setToolTip( + "Translate text stored in game variables." + ) + layout.addWidget(self.code_122_cb) + + group.setLayout(layout) + return group + + def create_other_codes_group(self): + """Create other codes group.""" + group = QGroupBox("Other Event Codes") + layout = QVBoxLayout() + + codes_info = [ + ("355655", "Scripts", "Translate text within script commands"), + ("357", "Picture Text", "Translate text displayed on pictures"), + ("657", "Picture Text Extended", "Extended picture text translation"), + ("356", "Plugin Commands", "Translate plugin command parameters"), + ("320", "Change Name Input", "Translate name input prompts"), + ("324", "Change Nickname", "Translate nickname changes"), + ("111", "Conditional Branch", "Translate conditional text"), + ("108", "Comments", "Translate comment blocks") + ] + + self.other_code_checkboxes = {} + + for code, name, tooltip in codes_info: + cb = QCheckBox(f"CODE {code} - {name}") + cb.setToolTip(tooltip) + layout.addWidget(cb) + self.other_code_checkboxes[code] = cb + + group.setLayout(layout) + return group + + 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) + + # Main dialogue codes (enabled by default) + self.code_401_cb.setChecked(True) + self.code_405_cb.setChecked(True) + self.code_102_cb.setChecked(True) + + # Optional codes (disabled by default) + self.code_101_cb.setChecked(False) + self.code_408_cb.setChecked(False) + + # Variable codes (disabled by default) + self.code_122_cb.setChecked(False) + + # Other codes (all disabled by default) + for cb in self.other_code_checkboxes.values(): + cb.setChecked(False) + + def get_config(self): + """Get current configuration as dictionary.""" + config = { + # General settings + "FIRSTLINESPEAKERS": self.first_line_speakers_cb.isChecked(), + "FACENAME101": self.facename_101_cb.isChecked(), + "NAMES": self.names_cb.isChecked(), + "BRFLAG": self.br_flag_cb.isChecked(), + "FIXTEXTWRAP": self.fix_textwrap_cb.isChecked(), + "IGNORETLTEXT": self.ignore_tl_text_cb.isChecked(), + + # Main dialogue codes + "CODE401": self.code_401_cb.isChecked(), + "CODE405": self.code_405_cb.isChecked(), + "CODE102": self.code_102_cb.isChecked(), + + # Optional codes + "CODE101": self.code_101_cb.isChecked(), + "CODE408": self.code_408_cb.isChecked(), + + # Variable codes + "CODE122": self.code_122_cb.isChecked(), + } + + # Other codes + for code, cb in self.other_code_checkboxes.items(): + config[f"CODE{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.facename_101_cb.setChecked(config.get("FACENAME101", False)) + self.names_cb.setChecked(config.get("NAMES", False)) + self.br_flag_cb.setChecked(config.get("BRFLAG", False)) + self.fix_textwrap_cb.setChecked(config.get("FIXTEXTWRAP", True)) + self.ignore_tl_text_cb.setChecked(config.get("IGNORETLTEXT", False)) + + # Main dialogue codes + self.code_401_cb.setChecked(config.get("CODE401", True)) + self.code_405_cb.setChecked(config.get("CODE405", True)) + self.code_102_cb.setChecked(config.get("CODE102", True)) + + # Optional codes + self.code_101_cb.setChecked(config.get("CODE101", False)) + self.code_408_cb.setChecked(config.get("CODE408", False)) + + # Variable codes + self.code_122_cb.setChecked(config.get("CODE122", False)) + + # Other codes + for code, cb in self.other_code_checkboxes.items(): + cb.setChecked(config.get(f"CODE{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.code_401_cb.isChecked() or + self.code_405_cb.isChecked() or + self.code_102_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.code_408_cb.isChecked(): + warnings.append("CODE 408 (Comments) is enabled. This can significantly increase translation costs!") + + # Check for conflicting options + if self.br_flag_cb.isChecked() and self.fix_textwrap_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): + """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: + 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)}") + + def load_from_module(self): + """Load configuration from the actual module file.""" + if not self.config_integration: + return + + try: + config = self.config_integration.read_current_config() + if config: + self.set_config(config) + QMessageBox.information(self, "Success", "Configuration loaded from module file") + else: + QMessageBox.warning(self, "Warning", "No configuration found in module file") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load from module:\n{str(e)}") diff --git a/requirements.txt b/requirements.txt index 04dd94c..84e0a9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ ruamel.yaml==0.17.32 tiktoken==0.8.0 tqdm==4.65.0 pillow==10.4.0 +PyQt5>=5.15.0 diff --git a/start_gui.py b/start_gui.py new file mode 100644 index 0000000..19860d9 --- /dev/null +++ b/start_gui.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +Launch script for DazedMTLTool GUI +""" + +import sys +import os +from pathlib import Path + +# Add the project root to Python path +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +def check_dependencies(): + """Check if required dependencies are installed.""" + missing_deps = [] + + try: + import PyQt5 + except ImportError: + missing_deps.append("PyQt5") + + try: + from dotenv import load_dotenv + except ImportError: + missing_deps.append("python-dotenv") + + if missing_deps: + print("Missing dependencies:") + for dep in missing_deps: + print(f" - {dep}") + print("\nPlease install them using:") + print(" pip install -r requirements_gui.txt") + return False + + return True + +def main(): + """Main entry point.""" + print("DazedMTLTool GUI Launcher") + print("=" * 40) + + # Check dependencies + if not check_dependencies(): + sys.exit(1) + + # Import and run GUI + try: + from gui.main import main as gui_main + gui_main() + except ImportError as e: + print(f"Error importing GUI modules: {e}") + print("Make sure all GUI files are in the 'gui' directory") + sys.exit(1) + except Exception as e: + print(f"Error starting GUI: {e}") + sys.exit(1) + +if __name__ == "__main__": + main()