diff --git a/.gitignore b/.gitignore
index ed861bf..5849736 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,7 +14,7 @@
*.script
__pycache__
-!screens/*.png
+!assets/*.png
!.pre-commit-config.yaml
!prompt.txt
!vocab_base.txt
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..eec38b1
Binary files /dev/null and b/assets/icon.png differ
diff --git a/gui/main.py b/gui/main.py
index 60fa823..c8a69cf 100644
--- a/gui/main.py
+++ b/gui/main.py
@@ -1,1052 +1,1084 @@
-#!/usr/bin/env python3
-"""
-DazedMTLTool GUI - Main Application
-A PyQt-based graphical user interface for the DazedMTLTool translation system.
-"""
-
-import re
-import sys
-import os
-import json
-import urllib.request
-import zipfile
-import shutil
-import tempfile
-from pathlib import Path
-from PyQt5.QtWidgets import (
- QApplication, QMainWindow, QTabWidget, QVBoxLayout, QHBoxLayout,
- QWidget, QPushButton, QLabel, QFileDialog, QMessageBox, QProgressBar,
- QTextEdit, QSplitter, QGroupBox, QStatusBar, QStackedWidget, QToolButton,
- QDialog
-)
-from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings
-from PyQt5.QtGui import QIcon, QFont, QPixmap, QScreen
-
-
-class UpdateThread(QThread):
- """Downloads and applies a tool update from the GitGud repository."""
-
- REPO_USER = "DazedAnon"
- REPO_NAME = "DazedMTLTool"
- REPO_BRANCH = "main"
- SHA_FILE = "last_update_sha.txt"
-
- # Paths (relative, top-level) that should never be touched during update
- PROTECTED = {".env", "venv", "log", "files", "translated",
- "vocab.txt", "last_update_sha.txt"}
-
- progress = pyqtSignal(str) # status message
- finished = pyqtSignal(bool, str) # (success, message)
-
- def __init__(self, check_only=False, parent=None):
- super().__init__(parent)
- self.check_only = check_only
-
- # ------------------------------------------------------------------ #
-
- def run(self):
- try:
- latest_sha = self._fetch_latest_sha()
- current_sha = self._read_stored_sha()
-
- if latest_sha == current_sha:
- self.finished.emit(True, "already_up_to_date")
- return
-
- if self.check_only:
- self.finished.emit(True, f"update_available:{latest_sha}")
- return
-
- self._download_and_apply(latest_sha)
-
- except Exception as exc:
- self.finished.emit(False, str(exc))
-
- # ------------------------------------------------------------------ #
-
- def _fetch_latest_sha(self):
- api = (
- f"https://gitgud.io/api/v4/projects/"
- f"{self.REPO_USER}%2F{self.REPO_NAME}"
- f"/repository/branches/{self.REPO_BRANCH}"
- )
- req = urllib.request.Request(api, headers={"User-Agent": "DazedMTLTool"})
- with urllib.request.urlopen(req, timeout=15) as resp:
- return json.loads(resp.read())["commit"]["id"]
-
- def _read_stored_sha(self):
- p = Path(self.SHA_FILE)
- return p.read_text().strip() if p.exists() else ""
-
- def _download_and_apply(self, latest_sha):
- zip_url = (
- f"https://gitgud.io/{self.REPO_USER}/{self.REPO_NAME}/-/archive/"
- f"{self.REPO_BRANCH}/{self.REPO_NAME}-{self.REPO_BRANCH}.zip"
- )
-
- self.progress.emit("Downloading update…")
- with tempfile.TemporaryDirectory() as tmp_dir:
- tmp = Path(tmp_dir)
- zip_path = tmp / "update.zip"
-
- req = urllib.request.Request(zip_url, headers={"User-Agent": "DazedMTLTool"})
- with urllib.request.urlopen(req, timeout=120) as resp, \
- open(zip_path, "wb") as fh:
- shutil.copyfileobj(resp, fh)
-
- self.progress.emit("Extracting…")
- with zipfile.ZipFile(zip_path, "r") as zf:
- zf.extractall(tmp)
-
- extracted = tmp / f"{self.REPO_NAME}-{self.REPO_BRANCH}"
- root = Path(".").resolve()
-
- self.progress.emit("Applying update…")
- for src in extracted.rglob("*"):
- rel = src.relative_to(extracted)
- parts = rel.parts
- if not parts or parts[0] in self.PROTECTED:
- continue
- dst = root / rel
- if src.is_dir():
- dst.mkdir(parents=True, exist_ok=True)
- else:
- dst.parent.mkdir(parents=True, exist_ok=True)
- shutil.copy2(src, dst)
-
- Path(self.SHA_FILE).write_text(latest_sha)
- self.finished.emit(True, f"updated:{latest_sha[:8]}")
-
-
-class UpdateDialog(QDialog):
- """Modal dialog that shows update progress and result."""
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Tool Update")
- self.setMinimumWidth(420)
- self.setModal(True)
-
- layout = QVBoxLayout(self)
- layout.setSpacing(10)
-
- self.status_label = QLabel("Checking for updates…")
- self.status_label.setWordWrap(True)
- layout.addWidget(self.status_label)
-
- self.progress_bar = QProgressBar()
- self.progress_bar.setRange(0, 0) # indeterminate
- layout.addWidget(self.progress_bar)
-
- self.close_btn = QPushButton("Cancel")
- self.close_btn.clicked.connect(self.reject)
- layout.addWidget(self.close_btn)
-
- self._thread = None
-
- def start(self, check_only=False):
- self._thread = UpdateThread(check_only=check_only, parent=self)
- self._thread.progress.connect(self.status_label.setText)
- self._thread.finished.connect(self._on_finished)
- self._thread.start()
-
- def _on_finished(self, success, message):
- self.progress_bar.setRange(0, 1)
- self.progress_bar.setValue(1)
- self.close_btn.setText("Close")
-
- if not success:
- self.status_label.setText(f"❌ Update failed:\n{message}")
- return
-
- if message == "already_up_to_date":
- self.status_label.setText("✅ Already up to date.")
- elif message.startswith("update_available:"):
- sha = message.split(":", 1)[1]
- self.status_label.setText(
- f"🆕 Update available ({sha[:8]}).\nClick 'Update Now' to install."
- )
- self.close_btn.setText("Cancel")
- update_btn = QPushButton("Update Now")
- update_btn.clicked.connect(self._do_update)
- self.layout().insertWidget(self.layout().count() - 1, update_btn)
- elif message.startswith("updated:"):
- sha = message.split(":", 1)[1]
- self.status_label.setText(
- f"✅ Updated to {sha}.\n\nPlease restart the tool for changes to take effect."
- )
-
- def _do_update(self):
- # Remove any extra buttons so only the close button remains
- for i in reversed(range(self.layout().count())):
- item = self.layout().itemAt(i)
- if item and item.widget() and item.widget() is not self.close_btn:
- w = item.widget()
- if isinstance(w, QPushButton) and w.text() in ("Update Now", "Cancel"):
- if w.text() != "Close":
- self.layout().removeWidget(w)
- w.deleteLater()
- self.progress_bar.setRange(0, 0)
- self.status_label.setText("Downloading update…")
- self.start(check_only=False)
-
-# Import configuration widgets
-from gui.config_tab import ConfigTab
-from gui.translation_tab import TranslationTab
-from gui.workflow_tab import WorkflowTab
-
-class DazedMTLGUI(QMainWindow):
- """Main GUI window for the DazedMTLTool."""
-
- def __init__(self):
- super().__init__()
- self.settings = QSettings("DazedTranslations", "DazedMTLTool")
- self.init_ui()
- self.setup_status_bar()
- self.setup_font_scaling()
- self.restore_window_state()
-
- def restore_window_state(self):
- """Restore window geometry and state from settings."""
- try:
- # Restore window geometry
- geometry = self.settings.value("geometry")
- if geometry:
- self.restoreGeometry(geometry)
-
- # Restore window state (maximized, etc.)
- window_state = self.settings.value("windowState")
- if window_state:
- self.restoreState(window_state)
-
- except Exception as e:
- print(f"Warning: Could not restore window state: {e}")
-
- def save_window_state(self):
- """Save window geometry and state to settings."""
- try:
- self.settings.setValue("geometry", self.saveGeometry())
- self.settings.setValue("windowState", self.saveState())
- except Exception as e:
- print(f"Warning: Could not save window state: {e}")
-
- def closeEvent(self, event):
- """Handle application close event."""
- # Save window geometry/state
- try:
- self.save_window_state()
- except Exception:
- pass
-
- # Attempt to stop any running translation worker to ensure
- # ThreadPoolExecutors and subprocesses are shut down so the
- # Python process can exit cleanly.
- try:
- if hasattr(self, 'translation_tab') and self.translation_tab:
- tt = self.translation_tab
- # Stop log tailing first (if active)
- try:
- if hasattr(tt, 'translation_log_viewer') and tt.translation_log_viewer:
- tt.translation_log_viewer.stop_tail()
- except Exception:
- pass
-
- # If a worker exists and is running, request it to stop and wait
- try:
- if hasattr(tt, 'translation_worker') and tt.translation_worker and tt.translation_worker.isRunning():
- tt.translation_worker.stop()
- # Wait up to 5s for graceful stop, otherwise terminate
- if not tt.translation_worker.wait(5000):
- try:
- tt.translation_worker.terminate()
- tt.translation_worker.wait(2000)
- except Exception:
- pass
- except Exception:
- pass
- except Exception:
- pass
-
- event.accept()
-
- 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 not app:
- return
-
- # Scale the application default font (affects widgets without inline font-size)
- font = app.font()
- font.setPointSize(max(6, int(9 * scale_factor)))
- app.setFont(font)
-
- # Scale inline font-size values in every widget's individual stylesheet.
- # We store the *original* stylesheet on each widget (using a Qt property)
- # so that re-scaling always starts from the unmodified values.
- for widget in app.allWidgets():
- original = widget.property("_orig_stylesheet")
- if original is None:
- ss = widget.styleSheet()
- if not ss or 'font-size' not in ss:
- continue
- widget.setProperty("_orig_stylesheet", ss)
- original = ss
-
- scaled = re.sub(
- r'font-size:\s*(\d+(?:\.\d+)?)px',
- lambda m: f'font-size: {max(6, round(float(m.group(1)) * scale_factor))}px',
- original
- )
- widget.setStyleSheet(scaled)
-
- # Update window title only when non-default scale is active
- 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")
-
- # Get screen geometry and set window size more responsively
- screen = QApplication.primaryScreen()
- screen_geometry = screen.availableGeometry()
-
- # Set window size to 80% of screen size, with reasonable minimums
- window_width = max(1200, int(screen_geometry.width() * 0.8))
- window_height = max(800, int(screen_geometry.height() * 0.8))
-
- # Center the window on screen
- x = (screen_geometry.width() - window_width) // 2
- y = (screen_geometry.height() - window_height) // 2
-
- self.setGeometry(x, y, window_width, window_height)
-
- # Set minimum size to prevent the window from becoming too small
- self.setMinimumSize(1000, 600)
-
- # 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 layout with VSCode-style sidebar
- main_layout = QHBoxLayout()
- main_layout.setContentsMargins(0, 0, 0, 0)
- main_layout.setSpacing(0)
-
- # Create sidebar for navigation
- sidebar = QWidget()
- sidebar.setFixedWidth(60)
- sidebar.setStyleSheet("""
- QWidget {
- background-color: #2d2d30;
- }
- """)
- sidebar_layout = QVBoxLayout()
- sidebar_layout.setContentsMargins(0, 10, 0, 10)
- sidebar_layout.setSpacing(2)
-
- # Create navigation buttons
- self.nav_buttons = []
-
- # Translation button (first)
- btn_translation = self.create_nav_button("🌐", "Translation")
- btn_translation.clicked.connect(lambda: self.switch_page(0))
- sidebar_layout.addWidget(btn_translation)
- self.nav_buttons.append(btn_translation)
-
- # Workflow / Automation button (second)
- btn_workflow = self.create_nav_button("⚡", "Workflow")
- btn_workflow.setToolTip("Workflow — automated translation pipeline")
- btn_workflow.clicked.connect(lambda: self.switch_page(1))
- sidebar_layout.addWidget(btn_workflow)
- self.nav_buttons.append(btn_workflow)
-
- # Configuration button (third)
- btn_config = self.create_nav_button("⚙️", "Configuration")
- btn_config.clicked.connect(lambda: self.switch_page(2))
- sidebar_layout.addWidget(btn_config)
- self.nav_buttons.append(btn_config)
-
- sidebar_layout.addStretch()
-
- # Update button at the bottom of the sidebar
- btn_update = self.create_nav_button("🔄", "Check for Updates")
- btn_update.setCheckable(False)
- btn_update.clicked.connect(self.show_update_dialog)
- sidebar_layout.addWidget(btn_update)
-
- sidebar.setLayout(sidebar_layout)
-
- # Create stacked widget for content pages
- self.content_stack = QStackedWidget()
-
- # Add tabs to stacked widget
- self.setup_tabs()
-
- # Add sidebar and content to main layout
- main_layout.addWidget(sidebar)
- main_layout.addWidget(self.content_stack)
- central_widget.setLayout(main_layout)
-
- # Create menu bar
- self.create_menu_bar()
-
- # Select first page by default
- self.switch_page(0)
-
- def create_nav_button(self, icon_text, tooltip):
- """Create a navigation button for the sidebar."""
- btn = QToolButton()
- btn.setText(icon_text)
- btn.setToolTip(tooltip)
- btn.setFixedSize(60, 50)
- btn.setCheckable(True)
- btn.setStyleSheet("""
- QToolButton {
- background-color: transparent;
- border: none;
- border-left: 3px solid transparent;
- color: #cccccc;
- font-size: 24px;
- padding: 5px;
- }
- QToolButton:hover {
- background-color: #3e3e42;
- }
- QToolButton:checked {
- background-color: #37373d;
- border-left: 3px solid #007acc;
- color: #ffffff;
- }
- """)
- return btn
-
- def switch_page(self, index):
- """Switch to the specified page and update button states."""
- self.content_stack.setCurrentIndex(index)
-
- # Update button checked states
- for i, btn in enumerate(self.nav_buttons):
- btn.setChecked(i == index)
-
- def setup_tabs(self):
- """Set up all the tabs in the interface."""
- # Translation Execution Tab (index 0)
- self.translation_tab = TranslationTab(self)
- self.content_stack.addWidget(self.translation_tab)
-
- # Workflow / Automation Tab (index 1)
- self.workflow_tab = WorkflowTab(self)
- self.content_stack.addWidget(self.workflow_tab)
-
- # Configuration Tab (index 2)
- self.config_tab = ConfigTab()
- self.config_tab.config_changed.connect(self.on_config_changed)
- self.content_stack.addWidget(self.config_tab)
-
- def show_update_dialog(self):
- """Open the update dialog and check for a newer version."""
- dlg = UpdateDialog(self)
- dlg.start(check_only=True)
- dlg.exec_()
-
- 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 if it provides a font_scale widget
- try:
- if hasattr(self.config_tab, "font_scale_spin"):
- self.config_tab.font_scale_spin.setValue(scale_factor)
- except Exception:
- # If the widget isn't present or fails to update, continue silently
- pass
-
- # Save to .env file if the config tab provides save functionality
- try:
- if hasattr(self.config_tab, "save_to_env"):
- self.config_tab.save_to_env()
- except Exception:
- pass
-
- 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 removed — font scaling menu was unreliable and has been removed
-
- tools_menu.addSeparator()
-
- # Check for updates
- update_action = tools_menu.addAction('Check for Updates')
- update_action.triggered.connect(self.show_update_dialog)
-
- 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 (removed to save space)."""
- # Status bar removed to maximize space for content
- pass
-
- 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)
- 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 = self.config_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)
-
- 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()
-
- def validate_configuration(self):
- """Validate the current configuration."""
- try:
- # Validate configuration settings
- config_valid = self.config_tab.validate()
-
- if config_valid:
- QMessageBox.information(self, "Validation", "Configuration is valid!")
- else:
- QMessageBox.warning(self, "Validation", "Configuration has issues. Check the warnings.")
-
- 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:
-
- - Visual configuration management
- - Module-specific settings
- - Real-time translation monitoring
- - File management and organization
-
- """
- )
-
- def update_status(self, message):
- """Update the status bar message (removed to save space)."""
- pass
-
- def show_progress(self, show=True):
- """Show or hide the progress bar (removed to save space)."""
- pass
-
- def set_progress(self, value):
- """Set the progress bar value (removed to save space)."""
- pass
-
-
-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)
-
- # Additional DPI handling for better compatibility
- if hasattr(Qt, 'AA_DisableWindowContextHelpButton'):
- QApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton, True)
-
- # Set high DPI scale factor policy
- if hasattr(QApplication, 'setHighDpiScaleFactorRoundingPolicy'):
- QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
-
- app = QApplication(sys.argv)
-
- # Additional screen-aware settings
- screen = app.primaryScreen()
- if screen:
- dpi = screen.logicalDotsPerInch()
- print(f"Screen DPI: {dpi}")
-
- 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 with cleaner, more compact styling
- app.setStyleSheet("""
- QMainWindow {
- background-color: #2b2b2b;
- color: #ffffff;
- }
- QWidget {
- background-color: #2b2b2b;
- color: #ffffff;
- }
- QTabWidget::pane {
- border: 1px solid #555555;
- background-color: #3c3c3c;
- padding: 5px;
- }
- QTabBar::tab {
- background-color: #555555;
- color: #ffffff;
- padding: 8px 16px;
- margin-right: 2px;
- border: 1px solid #666666;
- border-bottom: none;
- }
- QTabBar::tab:selected {
- background-color: #007acc;
- color: #ffffff;
- }
- QTabBar::tab:hover {
- background-color: #666666;
- color: #ffffff;
- }
- QGroupBox {
- font-weight: normal;
- border: 1px solid #444444;
- border-radius: 3px;
- margin-top: 8px;
- padding: 8px;
- color: #ffffff;
- background-color: transparent;
- }
- QGroupBox::title {
- subcontrol-origin: margin;
- subcontrol-position: top left;
- left: 8px;
- padding: 2px 5px;
- color: #007acc;
- background-color: #2b2b2b;
- font-weight: bold;
- }
- QPushButton {
- background-color: #0078d4;
- color: white;
- border: none;
- padding: 7px 14px;
- border-radius: 3px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #106ebe;
- }
- QPushButton:pressed {
- background-color: #005a9e;
- }
- QPushButton:disabled {
- background-color: #404040;
- color: #888888;
- }
- QCheckBox {
- color: #ffffff;
- spacing: 6px;
- background-color: transparent;
- padding: 2px;
- }
- QCheckBox::indicator {
- width: 16px;
- height: 16px;
- }
- QCheckBox::indicator:unchecked {
- background-color: #404040;
- border: 1px solid #555555;
- border-radius: 2px;
- }
- QCheckBox::indicator:checked {
- background-color: #0078d4;
- border: 1px solid #0078d4;
- border-radius: 2px;
- }
- QLabel {
- color: #ffffff;
- background-color: transparent;
- padding: 2px;
- }
- QLineEdit {
- background-color: #404040;
- color: #ffffff;
- border: 1px solid #555555;
- padding: 5px 8px;
- border-radius: 2px;
- selection-background-color: #007acc;
- }
- QLineEdit:focus {
- border: 1px solid #007acc;
- }
- QSpinBox, QDoubleSpinBox {
- background-color: #404040;
- color: #ffffff;
- border: 1px solid #555555;
- padding: 5px 8px;
- border-radius: 2px;
- }
- QSpinBox:focus, QDoubleSpinBox:focus {
- border: 1px solid #007acc;
- }
- QSpinBox::up-button, QDoubleSpinBox::up-button {
- background-color: #555555;
- border: none;
- border-radius: 0;
- }
- QSpinBox::down-button, QDoubleSpinBox::down-button {
- background-color: #555555;
- border: none;
- border-radius: 0;
- }
- QComboBox {
- background-color: #404040;
- color: #ffffff;
- border: 1px solid #555555;
- padding: 5px;
- padding-right: 30px;
- border-radius: 2px;
- }
- QComboBox:focus {
- border: 1px solid #007acc;
- }
- QComboBox::drop-down {
- subcontrol-origin: padding;
- subcontrol-position: center right;
- width: 25px;
- border-left: 1px solid #555555;
- background-color: #555555;
- }
- QComboBox::drop-down:hover {
- background-color: #007acc;
- }
- QComboBox::down-arrow {
- width: 0;
- height: 0;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- border-top: 6px solid #ffffff;
- }
- QComboBox QAbstractItemView {
- background-color: #404040;
- color: #ffffff;
- selection-background-color: #007acc;
- border: 1px solid #555555;
- padding: 3px;
- }
- QTextEdit {
- background-color: #1e1e1e;
- color: #ffffff;
- border: 1px solid #555555;
- selection-background-color: #007acc;
- padding: 5px;
- }
- QTextEdit:focus {
- border: 1px solid #007acc;
- }
- QScrollArea {
- background-color: transparent;
- border: none;
- }
- QScrollBar:vertical {
- background-color: #2b2b2b;
- width: 12px;
- border: none;
- }
- QScrollBar::handle:vertical {
- background-color: #555555;
- border-radius: 6px;
- min-height: 20px;
- margin: 2px;
- }
- QScrollBar::handle:vertical:hover {
- background-color: #007acc;
- }
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
- height: 0px;
- }
- QScrollBar:horizontal {
- background-color: #2b2b2b;
- height: 12px;
- border: none;
- }
- QScrollBar::handle:horizontal {
- background-color: #555555;
- border-radius: 6px;
- min-width: 20px;
- margin: 2px;
- }
- QScrollBar::handle:horizontal:hover {
- background-color: #007acc;
- }
- QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
- width: 0px;
- }
- QListWidget {
- background-color: #1e1e1e;
- color: #ffffff;
- border: 1px solid #555555;
- selection-background-color: #007acc;
- padding: 3px;
- }
- QListWidget::item {
- padding: 5px;
- border-bottom: 1px solid #333333;
- }
- QListWidget::item:selected {
- background-color: #007acc;
- color: #ffffff;
- }
- QListWidget::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;
- height: 20px;
- }
- 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 20px;
- }
- QMenu::item:selected {
- background-color: #007acc;
- }
- QFrame[frameShape="4"], QFrame[frameShape="5"] {
- color: #555555;
- }
- QSplitter::handle {
- background-color: #555555;
- }
- QSplitter::handle:hover {
- background-color: #007acc;
- }
- """)
-
- try:
- # Create and show the main window
- window = DazedMTLGUI()
- window.show()
-
- # Safety: ensure any running translation worker is stopped when the
- # application is quitting (aboutToQuit). This helps guarantee the
- # ThreadPoolExecutor threads and subprocesses are shut down so the
- # Python interpreter can exit cleanly.
- def _on_about_to_quit():
- try:
- if hasattr(window, 'translation_tab') and window.translation_tab:
- tt = window.translation_tab
- try:
- if hasattr(tt, 'translation_log_viewer') and tt.translation_log_viewer:
- tt.translation_log_viewer.stop_tail()
- except Exception:
- pass
-
- try:
- if hasattr(tt, 'translation_worker') and tt.translation_worker and tt.translation_worker.isRunning():
- tt.translation_worker.stop()
- # Wait briefly for graceful shutdown
- if not tt.translation_worker.wait(3000):
- try:
- tt.translation_worker.terminate()
- tt.translation_worker.wait(2000)
- except Exception:
- pass
- except Exception:
- pass
- except Exception:
- pass
-
- try:
- app.aboutToQuit.connect(_on_about_to_quit)
- except Exception:
- pass
-
- # 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())
+#!/usr/bin/env python3
+"""
+DazedMTLTool GUI - Main Application
+A PyQt-based graphical user interface for the DazedMTLTool translation system.
+"""
+
+import re
+import sys
+import os
+import json
+import urllib.request
+import zipfile
+import shutil
+import tempfile
+from pathlib import Path
+from PyQt5.QtWidgets import (
+ QApplication, QMainWindow, QTabWidget, QVBoxLayout, QHBoxLayout,
+ QWidget, QPushButton, QLabel, QFileDialog, QMessageBox, QProgressBar,
+ QTextEdit, QSplitter, QGroupBox, QStatusBar, QStackedWidget, QToolButton,
+ QDialog
+)
+from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings
+from PyQt5.QtGui import QIcon, QFont, QPixmap, QScreen
+
+
+def _project_root() -> Path:
+ """Directory containing start_gui.py / requirements.txt (parent of gui/)."""
+ return Path(__file__).resolve().parent.parent
+
+
+def load_application_icon() -> QIcon:
+ """Prefer ICO on Windows; PNG is fine for all platforms. Empty if missing."""
+ root = _project_root()
+ for rel in ("assets/icon.ico", "assets/icon.png"):
+ path = root / rel
+ if path.is_file():
+ icon = QIcon(str(path))
+ if not icon.isNull():
+ return icon
+ return QIcon()
+
+
+class UpdateThread(QThread):
+ """Downloads and applies a tool update from the GitGud repository."""
+
+ REPO_USER = "DazedAnon"
+ REPO_NAME = "DazedMTLTool"
+ REPO_BRANCH = "main"
+ SHA_FILE = "last_update_sha.txt"
+
+ # Paths (relative, top-level) that should never be touched during update
+ PROTECTED = {".env", "venv", "log", "files", "translated",
+ "vocab.txt", "last_update_sha.txt"}
+
+ progress = pyqtSignal(str) # status message
+ finished = pyqtSignal(bool, str) # (success, message)
+
+ def __init__(self, check_only=False, parent=None):
+ super().__init__(parent)
+ self.check_only = check_only
+
+ # ------------------------------------------------------------------ #
+
+ def run(self):
+ try:
+ latest_sha = self._fetch_latest_sha()
+ current_sha = self._read_stored_sha()
+
+ if latest_sha == current_sha:
+ self.finished.emit(True, "already_up_to_date")
+ return
+
+ if self.check_only:
+ self.finished.emit(True, f"update_available:{latest_sha}")
+ return
+
+ self._download_and_apply(latest_sha)
+
+ except Exception as exc:
+ self.finished.emit(False, str(exc))
+
+ # ------------------------------------------------------------------ #
+
+ def _fetch_latest_sha(self):
+ api = (
+ f"https://gitgud.io/api/v4/projects/"
+ f"{self.REPO_USER}%2F{self.REPO_NAME}"
+ f"/repository/branches/{self.REPO_BRANCH}"
+ )
+ req = urllib.request.Request(api, headers={"User-Agent": "DazedMTLTool"})
+ with urllib.request.urlopen(req, timeout=15) as resp:
+ return json.loads(resp.read())["commit"]["id"]
+
+ def _read_stored_sha(self):
+ p = Path(self.SHA_FILE)
+ return p.read_text().strip() if p.exists() else ""
+
+ def _download_and_apply(self, latest_sha):
+ zip_url = (
+ f"https://gitgud.io/{self.REPO_USER}/{self.REPO_NAME}/-/archive/"
+ f"{self.REPO_BRANCH}/{self.REPO_NAME}-{self.REPO_BRANCH}.zip"
+ )
+
+ self.progress.emit("Downloading update…")
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp = Path(tmp_dir)
+ zip_path = tmp / "update.zip"
+
+ req = urllib.request.Request(zip_url, headers={"User-Agent": "DazedMTLTool"})
+ with urllib.request.urlopen(req, timeout=120) as resp, \
+ open(zip_path, "wb") as fh:
+ shutil.copyfileobj(resp, fh)
+
+ self.progress.emit("Extracting…")
+ with zipfile.ZipFile(zip_path, "r") as zf:
+ zf.extractall(tmp)
+
+ extracted = tmp / f"{self.REPO_NAME}-{self.REPO_BRANCH}"
+ root = Path(".").resolve()
+
+ self.progress.emit("Applying update…")
+ for src in extracted.rglob("*"):
+ rel = src.relative_to(extracted)
+ parts = rel.parts
+ if not parts or parts[0] in self.PROTECTED:
+ continue
+ dst = root / rel
+ if src.is_dir():
+ dst.mkdir(parents=True, exist_ok=True)
+ else:
+ dst.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(src, dst)
+
+ Path(self.SHA_FILE).write_text(latest_sha)
+ self.finished.emit(True, f"updated:{latest_sha[:8]}")
+
+
+class UpdateDialog(QDialog):
+ """Modal dialog that shows update progress and result."""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Tool Update")
+ self.setMinimumWidth(420)
+ self.setModal(True)
+
+ layout = QVBoxLayout(self)
+ layout.setSpacing(10)
+
+ self.status_label = QLabel("Checking for updates…")
+ self.status_label.setWordWrap(True)
+ layout.addWidget(self.status_label)
+
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setRange(0, 0) # indeterminate
+ layout.addWidget(self.progress_bar)
+
+ self.close_btn = QPushButton("Cancel")
+ self.close_btn.clicked.connect(self.reject)
+ layout.addWidget(self.close_btn)
+
+ self._thread = None
+
+ def start(self, check_only=False):
+ self._thread = UpdateThread(check_only=check_only, parent=self)
+ self._thread.progress.connect(self.status_label.setText)
+ self._thread.finished.connect(self._on_finished)
+ self._thread.start()
+
+ def _on_finished(self, success, message):
+ self.progress_bar.setRange(0, 1)
+ self.progress_bar.setValue(1)
+ self.close_btn.setText("Close")
+
+ if not success:
+ self.status_label.setText(f"❌ Update failed:\n{message}")
+ return
+
+ if message == "already_up_to_date":
+ self.status_label.setText("✅ Already up to date.")
+ elif message.startswith("update_available:"):
+ sha = message.split(":", 1)[1]
+ self.status_label.setText(
+ f"🆕 Update available ({sha[:8]}).\nClick 'Update Now' to install."
+ )
+ self.close_btn.setText("Cancel")
+ update_btn = QPushButton("Update Now")
+ update_btn.clicked.connect(self._do_update)
+ self.layout().insertWidget(self.layout().count() - 1, update_btn)
+ elif message.startswith("updated:"):
+ sha = message.split(":", 1)[1]
+ self.status_label.setText(
+ f"✅ Updated to {sha}.\n\nPlease restart the tool for changes to take effect."
+ )
+
+ def _do_update(self):
+ # Remove any extra buttons so only the close button remains
+ for i in reversed(range(self.layout().count())):
+ item = self.layout().itemAt(i)
+ if item and item.widget() and item.widget() is not self.close_btn:
+ w = item.widget()
+ if isinstance(w, QPushButton) and w.text() in ("Update Now", "Cancel"):
+ if w.text() != "Close":
+ self.layout().removeWidget(w)
+ w.deleteLater()
+ self.progress_bar.setRange(0, 0)
+ self.status_label.setText("Downloading update…")
+ self.start(check_only=False)
+
+# Import configuration widgets
+from gui.config_tab import ConfigTab
+from gui.translation_tab import TranslationTab
+from gui.workflow_tab import WorkflowTab
+
+class DazedMTLGUI(QMainWindow):
+ """Main GUI window for the DazedMTLTool."""
+
+ def __init__(self):
+ super().__init__()
+ self.settings = QSettings("DazedTranslations", "DazedMTLTool")
+ self.init_ui()
+ self.setup_status_bar()
+ self.setup_font_scaling()
+ self.restore_window_state()
+
+ def restore_window_state(self):
+ """Restore window geometry and state from settings."""
+ try:
+ # Restore window geometry
+ geometry = self.settings.value("geometry")
+ if geometry:
+ self.restoreGeometry(geometry)
+
+ # Restore window state (maximized, etc.)
+ window_state = self.settings.value("windowState")
+ if window_state:
+ self.restoreState(window_state)
+
+ except Exception as e:
+ print(f"Warning: Could not restore window state: {e}")
+
+ def save_window_state(self):
+ """Save window geometry and state to settings."""
+ try:
+ self.settings.setValue("geometry", self.saveGeometry())
+ self.settings.setValue("windowState", self.saveState())
+ except Exception as e:
+ print(f"Warning: Could not save window state: {e}")
+
+ def closeEvent(self, event):
+ """Handle application close event."""
+ # Save window geometry/state
+ try:
+ self.save_window_state()
+ except Exception:
+ pass
+
+ # Attempt to stop any running translation worker to ensure
+ # ThreadPoolExecutors and subprocesses are shut down so the
+ # Python process can exit cleanly.
+ try:
+ if hasattr(self, 'translation_tab') and self.translation_tab:
+ tt = self.translation_tab
+ # Stop log tailing first (if active)
+ try:
+ if hasattr(tt, 'translation_log_viewer') and tt.translation_log_viewer:
+ tt.translation_log_viewer.stop_tail()
+ except Exception:
+ pass
+
+ # If a worker exists and is running, request it to stop and wait
+ try:
+ if hasattr(tt, 'translation_worker') and tt.translation_worker and tt.translation_worker.isRunning():
+ tt.translation_worker.stop()
+ # Wait up to 5s for graceful stop, otherwise terminate
+ if not tt.translation_worker.wait(5000):
+ try:
+ tt.translation_worker.terminate()
+ tt.translation_worker.wait(2000)
+ except Exception:
+ pass
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ event.accept()
+
+ 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 not app:
+ return
+
+ # Scale the application default font (affects widgets without inline font-size)
+ font = app.font()
+ font.setPointSize(max(6, int(9 * scale_factor)))
+ app.setFont(font)
+
+ # Scale inline font-size values in every widget's individual stylesheet.
+ # We store the *original* stylesheet on each widget (using a Qt property)
+ # so that re-scaling always starts from the unmodified values.
+ for widget in app.allWidgets():
+ original = widget.property("_orig_stylesheet")
+ if original is None:
+ ss = widget.styleSheet()
+ if not ss or 'font-size' not in ss:
+ continue
+ widget.setProperty("_orig_stylesheet", ss)
+ original = ss
+
+ scaled = re.sub(
+ r'font-size:\s*(\d+(?:\.\d+)?)px',
+ lambda m: f'font-size: {max(6, round(float(m.group(1)) * scale_factor))}px',
+ original
+ )
+ widget.setStyleSheet(scaled)
+
+ # Update window title only when non-default scale is active
+ 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")
+
+ # Get screen geometry and set window size more responsively
+ screen = QApplication.primaryScreen()
+ screen_geometry = screen.availableGeometry()
+
+ # Set window size to 80% of screen size, with reasonable minimums
+ window_width = max(1200, int(screen_geometry.width() * 0.8))
+ window_height = max(800, int(screen_geometry.height() * 0.8))
+
+ # Center the window on screen
+ x = (screen_geometry.width() - window_width) // 2
+ y = (screen_geometry.height() - window_height) // 2
+
+ self.setGeometry(x, y, window_width, window_height)
+
+ # Set minimum size to prevent the window from becoming too small
+ self.setMinimumSize(1000, 600)
+
+ app_icon = load_application_icon()
+ if not app_icon.isNull():
+ self.setWindowIcon(app_icon)
+
+ # Create central widget and main layout
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ # Create main layout with VSCode-style sidebar
+ main_layout = QHBoxLayout()
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ # Create sidebar for navigation
+ sidebar = QWidget()
+ sidebar.setFixedWidth(60)
+ sidebar.setStyleSheet("""
+ QWidget {
+ background-color: #2d2d30;
+ }
+ """)
+ sidebar_layout = QVBoxLayout()
+ sidebar_layout.setContentsMargins(0, 10, 0, 10)
+ sidebar_layout.setSpacing(2)
+
+ # Create navigation buttons
+ self.nav_buttons = []
+
+ # Translation button (first)
+ btn_translation = self.create_nav_button("🌐", "Translation")
+ btn_translation.clicked.connect(lambda: self.switch_page(0))
+ sidebar_layout.addWidget(btn_translation)
+ self.nav_buttons.append(btn_translation)
+
+ # Workflow / Automation button (second)
+ btn_workflow = self.create_nav_button("⚡", "Workflow")
+ btn_workflow.setToolTip("Workflow — automated translation pipeline")
+ btn_workflow.clicked.connect(lambda: self.switch_page(1))
+ sidebar_layout.addWidget(btn_workflow)
+ self.nav_buttons.append(btn_workflow)
+
+ # Configuration button (third)
+ btn_config = self.create_nav_button("⚙️", "Configuration")
+ btn_config.clicked.connect(lambda: self.switch_page(2))
+ sidebar_layout.addWidget(btn_config)
+ self.nav_buttons.append(btn_config)
+
+ sidebar_layout.addStretch()
+
+ # Update button at the bottom of the sidebar
+ btn_update = self.create_nav_button("🔄", "Check for Updates")
+ btn_update.setCheckable(False)
+ btn_update.clicked.connect(self.show_update_dialog)
+ sidebar_layout.addWidget(btn_update)
+
+ sidebar.setLayout(sidebar_layout)
+
+ # Create stacked widget for content pages
+ self.content_stack = QStackedWidget()
+
+ # Add tabs to stacked widget
+ self.setup_tabs()
+
+ # Add sidebar and content to main layout
+ main_layout.addWidget(sidebar)
+ main_layout.addWidget(self.content_stack)
+ central_widget.setLayout(main_layout)
+
+ # Create menu bar
+ self.create_menu_bar()
+
+ # Select first page by default
+ self.switch_page(0)
+
+ def create_nav_button(self, icon_text, tooltip):
+ """Create a navigation button for the sidebar."""
+ btn = QToolButton()
+ btn.setText(icon_text)
+ btn.setToolTip(tooltip)
+ btn.setFixedSize(60, 50)
+ btn.setCheckable(True)
+ btn.setStyleSheet("""
+ QToolButton {
+ background-color: transparent;
+ border: none;
+ border-left: 3px solid transparent;
+ color: #cccccc;
+ font-size: 24px;
+ padding: 5px;
+ }
+ QToolButton:hover {
+ background-color: #3e3e42;
+ }
+ QToolButton:checked {
+ background-color: #37373d;
+ border-left: 3px solid #007acc;
+ color: #ffffff;
+ }
+ """)
+ return btn
+
+ def switch_page(self, index):
+ """Switch to the specified page and update button states."""
+ self.content_stack.setCurrentIndex(index)
+
+ # Update button checked states
+ for i, btn in enumerate(self.nav_buttons):
+ btn.setChecked(i == index)
+
+ def setup_tabs(self):
+ """Set up all the tabs in the interface."""
+ # Translation Execution Tab (index 0)
+ self.translation_tab = TranslationTab(self)
+ self.content_stack.addWidget(self.translation_tab)
+
+ # Workflow / Automation Tab (index 1)
+ self.workflow_tab = WorkflowTab(self)
+ self.content_stack.addWidget(self.workflow_tab)
+
+ # Configuration Tab (index 2)
+ self.config_tab = ConfigTab()
+ self.config_tab.config_changed.connect(self.on_config_changed)
+ self.content_stack.addWidget(self.config_tab)
+
+ def show_update_dialog(self):
+ """Open the update dialog and check for a newer version."""
+ dlg = UpdateDialog(self)
+ dlg.start(check_only=True)
+ dlg.exec_()
+
+ 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 if it provides a font_scale widget
+ try:
+ if hasattr(self.config_tab, "font_scale_spin"):
+ self.config_tab.font_scale_spin.setValue(scale_factor)
+ except Exception:
+ # If the widget isn't present or fails to update, continue silently
+ pass
+
+ # Save to .env file if the config tab provides save functionality
+ try:
+ if hasattr(self.config_tab, "save_to_env"):
+ self.config_tab.save_to_env()
+ except Exception:
+ pass
+
+ 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 removed — font scaling menu was unreliable and has been removed
+
+ tools_menu.addSeparator()
+
+ # Check for updates
+ update_action = tools_menu.addAction('Check for Updates')
+ update_action.triggered.connect(self.show_update_dialog)
+
+ 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 (removed to save space)."""
+ # Status bar removed to maximize space for content
+ pass
+
+ 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)
+ 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 = self.config_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)
+
+ 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()
+
+ def validate_configuration(self):
+ """Validate the current configuration."""
+ try:
+ # Validate configuration settings
+ config_valid = self.config_tab.validate()
+
+ if config_valid:
+ QMessageBox.information(self, "Validation", "Configuration is valid!")
+ else:
+ QMessageBox.warning(self, "Validation", "Configuration has issues. Check the warnings.")
+
+ 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:
+
+ - Visual configuration management
+ - Module-specific settings
+ - Real-time translation monitoring
+ - File management and organization
+
+ """
+ )
+
+ def update_status(self, message):
+ """Update the status bar message (removed to save space)."""
+ pass
+
+ def show_progress(self, show=True):
+ """Show or hide the progress bar (removed to save space)."""
+ pass
+
+ def set_progress(self, value):
+ """Set the progress bar value (removed to save space)."""
+ pass
+
+
+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)
+
+ # Additional DPI handling for better compatibility
+ if hasattr(Qt, 'AA_DisableWindowContextHelpButton'):
+ QApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton, True)
+
+ # Set high DPI scale factor policy
+ if hasattr(QApplication, 'setHighDpiScaleFactorRoundingPolicy'):
+ QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
+
+ app = QApplication(sys.argv)
+
+ # Windows taskbar groups by executable (python.exe) unless we set an explicit
+ # AppUserModelID; combine with QApplication window icon so the pinned icon
+ # matches the window instead of the Python launcher.
+ if sys.platform == "win32":
+ try:
+ import ctypes
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
+ "DazedTranslations.DazedMTLTool.1"
+ )
+ except Exception:
+ pass
+
+ app_icon = load_application_icon()
+ if not app_icon.isNull():
+ app.setWindowIcon(app_icon)
+
+ # Additional screen-aware settings
+ screen = app.primaryScreen()
+ if screen:
+ dpi = screen.logicalDotsPerInch()
+ print(f"Screen DPI: {dpi}")
+
+ 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 with cleaner, more compact styling
+ app.setStyleSheet("""
+ QMainWindow {
+ background-color: #2b2b2b;
+ color: #ffffff;
+ }
+ QWidget {
+ background-color: #2b2b2b;
+ color: #ffffff;
+ }
+ QTabWidget::pane {
+ border: 1px solid #555555;
+ background-color: #3c3c3c;
+ padding: 5px;
+ }
+ QTabBar::tab {
+ background-color: #555555;
+ color: #ffffff;
+ padding: 8px 16px;
+ margin-right: 2px;
+ border: 1px solid #666666;
+ border-bottom: none;
+ }
+ QTabBar::tab:selected {
+ background-color: #007acc;
+ color: #ffffff;
+ }
+ QTabBar::tab:hover {
+ background-color: #666666;
+ color: #ffffff;
+ }
+ QGroupBox {
+ font-weight: normal;
+ border: 1px solid #444444;
+ border-radius: 3px;
+ margin-top: 8px;
+ padding: 8px;
+ color: #ffffff;
+ background-color: transparent;
+ }
+ QGroupBox::title {
+ subcontrol-origin: margin;
+ subcontrol-position: top left;
+ left: 8px;
+ padding: 2px 5px;
+ color: #007acc;
+ background-color: #2b2b2b;
+ font-weight: bold;
+ }
+ QPushButton {
+ background-color: #0078d4;
+ color: white;
+ border: none;
+ padding: 7px 14px;
+ border-radius: 3px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #106ebe;
+ }
+ QPushButton:pressed {
+ background-color: #005a9e;
+ }
+ QPushButton:disabled {
+ background-color: #404040;
+ color: #888888;
+ }
+ QCheckBox {
+ color: #ffffff;
+ spacing: 6px;
+ background-color: transparent;
+ padding: 2px;
+ }
+ QCheckBox::indicator {
+ width: 16px;
+ height: 16px;
+ }
+ QCheckBox::indicator:unchecked {
+ background-color: #404040;
+ border: 1px solid #555555;
+ border-radius: 2px;
+ }
+ QCheckBox::indicator:checked {
+ background-color: #0078d4;
+ border: 1px solid #0078d4;
+ border-radius: 2px;
+ }
+ QLabel {
+ color: #ffffff;
+ background-color: transparent;
+ padding: 2px;
+ }
+ QLineEdit {
+ background-color: #404040;
+ color: #ffffff;
+ border: 1px solid #555555;
+ padding: 5px 8px;
+ border-radius: 2px;
+ selection-background-color: #007acc;
+ }
+ QLineEdit:focus {
+ border: 1px solid #007acc;
+ }
+ QSpinBox, QDoubleSpinBox {
+ background-color: #404040;
+ color: #ffffff;
+ border: 1px solid #555555;
+ padding: 5px 8px;
+ border-radius: 2px;
+ }
+ QSpinBox:focus, QDoubleSpinBox:focus {
+ border: 1px solid #007acc;
+ }
+ QSpinBox::up-button, QDoubleSpinBox::up-button {
+ background-color: #555555;
+ border: none;
+ border-radius: 0;
+ }
+ QSpinBox::down-button, QDoubleSpinBox::down-button {
+ background-color: #555555;
+ border: none;
+ border-radius: 0;
+ }
+ QComboBox {
+ background-color: #404040;
+ color: #ffffff;
+ border: 1px solid #555555;
+ padding: 5px;
+ padding-right: 30px;
+ border-radius: 2px;
+ }
+ QComboBox:focus {
+ border: 1px solid #007acc;
+ }
+ QComboBox::drop-down {
+ subcontrol-origin: padding;
+ subcontrol-position: center right;
+ width: 25px;
+ border-left: 1px solid #555555;
+ background-color: #555555;
+ }
+ QComboBox::drop-down:hover {
+ background-color: #007acc;
+ }
+ QComboBox::down-arrow {
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 6px solid #ffffff;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #404040;
+ color: #ffffff;
+ selection-background-color: #007acc;
+ border: 1px solid #555555;
+ padding: 3px;
+ }
+ QTextEdit {
+ background-color: #1e1e1e;
+ color: #ffffff;
+ border: 1px solid #555555;
+ selection-background-color: #007acc;
+ padding: 5px;
+ }
+ QTextEdit:focus {
+ border: 1px solid #007acc;
+ }
+ QScrollArea {
+ background-color: transparent;
+ border: none;
+ }
+ QScrollBar:vertical {
+ background-color: #2b2b2b;
+ width: 12px;
+ border: none;
+ }
+ QScrollBar::handle:vertical {
+ background-color: #555555;
+ border-radius: 6px;
+ min-height: 20px;
+ margin: 2px;
+ }
+ QScrollBar::handle:vertical:hover {
+ background-color: #007acc;
+ }
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ height: 0px;
+ }
+ QScrollBar:horizontal {
+ background-color: #2b2b2b;
+ height: 12px;
+ border: none;
+ }
+ QScrollBar::handle:horizontal {
+ background-color: #555555;
+ border-radius: 6px;
+ min-width: 20px;
+ margin: 2px;
+ }
+ QScrollBar::handle:horizontal:hover {
+ background-color: #007acc;
+ }
+ QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
+ width: 0px;
+ }
+ QListWidget {
+ background-color: #1e1e1e;
+ color: #ffffff;
+ border: 1px solid #555555;
+ selection-background-color: #007acc;
+ padding: 3px;
+ }
+ QListWidget::item {
+ padding: 5px;
+ border-bottom: 1px solid #333333;
+ }
+ QListWidget::item:selected {
+ background-color: #007acc;
+ color: #ffffff;
+ }
+ QListWidget::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;
+ height: 20px;
+ }
+ 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 20px;
+ }
+ QMenu::item:selected {
+ background-color: #007acc;
+ }
+ QFrame[frameShape="4"], QFrame[frameShape="5"] {
+ color: #555555;
+ }
+ QSplitter::handle {
+ background-color: #555555;
+ }
+ QSplitter::handle:hover {
+ background-color: #007acc;
+ }
+ """)
+
+ try:
+ # Create and show the main window
+ window = DazedMTLGUI()
+ window.show()
+
+ # Safety: ensure any running translation worker is stopped when the
+ # application is quitting (aboutToQuit). This helps guarantee the
+ # ThreadPoolExecutor threads and subprocesses are shut down so the
+ # Python interpreter can exit cleanly.
+ def _on_about_to_quit():
+ try:
+ if hasattr(window, 'translation_tab') and window.translation_tab:
+ tt = window.translation_tab
+ try:
+ if hasattr(tt, 'translation_log_viewer') and tt.translation_log_viewer:
+ tt.translation_log_viewer.stop_tail()
+ except Exception:
+ pass
+
+ try:
+ if hasattr(tt, 'translation_worker') and tt.translation_worker and tt.translation_worker.isRunning():
+ tt.translation_worker.stop()
+ # Wait briefly for graceful shutdown
+ if not tt.translation_worker.wait(3000):
+ try:
+ tt.translation_worker.terminate()
+ tt.translation_worker.wait(2000)
+ except Exception:
+ pass
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ try:
+ app.aboutToQuit.connect(_on_about_to_quit)
+ except Exception:
+ pass
+
+ # 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/screens/branch.png b/screens/branch.png
deleted file mode 100644
index ae906c8..0000000
Binary files a/screens/branch.png and /dev/null differ
diff --git a/screens/changes.png b/screens/changes.png
deleted file mode 100644
index 874dd2e..0000000
Binary files a/screens/changes.png and /dev/null differ
diff --git a/screens/codes.png b/screens/codes.png
deleted file mode 100644
index 9fd793b..0000000
Binary files a/screens/codes.png and /dev/null differ
diff --git a/screens/commit.png b/screens/commit.png
deleted file mode 100644
index d8a261b..0000000
Binary files a/screens/commit.png and /dev/null differ
diff --git a/screens/files.png b/screens/files.png
deleted file mode 100644
index 282eaf7..0000000
Binary files a/screens/files.png and /dev/null differ
diff --git a/screens/format.png b/screens/format.png
deleted file mode 100644
index 82f89c1..0000000
Binary files a/screens/format.png and /dev/null differ
diff --git a/screens/history.png b/screens/history.png
deleted file mode 100644
index 9304e84..0000000
Binary files a/screens/history.png and /dev/null differ
diff --git a/screens/open-with-code.png b/screens/open-with-code.png
deleted file mode 100644
index c5cdfd3..0000000
Binary files a/screens/open-with-code.png and /dev/null differ
diff --git a/screens/plugins.png b/screens/plugins.png
deleted file mode 100644
index fcf3991..0000000
Binary files a/screens/plugins.png and /dev/null differ
diff --git a/screens/prologue.png b/screens/prologue.png
deleted file mode 100644
index 52a0aea..0000000
Binary files a/screens/prologue.png and /dev/null differ
diff --git a/screens/search.png b/screens/search.png
deleted file mode 100644
index 98232ab..0000000
Binary files a/screens/search.png and /dev/null differ
diff --git a/screens/tool.png b/screens/tool.png
deleted file mode 100644
index fe38142..0000000
Binary files a/screens/tool.png and /dev/null differ
diff --git a/screens/vscode.png b/screens/vscode.png
deleted file mode 100644
index bd409b0..0000000
Binary files a/screens/vscode.png and /dev/null differ