From 2a923d4971dd2f40dbefa55ad151e65457dd820f Mon Sep 17 00:00:00 2001 From: dazedanon Date: Sat, 25 Oct 2025 20:35:38 -0500 Subject: [PATCH] Fixing shit --- gui/log_viewer.py | 342 ++++++++++++++++------------------------- gui/translation_tab.py | 77 +++++++--- 2 files changed, 194 insertions(+), 225 deletions(-) diff --git a/gui/log_viewer.py b/gui/log_viewer.py index 8aa9606..f74d8a2 100644 --- a/gui/log_viewer.py +++ b/gui/log_viewer.py @@ -3,10 +3,9 @@ Log Viewer - Real-time log display and monitoring """ from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, - QLabel, QCheckBox, QComboBox, QGroupBox, QSplitter, QSpinBox + QWidget, QVBoxLayout, QTextEdit, QLabel, QSizePolicy ) -from PyQt5.QtCore import Qt, QTimer, pyqtSignal +from PyQt5.QtCore import Qt, pyqtSignal, QTimer from PyQt5.QtGui import QTextCursor, QFont from pathlib import Path import datetime @@ -20,65 +19,35 @@ class LogViewer(QWidget): 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) - - # Performance optimization tracking - self.file_mod_times = {} # Track file modification times - self.file_positions = {} # Track file read positions for tail-only updates - self.max_lines = 1000 # Maximum lines to display - self.refresh_interval = 2000 # Default refresh interval in ms - + # Lightweight append-only log viewer. Logs are provided by the + # translation worker via signals so we don't poll files or provide + # controls here. This keeps the UI responsive. 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) - - # Max lines control - control_layout.addWidget(QLabel("Max Lines:")) - self.max_lines_spinbox = QSpinBox() - self.max_lines_spinbox.setRange(100, 10000) - self.max_lines_spinbox.setValue(self.max_lines) - self.max_lines_spinbox.setSingleStep(100) - self.max_lines_spinbox.valueChanged.connect(self.update_max_lines) - control_layout.addWidget(self.max_lines_spinbox) - - # 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 + # Remove internal margins so header aligns with the left column's header + layout.setContentsMargins(0, 0, 0, 0) + + # Simple header to match left-side styling (use same look as create_section_header) + header = QLabel("📝 Translation Log") + header.setStyleSheet(""" + QLabel { + font-size: 13px; + font-weight: bold; + color: #007acc; + padding: 8px 0px 5px 0px; + background-color: transparent; + } + """) + layout.addWidget(header, 0) + + # Match spacing used in left column so the gap between header and + # content lines up visually. + layout.setSpacing(8) + + # Log display area (append-only) self.log_display = QTextEdit() self.log_display.setReadOnly(True) self.log_display.setFont(QFont("Consolas", 10)) @@ -89,182 +58,143 @@ class LogViewer(QWidget): border: 1px solid #555555; } """) - layout.addWidget(self.log_display) - - # Status bar + # Make the log display expand to take available vertical space + self.log_display.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + layout.addWidget(self.log_display, 1) + + # Lightweight status label self.status_label = QLabel("Ready") self.status_label.setStyleSheet("color: #cccccc; padding: 5px;") - layout.addWidget(self.status_label) - + layout.addWidget(self.status_label, 0) + self.setLayout(layout) - - # Initial load - self.load_selected_log() + # Tail-related fields + self._tail_timer = QTimer(self) + self._tail_timer.timeout.connect(self._poll_tail) + self._tail_interval = 300 # ms + self._tail_f = None + self._tail_path = Path("log/translationHistory.txt") def toggle_auto_refresh(self, enabled): """Toggle auto-refresh functionality.""" - if enabled: - # Adjust refresh interval based on file size - self.adjust_refresh_interval() - self.auto_refresh_timer.start(self.refresh_interval) - self.status_label.setText(f"Auto-refresh enabled ({self.refresh_interval/1000:.1f}s interval)") - else: - self.auto_refresh_timer.stop() - self.status_label.setText("Auto-refresh disabled") + # No-op: controls removed in simplified viewer + pass def adjust_refresh_interval(self): """Adjust refresh interval based on current log file size.""" - selected_log = self.log_selector.currentText() - if selected_log in self.log_files: - log_path = Path(self.log_files[selected_log]) - if log_path.exists(): - file_size = log_path.stat().st_size - # Increase interval for larger files - if file_size > 500000: # > 500KB - self.refresh_interval = 5000 # 5 seconds - elif file_size > 100000: # > 100KB - self.refresh_interval = 3000 # 3 seconds - else: - self.refresh_interval = 2000 # 2 seconds - + # No-op in simplified viewer + pass + def update_max_lines(self, value): - """Update maximum lines to display.""" - self.max_lines = value - self.load_selected_log() # Reload with new limit + """No-op in simplified viewer (controls removed).""" + pass 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]) + # No-op in simplified viewer + pass def load_log_file(self, file_path): """Load content from a specific log file with optimization.""" - try: - log_path = Path(file_path) - if not log_path.exists(): - self.log_display.setPlainText(f"Log file not found: {file_path}") - self.status_label.setText("Log file not found") - return - - # Check if file has been modified since last read - current_mod_time = log_path.stat().st_mtime - last_mod_time = self.file_mod_times.get(file_path, 0) - - if current_mod_time <= last_mod_time and file_path in self.file_mod_times: - # File hasn't changed, no need to reload - return - - # Update modification time - self.file_mod_times[file_path] = current_mod_time - - # Read file efficiently - file_size = log_path.stat().st_size - - if file_size > 1000000: # > 1MB, read only tail - content = self.read_file_tail(log_path, self.max_lines) - else: - # For smaller files, read normally but limit lines - with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: - content = f.read() - - # Limit to max lines if content is too long - lines = content.splitlines() - if len(lines) > self.max_lines: - content = '\n'.join(lines[-self.max_lines:]) - content = f"... (showing last {self.max_lines} lines) ...\n" + content - - self.log_display.setPlainText(content) - - # Scroll to bottom - cursor = self.log_display.textCursor() - cursor.movePosition(QTextCursor.End) - self.log_display.setTextCursor(cursor) - - # Update status - lines_count = len(content.splitlines()) - self.status_label.setText(f"Loaded {log_path.name} ({file_size:,} bytes, {lines_count:,} lines)") - - except Exception as e: - self.log_display.setPlainText(f"Error loading log file: {str(e)}") - self.status_label.setText(f"Error: {str(e)}") + # File-based loading removed in simplified viewer + pass def read_file_tail(self, file_path, max_lines): """Efficiently read the last N lines of a large file.""" - try: - with open(file_path, 'rb') as f: - # Start from end of file - f.seek(0, 2) # Seek to end - file_size = f.tell() - - # Read chunks from end until we have enough lines - lines = [] - chunk_size = min(8192, file_size) # 8KB chunks - pos = file_size - - while len(lines) < max_lines and pos > 0: - # Calculate chunk position - chunk_start = max(0, pos - chunk_size) - f.seek(chunk_start) - - # Read chunk - chunk = f.read(pos - chunk_start).decode('utf-8', errors='ignore') - chunk_lines = chunk.splitlines() - - # Prepend to lines list - if chunk_lines: - if lines and not chunk.endswith('\n'): - # Merge partial line at end of chunk with first existing line - chunk_lines[-1] += lines[0] - lines = chunk_lines + lines[1:] - else: - lines = chunk_lines + lines - - pos = chunk_start - - # Return last max_lines - if len(lines) > max_lines: - lines = lines[-max_lines:] - return f"... (showing last {max_lines} lines) ...\n" + '\n'.join(lines) - else: - return '\n'.join(lines) - - except Exception as e: - return f"Error reading file tail: {str(e)}" + # No-op in simplified viewer + return "" def refresh_logs(self): """Refresh the current log display - only if tab is visible and file changed.""" - # Only refresh if widget is visible (optimization for when tab is not active) - if not self.isVisible(): - return - - self.load_selected_log() - - # Readjust refresh interval if auto-refresh is enabled - if self.auto_refresh_cb.isChecked(): - self.adjust_refresh_interval() - if self.auto_refresh_timer.interval() != self.refresh_interval: - self.auto_refresh_timer.stop() - self.auto_refresh_timer.start(self.refresh_interval) + # No-op for simplified viewer + pass def clear_log(self): """Clear the log display and reset tracking.""" self.log_display.clear() - # Reset modification time tracking to force reload on next refresh - selected_log = self.log_selector.currentText() - if selected_log in self.log_files: - file_path = self.log_files[selected_log] - if file_path in self.file_mod_times: - del self.file_mod_times[file_path] self.status_label.setText("Log cleared") + # Reset tail file pointer if active + if self._tail_f: + try: + # Move pointer to end so we continue only with new lines + self._tail_f.seek(0, os.SEEK_END) + except Exception: + pass + + def start_tail(self, file_path: str | Path = None, interval_ms: int = None): + """Start tailing the given log file and append new lines as they arrive. + + By default tails `log/translationHistory.txt`. Seeks to the end so + only new lines after this call are shown. + """ + if file_path: + self._tail_path = Path(file_path) + if interval_ms: + self._tail_interval = interval_ms + + # Ensure directory exists + try: + self._tail_path.parent.mkdir(parents=True, exist_ok=True) + # Ensure file exists + self._tail_path.touch(exist_ok=True) + except Exception: + pass + + # Close previous handle + if self._tail_f: + try: + self._tail_f.close() + except Exception: + pass + self._tail_f = None + + try: + self._tail_f = open(self._tail_path, 'r', encoding='utf-8', errors='ignore') + # Seek to end so we only read new lines + self._tail_f.seek(0, os.SEEK_END) + except Exception as e: + self.status_label.setText(f"Log error: {str(e)}") + self._tail_f = None + return + + self._tail_timer.start(self._tail_interval) + + def stop_tail(self): + """Stop tailing the log file and close internal file handle.""" + try: + self._tail_timer.stop() + except Exception: + pass + if self._tail_f: + try: + self._tail_f.close() + except Exception: + pass + self._tail_f = None + + def _poll_tail(self): + """Timer callback: read any new data from the tailed file and append it.""" + if not self._tail_f: + return + try: + new_data = self._tail_f.read() + if not new_data: + return + # Split into lines and append + for line in new_data.splitlines(): + if line.strip(): + self.append_log_message(line) + except Exception: + # If anything goes wrong, stop the tail to avoid repeated errors + self.stop_tail() 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) - + # Append raw message (worker already includes any desired context). + # We intentionally avoid adding timestamps here so wrapped lines stay + # readable and do not misalign JSON-like outputs. + self.log_display.append(message) + # Scroll to bottom cursor = self.log_display.textCursor() cursor.movePosition(QTextCursor.End) @@ -277,11 +207,9 @@ class LogViewer(QWidget): def showEvent(self, event): """Handle widget show event - resume refresh if auto-refresh is enabled.""" super().showEvent(event) - if self.auto_refresh_cb.isChecked() and not self.auto_refresh_timer.isActive(): - self.auto_refresh_timer.start(self.refresh_interval) + # No-op for simplified viewer def hideEvent(self, event): """Handle widget hide event - pause refresh to save resources.""" super().hideEvent(event) - # Note: Don't stop timer completely as other parts may depend on it - # The refresh_logs method now checks visibility anyway + # No-op for simplified viewer diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 0750694..21f6c13 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -22,6 +22,7 @@ from PyQt5.QtWidgets import ( QTextEdit, QMessageBox, QListWidget, QListWidgetItem, QSplitter, QFileDialog, QComboBox, QCheckBox, QProgressBar, QFrame, QFormLayout, QStackedWidget ) +from PyQt5.QtWidgets import QSizePolicy from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess from PyQt5.QtGui import QFont from gui.log_viewer import LogViewer @@ -440,14 +441,24 @@ class TranslationTab(QWidget): def setup_ui(self): """Set up the user interface.""" - # Create main splitter to separate translation controls from log viewer - main_splitter = QSplitter(Qt.Horizontal) - + # Create a fixed horizontal layout to separate translation controls from log viewer. + # Using a layout instead of QSplitter prevents the user from resizing panes. + main_container = QWidget() + main_hbox = QHBoxLayout() + # Match left side padding so headers align at the top of the boxes + main_hbox.setContentsMargins(15, 15, 15, 15) + main_hbox.setSpacing(8) + # Align child widgets individually when needed; avoid setting a + # global AlignTop on the HBox so children with Expanding size + # policies can grow vertically to fill available space. + # Left side - translation controls left_widget = QWidget() layout = QVBoxLayout() layout.setSpacing(8) - layout.setContentsMargins(15, 15, 15, 15) + # Remove the top internal margin so the left header lines up with the + # right header (main_hbox already provides top padding). + layout.setContentsMargins(15, 0, 15, 15) # Files Section (at the top) layout.addWidget(create_section_header("📁 Input Files")) @@ -766,18 +777,30 @@ class TranslationTab(QWidget): # Right side - translation history log viewer self.translation_log_viewer = LogViewer() - - # Add both to splitter - main_splitter.addWidget(left_widget) - main_splitter.addWidget(self.translation_log_viewer) - - # Set proportional sizes (translation controls: 60%, log viewer: 40%) - main_splitter.setStretchFactor(0, 3) - main_splitter.setStretchFactor(1, 2) - + + # Allow both left and right widgets to expand vertically so the + # log viewer fills the full height to the bottom of the tab. + left_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.translation_log_viewer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Add both widgets to the fixed HBox with stretch factors (left 40%, right 60%). + # Keep the left column top-aligned so its header stays at the top, + # but allow the right-hand log viewer to expand vertically to the + # bottom of the tab so it fills available space. + main_hbox.addWidget(left_widget, 2, Qt.AlignTop) + # Do NOT force AlignTop on the log viewer; with an Expanding + # vertical size policy it will grow to fill the available height. + main_hbox.addWidget(self.translation_log_viewer, 3) + + main_container.setLayout(main_hbox) + + # Ensure main container will expand to fill the tab vertically + main_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Set main layout for this tab tab_layout = QVBoxLayout() - tab_layout.addWidget(main_splitter) + # Add with stretch so the container expands to fill available space + tab_layout.addWidget(main_container, 1) tab_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(tab_layout) @@ -1167,15 +1190,27 @@ class TranslationTab(QWidget): self.translation_worker.progress_signal.connect(self.update_file_progress) self.translation_worker.item_progress_signal.connect(self.update_item_progress) self.translation_worker.finished_signal.connect(self.on_translation_finished) - + # Clear and start tailing the translation history so only new + # lines are shown in the right-hand log panel. + try: + self.translation_log_viewer.clear_log() + self.translation_log_viewer.start_tail(self.project_root / 'log' / 'translationHistory.txt') + except Exception: + pass + # Start the worker self.translation_worker.start() def append_log(self, message): """Append a message to the log - now just for internal tracking.""" - # We no longer display logs in the UI, but we can keep this for debugging - # or future use with the log viewer - pass + # IMPORTANT: Do NOT forward worker messages directly to the + # LogViewer when tailing a file. The LogViewer is intended to + # show only the contents of the log file (e.g. log/translationHistory.txt). + # Forwarding worker messages here caused non-file messages to appear + # in the log window. Modules write to the log file themselves, so + # we no longer push worker messages into the UI. + # Keep this method as a no-op to preserve the signal connection. + return def update_file_progress(self, current_file, total_files, filename): """Update the file-level progress.""" @@ -1246,6 +1281,12 @@ class TranslationTab(QWidget): # Refresh file list to show any new translated files self.refresh_file_lists() + # Stop tailing the log when finished + try: + if hasattr(self, 'translation_log_viewer') and self.translation_log_viewer: + self.translation_log_viewer.stop_tail() + except Exception: + pass def stop_translation(self): """Stop the translation process."""