supervertaler 1.9.180__py3-none-any.whl → 1.9.194__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of supervertaler might be problematic. Click here for more details.

Supervertaler.py CHANGED
@@ -32,9 +32,9 @@ License: MIT
32
32
  """
33
33
 
34
34
  # Version Information.
35
- __version__ = "1.9.180"
35
+ __version__ = "1.9.194"
36
36
  __phase__ = "0.9"
37
- __release_date__ = "2026-01-28"
37
+ __release_date__ = "2026-02-01"
38
38
  __edition__ = "Qt"
39
39
 
40
40
  import sys
@@ -1401,10 +1401,15 @@ class ReadOnlyGridTextEditor(QTextEdit):
1401
1401
  self.setMouseTracking(True)
1402
1402
 
1403
1403
  # Add syntax highlighter for tags (no spellcheck for source cells)
1404
- # Get invisible char color from main window if available
1404
+ # Get invisible char color and tag color from main window (theme-aware)
1405
1405
  main_window = self._get_main_window()
1406
1406
  invisible_char_color = main_window.invisible_char_color if main_window and hasattr(main_window, 'invisible_char_color') else '#999999'
1407
- self.highlighter = TagHighlighter(self.document(), self.tag_highlight_color, invisible_char_color, enable_spellcheck=False)
1407
+
1408
+ # Use theme-aware tag color (light pink in dark mode, dark red in light mode)
1409
+ is_dark = main_window and hasattr(main_window, 'theme_manager') and main_window.theme_manager and main_window.theme_manager.current_theme.name == "Dark"
1410
+ tag_color = '#FFB6C1' if is_dark else self.tag_highlight_color # Light pink in dark mode
1411
+
1412
+ self.highlighter = TagHighlighter(self.document(), tag_color, invisible_char_color, enable_spellcheck=False)
1408
1413
 
1409
1414
  # Store raw text (with tags) for mode switching
1410
1415
  self._raw_text = text
@@ -1659,16 +1664,12 @@ class ReadOnlyGridTextEditor(QTextEdit):
1659
1664
  matches_dict: Dictionary of {term: {'translation': str, 'priority': int}} or {term: str}
1660
1665
  """
1661
1666
  from PyQt6.QtGui import QTextCursor, QTextCharFormat, QColor, QFont
1662
-
1663
- print(f"[HIGHLIGHT DEBUG] highlight_termbase_matches called with {len(matches_dict) if matches_dict else 0} matches")
1664
-
1667
+
1665
1668
  # Get the document and create a cursor
1666
1669
  doc = self.document()
1667
1670
  text = self.toPlainText()
1668
1671
  text_lower = text.lower()
1669
1672
 
1670
- print(f"[HIGHLIGHT DEBUG] Widget text length: {len(text)}, text preview: {text[:60]}...")
1671
-
1672
1673
  # IMPORTANT: Always clear all previous formatting first to prevent inconsistent highlighting
1673
1674
  cursor = QTextCursor(doc)
1674
1675
  cursor.select(QTextCursor.SelectionType.Document)
@@ -1677,7 +1678,6 @@ class ReadOnlyGridTextEditor(QTextEdit):
1677
1678
 
1678
1679
  # If no matches, we're done (highlighting has been cleared)
1679
1680
  if not matches_dict:
1680
- print(f"[HIGHLIGHT DEBUG] No matches, returning after clear")
1681
1681
  return
1682
1682
 
1683
1683
  # Get highlight style from main window settings
@@ -1695,9 +1695,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
1695
1695
  dotted_color = settings.get('termbase_dotted_color', '#808080')
1696
1696
  break
1697
1697
  parent = parent.parent() if hasattr(parent, 'parent') else None
1698
-
1699
- print(f"[HIGHLIGHT DEBUG] Using style: {highlight_style}")
1700
-
1698
+
1701
1699
  # Sort matches by source term length (longest first) to avoid partial matches
1702
1700
  # Since dict keys are now term_ids, we need to extract source terms first
1703
1701
  term_entries = []
@@ -1706,11 +1704,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
1706
1704
  source_term = match_info.get('source', '')
1707
1705
  if source_term:
1708
1706
  term_entries.append((source_term, term_id, match_info))
1709
-
1710
- print(f"[HIGHLIGHT DEBUG] Built {len(term_entries)} term entries from matches")
1711
- if term_entries:
1712
- print(f"[HIGHLIGHT DEBUG] First few terms to search: {[t[0] for t in term_entries[:3]]}")
1713
-
1707
+
1714
1708
  # Sort by source term length (longest first)
1715
1709
  term_entries.sort(key=lambda x: len(x[0]), reverse=True)
1716
1710
 
@@ -1835,8 +1829,6 @@ class ReadOnlyGridTextEditor(QTextEdit):
1835
1829
  highlighted_ranges.append((idx, end_idx))
1836
1830
 
1837
1831
  start = end_idx
1838
-
1839
- print(f"[HIGHLIGHT DEBUG] Applied formatting to {found_count} term occurrences in text")
1840
1832
 
1841
1833
  def highlight_non_translatables(self, nt_matches: list, highlighted_ranges: list = None):
1842
1834
  """
@@ -2306,10 +2298,15 @@ class ReadOnlyGridTextEditor(QTextEdit):
2306
2298
 
2307
2299
  # Superlookup search action
2308
2300
  if self.textCursor().hasSelection():
2309
- superlookup_action = QAction("🔍 Search in Superlookup (Ctrl+K)", self)
2301
+ superlookup_action = QAction("🔍 Search in SuperLookup (Ctrl+K)", self)
2310
2302
  superlookup_action.triggered.connect(self._handle_superlookup_search)
2311
2303
  menu.addAction(superlookup_action)
2312
- menu.addSeparator()
2304
+
2305
+ # MT Quick Lookup action
2306
+ mt_lookup_action = QAction("⚡ QuickTrans (Ctrl+M)", self)
2307
+ mt_lookup_action.triggered.connect(self._handle_mt_quick_lookup)
2308
+ menu.addAction(mt_lookup_action)
2309
+ menu.addSeparator()
2313
2310
 
2314
2311
  # QuickMenu (prompt-based actions)
2315
2312
  try:
@@ -2363,7 +2360,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
2363
2360
  """Handle Ctrl+Alt+N: Add selected text to active non-translatable list(s)"""
2364
2361
  # Get selected text
2365
2362
  selected_text = self.textCursor().selectedText().strip()
2366
-
2363
+
2367
2364
  if not selected_text:
2368
2365
  from PyQt6.QtWidgets import QMessageBox
2369
2366
  QMessageBox.warning(
@@ -2372,14 +2369,14 @@ class ReadOnlyGridTextEditor(QTextEdit):
2372
2369
  "Please select text in the Source cell before adding to non-translatables."
2373
2370
  )
2374
2371
  return
2375
-
2372
+
2376
2373
  # Find main window and call add_to_nt method
2377
2374
  table = self.table_ref if hasattr(self, 'table_ref') else self.parent()
2378
2375
  if table:
2379
2376
  main_window = table.parent()
2380
2377
  while main_window and not hasattr(main_window, 'add_text_to_non_translatables'):
2381
2378
  main_window = main_window.parent()
2382
-
2379
+
2383
2380
  if main_window and hasattr(main_window, 'add_text_to_non_translatables'):
2384
2381
  main_window.add_text_to_non_translatables(selected_text)
2385
2382
  else:
@@ -2390,6 +2387,16 @@ class ReadOnlyGridTextEditor(QTextEdit):
2390
2387
  "Non-translatables functionality not available."
2391
2388
  )
2392
2389
 
2390
+ def _handle_mt_quick_lookup(self):
2391
+ """Handle right-click: Open MT Quick Lookup popup for selected text or full source"""
2392
+ # Get selected text (if any)
2393
+ selected_text = self.textCursor().selectedText().strip() if self.textCursor().hasSelection() else None
2394
+
2395
+ # Find main window and call show_mt_quick_popup
2396
+ main_window = self._get_main_window()
2397
+ if main_window and hasattr(main_window, 'show_mt_quick_popup'):
2398
+ main_window.show_mt_quick_popup(text_override=selected_text)
2399
+
2393
2400
  def set_background_color(self, color: str):
2394
2401
  """Set the background color for this text editor (for alternating row colors)"""
2395
2402
  self.setStyleSheet(f"""
@@ -2739,10 +2746,15 @@ class EditableGridTextEditor(QTextEdit):
2739
2746
  self.setPalette(palette)
2740
2747
 
2741
2748
  # Add syntax highlighter for tags (with spellcheck enabled for target cells)
2742
- # Get invisible char color from main window if available
2749
+ # Get invisible char color and tag color from main window (theme-aware)
2743
2750
  main_window = self._get_main_window()
2744
2751
  invisible_char_color = main_window.invisible_char_color if main_window and hasattr(main_window, 'invisible_char_color') else '#999999'
2745
- self.highlighter = TagHighlighter(self.document(), self.tag_highlight_color, invisible_char_color, enable_spellcheck=True)
2752
+
2753
+ # Use theme-aware tag color (light pink in dark mode, dark red in light mode)
2754
+ is_dark = main_window and hasattr(main_window, 'theme_manager') and main_window.theme_manager and main_window.theme_manager.current_theme.name == "Dark"
2755
+ tag_color = '#FFB6C1' if is_dark else self.tag_highlight_color # Light pink in dark mode
2756
+
2757
+ self.highlighter = TagHighlighter(self.document(), tag_color, invisible_char_color, enable_spellcheck=True)
2746
2758
 
2747
2759
  # Style to look like a normal cell with subtle selection
2748
2760
  # Background and text colors now managed by theme system
@@ -2994,10 +3006,15 @@ class EditableGridTextEditor(QTextEdit):
2994
3006
 
2995
3007
  # Superlookup search action
2996
3008
  if self.textCursor().hasSelection():
2997
- superlookup_action = QAction("🔍 Search in Superlookup (Ctrl+K)", self)
3009
+ superlookup_action = QAction("🔍 Search in SuperLookup (Ctrl+K)", self)
2998
3010
  superlookup_action.triggered.connect(self._handle_superlookup_search)
2999
3011
  menu.addAction(superlookup_action)
3000
- menu.addSeparator()
3012
+
3013
+ # MT Quick Lookup action
3014
+ mt_lookup_action = QAction("⚡ QuickTrans (Ctrl+M)", self)
3015
+ mt_lookup_action.triggered.connect(self._handle_mt_quick_lookup)
3016
+ menu.addAction(mt_lookup_action)
3017
+ menu.addSeparator()
3001
3018
 
3002
3019
  # QuickMenu (prompt-based actions)
3003
3020
  try:
@@ -3510,6 +3527,24 @@ class EditableGridTextEditor(QTextEdit):
3510
3527
  "Non-translatables functionality not available."
3511
3528
  )
3512
3529
 
3530
+ def _handle_mt_quick_lookup(self):
3531
+ """Handle right-click: Open MT Quick Lookup popup for selected text or full source"""
3532
+ # Get selected text (if any) - prefer from target, then try source
3533
+ selected_text = self.textCursor().selectedText().strip() if self.textCursor().hasSelection() else None
3534
+
3535
+ if not selected_text and self.table and self.row >= 0:
3536
+ # Try getting selected text from source cell
3537
+ source_widget = self.table.cellWidget(self.row, 2)
3538
+ if source_widget and hasattr(source_widget, 'textCursor'):
3539
+ cursor = source_widget.textCursor()
3540
+ if cursor.hasSelection():
3541
+ selected_text = cursor.selectedText().strip()
3542
+
3543
+ # Find main window and call show_mt_quick_popup
3544
+ main_window = self._get_main_window()
3545
+ if main_window and hasattr(main_window, 'show_mt_quick_popup'):
3546
+ main_window.show_mt_quick_popup(text_override=selected_text)
3547
+
3513
3548
  def _insert_next_tag_or_wrap_selection(self):
3514
3549
  """
3515
3550
  Insert the next memoQ tag, HTML tag, or CafeTran pipe symbol from source, or wrap selection.
@@ -6224,6 +6259,12 @@ class SupervertalerQt(QMainWindow):
6224
6259
  self.termbase_cache_lock = threading.Lock() # Thread-safe cache access
6225
6260
  self.termbase_batch_worker_thread = None # Background worker thread
6226
6261
  self.termbase_batch_stop_event = threading.Event() # Signal to stop background worker
6262
+
6263
+ # In-memory termbase index for instant lookups (v1.9.182)
6264
+ # Loaded once on project load, contains ALL terms from activated termbases
6265
+ # Structure: list of term dicts with pre-compiled regex patterns
6266
+ self.termbase_index = []
6267
+ self.termbase_index_lock = threading.Lock()
6227
6268
 
6228
6269
  # TM/MT/LLM prefetch cache for instant segment switching (like memoQ)
6229
6270
  # Maps segment ID → {"TM": [...], "MT": [...], "LLM": [...]}
@@ -6237,9 +6278,9 @@ class SupervertalerQt(QMainWindow):
6237
6278
  self.idle_prefetch_timer = None # QTimer for triggering prefetch after typing pause
6238
6279
  self.idle_prefetch_delay_ms = 1500 # Start prefetch 1.5s after user stops typing
6239
6280
 
6240
- # 🧪 EXPERIMENTAL: Cache kill switch for performance testing
6281
+ # Cache kill switch for performance testing
6241
6282
  # When True, all caches are bypassed - direct lookups every time
6242
- self.disable_all_caches = True
6283
+ self.disable_all_caches = False # v1.9.183: Default to False (caches ENABLED)
6243
6284
 
6244
6285
  # Undo/Redo stack for grid edits
6245
6286
  self.undo_stack = [] # List of (segment_id, old_target, new_target, old_status, new_status)
@@ -6367,7 +6408,13 @@ class SupervertalerQt(QMainWindow):
6367
6408
  self.termview_widget.theme_manager = self.theme_manager
6368
6409
  if hasattr(self.termview_widget, 'apply_theme'):
6369
6410
  self.termview_widget.apply_theme()
6370
-
6411
+
6412
+ # Also update the Match Panel TermView (right panel)
6413
+ if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
6414
+ self.termview_widget_match.theme_manager = self.theme_manager
6415
+ if hasattr(self.termview_widget_match, 'apply_theme'):
6416
+ self.termview_widget_match.apply_theme()
6417
+
6371
6418
  if hasattr(self, 'translation_results_panel') and self.translation_results_panel:
6372
6419
  self.translation_results_panel.theme_manager = self.theme_manager
6373
6420
  # Also update class-level theme_manager for CompactMatchItem
@@ -6426,7 +6473,7 @@ class SupervertalerQt(QMainWindow):
6426
6473
 
6427
6474
  # Restore Termview under grid visibility state
6428
6475
  if hasattr(self, 'bottom_tabs'):
6429
- termview_visible = general_settings.get('termview_under_grid_visible', True)
6476
+ termview_visible = general_settings.get('termview_under_grid_visible', False)
6430
6477
  self.bottom_tabs.setVisible(termview_visible)
6431
6478
  if hasattr(self, 'termview_visible_action'):
6432
6479
  self.termview_visible_action.setChecked(termview_visible)
@@ -7274,7 +7321,12 @@ class SupervertalerQt(QMainWindow):
7274
7321
 
7275
7322
  # Alt+K - Open QuickMenu directly
7276
7323
  create_shortcut("editor_open_quickmenu", "Alt+K", self.open_quickmenu)
7277
-
7324
+
7325
+ # Ctrl+Shift+Q - MT Quick Lookup (GT4T-style popup)
7326
+ mt_quick_shortcut = create_shortcut("mt_quick_lookup", "Ctrl+Shift+Q", self.show_mt_quick_popup)
7327
+ # Use ApplicationShortcut context so it works even when focus is in QTextEdit widgets
7328
+ mt_quick_shortcut.setContext(Qt.ShortcutContext.ApplicationShortcut)
7329
+
7278
7330
  def focus_segment_notes(self):
7279
7331
  """Switch to Segment Note tab and focus the notes editor so user can start typing immediately"""
7280
7332
  if not hasattr(self, 'right_tabs'):
@@ -7346,7 +7398,105 @@ class SupervertalerQt(QMainWindow):
7346
7398
 
7347
7399
  except Exception as e:
7348
7400
  self.log(f"❌ Error opening QuickMenu: {e}")
7349
-
7401
+
7402
+ def show_mt_quick_popup(self, text_override: str = None):
7403
+ """Show GT4T-style MT Quick Lookup popup with translations from all enabled MT engines.
7404
+
7405
+ Triggered by Ctrl+Shift+Q or right-click menu. Shows machine translation
7406
+ suggestions from all configured and enabled MT providers in a popup window.
7407
+
7408
+ Args:
7409
+ text_override: Optional text to translate. If None, uses selected text
7410
+ in the current cell, or falls back to the full source text.
7411
+
7412
+ Features:
7413
+ - Displays source text at top
7414
+ - Shows numbered list of MT suggestions from each provider
7415
+ - Press 1-9 to quickly insert a translation
7416
+ - Arrow keys to navigate, Enter to insert selected
7417
+ - Escape to dismiss
7418
+ """
7419
+ try:
7420
+ # Get current segment
7421
+ current_row = self.table.currentRow()
7422
+ if current_row < 0:
7423
+ self.log("⚠️ No segment selected")
7424
+ return
7425
+
7426
+ # Determine what text to translate
7427
+ text_to_translate = text_override
7428
+
7429
+ if not text_to_translate:
7430
+ # Check for selected text in the currently focused widget
7431
+ focus_widget = QApplication.focusWidget()
7432
+ if focus_widget and hasattr(focus_widget, 'textCursor'):
7433
+ cursor = focus_widget.textCursor()
7434
+ if cursor.hasSelection():
7435
+ text_to_translate = cursor.selectedText().strip()
7436
+
7437
+ if not text_to_translate:
7438
+ # Fall back to full source text
7439
+ source_widget = self.table.cellWidget(current_row, 2)
7440
+ if not source_widget or not hasattr(source_widget, 'toPlainText'):
7441
+ self.log("⚠️ Could not get source text")
7442
+ return
7443
+ text_to_translate = source_widget.toPlainText().strip()
7444
+
7445
+ if not text_to_translate:
7446
+ self.log("⚠️ No text to translate")
7447
+ return
7448
+
7449
+ # Import and create the popup
7450
+ from modules.mt_quick_popup import MTQuickPopup
7451
+
7452
+ # Create popup
7453
+ popup = MTQuickPopup(
7454
+ parent_app=self,
7455
+ source_text=text_to_translate,
7456
+ source_lang=getattr(self, 'source_language', 'en'),
7457
+ target_lang=getattr(self, 'target_language', 'nl'),
7458
+ parent=self
7459
+ )
7460
+
7461
+ # Connect signal to insert translation into target cell
7462
+ def insert_translation(translation: str):
7463
+ # Get the target widget
7464
+ target_widget = self.table.cellWidget(current_row, 3)
7465
+ if not target_widget or not hasattr(target_widget, 'toPlainText'):
7466
+ return
7467
+
7468
+ # Check if there was a selection in the target - replace just that
7469
+ focus_widget = QApplication.focusWidget()
7470
+ if focus_widget == target_widget and hasattr(focus_widget, 'textCursor'):
7471
+ cursor = focus_widget.textCursor()
7472
+ if cursor.hasSelection():
7473
+ # Replace selection only
7474
+ cursor.insertText(translation)
7475
+ self.log(f"✅ Replaced selection with MT translation")
7476
+ else:
7477
+ # No selection in target, replace entire target
7478
+ target_widget.setPlainText(translation)
7479
+ self.log(f"✅ Inserted MT translation")
7480
+ else:
7481
+ # Focus was elsewhere, replace entire target
7482
+ target_widget.setPlainText(translation)
7483
+ self.log(f"✅ Inserted MT translation")
7484
+
7485
+ # Mark segment as modified
7486
+ if hasattr(self, 'segments') and current_row < len(self.segments):
7487
+ self.segments[current_row].target = target_widget.toPlainText()
7488
+ self.mark_segment_modified(current_row)
7489
+
7490
+ popup.translation_selected.connect(insert_translation)
7491
+
7492
+ # Show the popup (it positions itself near cursor)
7493
+ popup.show()
7494
+
7495
+ except ImportError as e:
7496
+ self.log(f"❌ MT Quick Popup module not found: {e}")
7497
+ except Exception as e:
7498
+ self.log(f"❌ Error showing MT Quick Popup: {e}")
7499
+
7350
7500
  def refresh_shortcut_enabled_states(self):
7351
7501
  """Refresh enabled/disabled states and key bindings of all global shortcuts from shortcut manager.
7352
7502
 
@@ -7982,7 +8132,7 @@ class SupervertalerQt(QMainWindow):
7982
8132
  edit_menu.addSeparator()
7983
8133
 
7984
8134
  # Superlookup
7985
- superlookup_action = QAction("🔍 &Superlookup...", self)
8135
+ superlookup_action = QAction("🔍 &SuperLookup...", self)
7986
8136
  superlookup_action.setShortcut("Ctrl+Alt+L")
7987
8137
  # Tab indices: Grid=0, Project resources=1, Tools=2, Settings=3
7988
8138
  superlookup_action.triggered.connect(lambda: self._go_to_superlookup() if hasattr(self, 'main_tabs') else None) # Navigate to Superlookup
@@ -8020,7 +8170,7 @@ class SupervertalerQt(QMainWindow):
8020
8170
  grid_zoom_menu = view_menu.addMenu("📊 &Grid Text Zoom")
8021
8171
 
8022
8172
  grid_zoom_in = QAction("Grid Zoom &In", self)
8023
- grid_zoom_in.setShortcut(QKeySequence.StandardKey.ZoomIn)
8173
+ grid_zoom_in.setShortcut("Ctrl+=")
8024
8174
  grid_zoom_in.triggered.connect(self.zoom_in)
8025
8175
  grid_zoom_menu.addAction(grid_zoom_in)
8026
8176
 
@@ -8089,7 +8239,7 @@ class SupervertalerQt(QMainWindow):
8089
8239
  # Termview visibility toggle
8090
8240
  self.termview_visible_action = QAction("🔍 &Termview Under Grid", self)
8091
8241
  self.termview_visible_action.setCheckable(True)
8092
- self.termview_visible_action.setChecked(True) # Default: visible
8242
+ self.termview_visible_action.setChecked(False) # Default: hidden (restored from settings if enabled)
8093
8243
  self.termview_visible_action.triggered.connect(self.toggle_termview_under_grid)
8094
8244
  self.termview_visible_action.setToolTip("Show/hide the Termview panel under the grid")
8095
8245
  view_menu.addAction(self.termview_visible_action)
@@ -10129,7 +10279,7 @@ class SupervertalerQt(QMainWindow):
10129
10279
 
10130
10280
  # Create detached window
10131
10281
  self.lookup_detached_window = QDialog(self)
10132
- self.lookup_detached_window.setWindowTitle("🔍 Superlookup - Supervertaler")
10282
+ self.lookup_detached_window.setWindowTitle("🔍 SuperLookup - Supervertaler")
10133
10283
  self.lookup_detached_window.setMinimumSize(600, 700)
10134
10284
  self.lookup_detached_window.resize(700, 800)
10135
10285
 
@@ -10183,7 +10333,7 @@ class SupervertalerQt(QMainWindow):
10183
10333
  # Header with reattach button
10184
10334
  header_layout = QVBoxLayout()
10185
10335
 
10186
- header_title = QLabel("🔍 Superlookup")
10336
+ header_title = QLabel("🔍 SuperLookup")
10187
10337
  header_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #333;")
10188
10338
  header_layout.addWidget(header_title)
10189
10339
 
@@ -10335,12 +10485,9 @@ class SupervertalerQt(QMainWindow):
10335
10485
 
10336
10486
  # Superdocs removed (online GitBook will be used instead)
10337
10487
 
10338
- print("[DEBUG] About to create SuperlookupTab...")
10339
10488
  lookup_tab = SuperlookupTab(self, user_data_path=self.user_data_path)
10340
- print("[DEBUG] SuperlookupTab created successfully")
10341
10489
  self.lookup_tab = lookup_tab # Store reference for later use
10342
- modules_tabs.addTab(lookup_tab, "🔍 Superlookup")
10343
- print("[DEBUG] Superlookup tab added to modules_tabs")
10490
+ modules_tabs.addTab(lookup_tab, "🔍 SuperLookup")
10344
10491
 
10345
10492
  # Supervoice - Voice Commands & Dictation
10346
10493
  supervoice_tab = self._create_voice_dictation_settings_tab()
@@ -12202,27 +12349,130 @@ class SupervertalerQt(QMainWindow):
12202
12349
  f"Error testing segmentation:\n\n{e}"
12203
12350
  )
12204
12351
 
12205
- def _update_both_termviews(self, source_text, termbase_list, nt_matches):
12352
+ def _update_both_termviews(self, source_text, termbase_list, nt_matches, status_hint=None):
12206
12353
  """Update all three Termview instances with the same data.
12207
-
12354
+
12208
12355
  Termview locations:
12209
12356
  1. Under grid (collapsible via View menu)
12210
12357
  2. Match Panel tab (top section)
12358
+
12359
+ Args:
12360
+ source_text: The source text for the current segment
12361
+ termbase_list: List of termbase match dictionaries
12362
+ nt_matches: List of NT (Never Translate) matches
12363
+ status_hint: Optional hint for display when no matches:
12364
+ 'no_termbases_activated' - no glossaries activated for project
12365
+ 'wrong_language' - activated glossaries don't match project language
12211
12366
  """
12212
12367
  # Update left Termview (under grid)
12213
12368
  if hasattr(self, 'termview_widget') and self.termview_widget:
12214
12369
  try:
12215
- self.termview_widget.update_with_matches(source_text, termbase_list, nt_matches)
12370
+ self.termview_widget.update_with_matches(source_text, termbase_list, nt_matches, status_hint)
12216
12371
  except Exception as e:
12217
12372
  self.log(f"Error updating left termview: {e}")
12218
-
12373
+
12219
12374
  # Update Match Panel Termview
12220
12375
  if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
12221
12376
  try:
12222
- self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches)
12377
+ self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches, status_hint)
12223
12378
  except Exception as e:
12224
12379
  self.log(f"Error updating Match Panel termview: {e}")
12225
-
12380
+
12381
+ def _update_termview_for_segment(self, segment):
12382
+ """Explicitly update termview for a segment (v1.9.182).
12383
+
12384
+ This is called directly from Ctrl+Enter navigation to ensure
12385
+ the termview updates immediately, bypassing the deferred timer approach.
12386
+ """
12387
+ if not segment or not hasattr(self, 'termview_widget'):
12388
+ return
12389
+
12390
+ try:
12391
+ # Use in-memory index for fast lookup
12392
+ stored_matches = self.find_termbase_matches_in_source(segment.source)
12393
+
12394
+ # Convert dict format to list format for termview
12395
+ termbase_matches = [
12396
+ {
12397
+ 'source_term': match_data.get('source', ''),
12398
+ 'target_term': match_data.get('translation', ''),
12399
+ 'termbase_name': match_data.get('termbase_name', ''),
12400
+ 'ranking': match_data.get('ranking', 99),
12401
+ 'is_project_termbase': match_data.get('is_project_termbase', False),
12402
+ 'term_id': match_data.get('term_id'),
12403
+ 'termbase_id': match_data.get('termbase_id'),
12404
+ 'notes': match_data.get('notes', '')
12405
+ }
12406
+ for match_data in stored_matches.values()
12407
+ ] if stored_matches else []
12408
+
12409
+ # Get NT matches
12410
+ nt_matches = self.find_nt_matches_in_source(segment.source)
12411
+
12412
+ # Get status hint
12413
+ status_hint = self._get_termbase_status_hint()
12414
+
12415
+ # Update both Termview widgets
12416
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
12417
+
12418
+ except Exception as e:
12419
+ self.log(f"Error in _update_termview_for_segment: {e}")
12420
+
12421
+ def _get_termbase_status_hint(self) -> str:
12422
+ """Check termbase activation status and return appropriate hint.
12423
+
12424
+ Returns:
12425
+ 'no_termbases_activated' - if no glossaries are activated for this project
12426
+ 'wrong_language' - if activated glossaries don't match project language pair
12427
+ None - if everything is correctly configured
12428
+ """
12429
+ if not self.current_project:
12430
+ return None
12431
+
12432
+ project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
12433
+ if not project_id:
12434
+ return None
12435
+
12436
+ # Check if termbase manager is available
12437
+ if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
12438
+ return None
12439
+
12440
+ try:
12441
+ # Get active termbase IDs for this project
12442
+ active_tb_ids = self.termbase_mgr.get_active_termbase_ids(project_id)
12443
+
12444
+ # Check if no termbases are activated
12445
+ if not active_tb_ids or len(active_tb_ids) == 0:
12446
+ return 'no_termbases_activated'
12447
+
12448
+ # Check if any activated termbases match the project's language pair
12449
+ project_source = (self.current_project.source_lang or '').lower()
12450
+ project_target = (self.current_project.target_lang or '').lower()
12451
+
12452
+ # Get all termbases and check language pairs
12453
+ all_termbases = self.termbase_mgr.get_all_termbases()
12454
+ has_matching_language = False
12455
+
12456
+ for tb in all_termbases:
12457
+ if tb['id'] in active_tb_ids:
12458
+ tb_source = (tb.get('source_lang') or '').lower()
12459
+ tb_target = (tb.get('target_lang') or '').lower()
12460
+ # Match if: no language set, or languages match (bidirectional)
12461
+ if (not tb_source and not tb_target) or \
12462
+ (tb_source == project_source and tb_target == project_target) or \
12463
+ (tb_source == project_target and tb_target == project_source):
12464
+ has_matching_language = True
12465
+ break
12466
+
12467
+ if not has_matching_language:
12468
+ return 'wrong_language'
12469
+
12470
+ return None # All good
12471
+
12472
+ except Exception as e:
12473
+ self.log(f"Error checking termbase status: {e}")
12474
+ return None
12475
+
12226
12476
  def _refresh_termbase_display_for_current_segment(self):
12227
12477
  """Refresh only termbase/glossary display for the current segment.
12228
12478
 
@@ -12282,9 +12532,12 @@ class SupervertalerQt(QMainWindow):
12282
12532
 
12283
12533
  # Get NT matches
12284
12534
  nt_matches = self.find_nt_matches_in_source(segment.source)
12285
-
12535
+
12536
+ # Get status hint for termbase activation
12537
+ status_hint = self._get_termbase_status_hint()
12538
+
12286
12539
  # Update both Termview widgets (left and right)
12287
- self._update_both_termviews(segment.source, termbase_list, nt_matches)
12540
+ self._update_both_termviews(segment.source, termbase_list, nt_matches, status_hint)
12288
12541
  except Exception as e:
12289
12542
  self.log(f"Error updating termview: {e}")
12290
12543
 
@@ -12752,6 +13005,39 @@ class SupervertalerQt(QMainWindow):
12752
13005
  # Use term_id as key to avoid duplicates
12753
13006
  self.termbase_cache[segment_id][term_id] = new_match
12754
13007
  self.log(f"⚡ Added term directly to cache (instant update)")
13008
+
13009
+ # v1.9.182: Also add to in-memory termbase index for future lookups
13010
+ import re
13011
+ source_lower = source_text.lower().strip()
13012
+ try:
13013
+ if any(c in source_lower for c in '.%,/-'):
13014
+ pattern = re.compile(r'(?<!\w)' + re.escape(source_lower) + r'(?!\w)')
13015
+ else:
13016
+ pattern = re.compile(r'\b' + re.escape(source_lower) + r'\b')
13017
+ except re.error:
13018
+ pattern = None
13019
+
13020
+ index_entry = {
13021
+ 'term_id': term_id,
13022
+ 'source_term': source_text,
13023
+ 'source_term_lower': source_lower,
13024
+ 'target_term': target_text,
13025
+ 'termbase_id': target_termbase['id'],
13026
+ 'priority': 99,
13027
+ 'domain': '',
13028
+ 'notes': '',
13029
+ 'project': '',
13030
+ 'client': '',
13031
+ 'forbidden': False,
13032
+ 'is_project_termbase': False,
13033
+ 'termbase_name': target_termbase['name'],
13034
+ 'ranking': glossary_rank,
13035
+ 'pattern': pattern,
13036
+ }
13037
+ with self.termbase_index_lock:
13038
+ self.termbase_index.append(index_entry)
13039
+ # Re-sort by length (longest first) for proper phrase matching
13040
+ self.termbase_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
12755
13041
 
12756
13042
  # Update TermView widget with the new term
12757
13043
  if hasattr(self, 'termview_widget') and self.termview_widget:
@@ -12776,9 +13062,12 @@ class SupervertalerQt(QMainWindow):
12776
13062
 
12777
13063
  # Get NT matches
12778
13064
  nt_matches = self.find_nt_matches_in_source(segment.source)
12779
-
13065
+
13066
+ # Get status hint (although after adding a term, it should be fine)
13067
+ status_hint = self._get_termbase_status_hint()
13068
+
12780
13069
  # Update both Termview widgets (left and right)
12781
- self._update_both_termviews(segment.source, termbase_list, nt_matches)
13070
+ self._update_both_termviews(segment.source, termbase_list, nt_matches, status_hint)
12782
13071
  self.log(f"✅ Both TermView widgets updated instantly with new term")
12783
13072
 
12784
13073
  # Update source cell highlighting with updated cache
@@ -13634,15 +13923,16 @@ class SupervertalerQt(QMainWindow):
13634
13923
  # Use 0 (global) when no project is loaded - allows Superlookup to work
13635
13924
  curr_proj = self.current_project if hasattr(self, 'current_project') else None
13636
13925
  curr_proj_id = curr_proj.id if (curr_proj and hasattr(curr_proj, 'id')) else 0 # 0 = global
13637
-
13926
+
13638
13927
  if checked:
13639
13928
  termbase_mgr.activate_termbase(tb_id, curr_proj_id)
13640
13929
  else:
13641
13930
  termbase_mgr.deactivate_termbase(tb_id, curr_proj_id)
13642
-
13643
- # Clear cache and refresh
13931
+
13932
+ # Clear cache and rebuild in-memory index (v1.9.182)
13644
13933
  with self.termbase_cache_lock:
13645
13934
  self.termbase_cache.clear()
13935
+ self._build_termbase_index() # Rebuild index with new activation state
13646
13936
  refresh_termbase_list()
13647
13937
 
13648
13938
  read_checkbox.toggled.connect(on_read_toggle)
@@ -14846,7 +15136,7 @@ class SupervertalerQt(QMainWindow):
14846
15136
 
14847
15137
  # Delete selected term button
14848
15138
  delete_term_btn = QPushButton("🗑️ Delete Selected Term")
14849
- delete_term_btn.setStyleSheet("background-color: #f44336; color: white; font-weight: bold;")
15139
+ delete_term_btn.setStyleSheet("background-color: #f44336; color: white; font-weight: bold; padding: 3px 5px;")
14850
15140
  def delete_selected_term():
14851
15141
  selected_row = terms_table.currentRow()
14852
15142
  if selected_row < 0:
@@ -15520,8 +15810,13 @@ class SupervertalerQt(QMainWindow):
15520
15810
  # ===== TAB 4: MT Settings =====
15521
15811
  mt_tab = self._create_mt_settings_tab()
15522
15812
  settings_tabs.addTab(scroll_area_wrapper(mt_tab), "🌐 MT Settings")
15523
-
15524
- # ===== TAB 5: View Settings =====
15813
+
15814
+ # ===== TAB 5: MT Quick Lookup Settings =====
15815
+ mt_quick_tab = self._create_mt_quick_lookup_settings_tab()
15816
+ settings_tabs.addTab(scroll_area_wrapper(mt_quick_tab), "⚡ QuickTrans")
15817
+ self.mt_quick_lookup_tab_index = settings_tabs.count() - 1 # Store index for opening
15818
+
15819
+ # ===== TAB 6: View Settings =====
15525
15820
  view_tab = self._create_view_settings_tab()
15526
15821
  settings_tabs.addTab(scroll_area_wrapper(view_tab), "🔍 View Settings")
15527
15822
 
@@ -16329,9 +16624,199 @@ class SupervertalerQt(QMainWindow):
16329
16624
  layout.addWidget(save_btn)
16330
16625
 
16331
16626
  layout.addStretch()
16332
-
16627
+
16333
16628
  return tab
16334
-
16629
+
16630
+ def _create_mt_quick_lookup_settings_tab(self):
16631
+ """Create MT Quick Lookup settings tab content"""
16632
+ from PyQt6.QtWidgets import QCheckBox, QGroupBox, QPushButton, QComboBox
16633
+
16634
+ tab = QWidget()
16635
+ layout = QVBoxLayout(tab)
16636
+ layout.setContentsMargins(20, 20, 20, 20)
16637
+ layout.setSpacing(15)
16638
+
16639
+ # Load current settings
16640
+ general_settings = self.load_general_settings()
16641
+ mt_quick_settings = general_settings.get('mt_quick_lookup', {})
16642
+ api_keys = self.load_api_keys()
16643
+ enabled_providers = self.load_provider_enabled_states()
16644
+
16645
+ # Header info
16646
+ header_info = QLabel(
16647
+ "⚡ <b>QuickTrans</b> - Configure which providers appear in the QuickTrans popup (Ctrl+M / Ctrl+Alt+M).<br>"
16648
+ "Enable MT engines and/or LLMs to get instant translation suggestions."
16649
+ )
16650
+ header_info.setTextFormat(Qt.TextFormat.RichText)
16651
+ header_info.setStyleSheet("font-size: 9pt; color: #444; padding: 10px; background-color: #E3F2FD; border-radius: 4px;")
16652
+ header_info.setWordWrap(True)
16653
+ layout.addWidget(header_info)
16654
+
16655
+ # ===== MT Providers Group =====
16656
+ mt_group = QGroupBox("🌐 Machine Translation Providers")
16657
+ mt_layout = QVBoxLayout()
16658
+
16659
+ mt_info = QLabel("Select which MT engines to query. Only enabled providers with valid API keys are shown.")
16660
+ mt_info.setWordWrap(True)
16661
+ mt_info.setStyleSheet("font-size: 8pt; color: #666; padding-bottom: 8px;")
16662
+ mt_layout.addWidget(mt_info)
16663
+
16664
+ # MT provider checkboxes
16665
+ self._mtql_checkboxes = {}
16666
+
16667
+ mt_providers = [
16668
+ ("gt", "Google Translate", "mt_google_translate", "google_translate"),
16669
+ ("dl", "DeepL", "mt_deepl", "deepl"),
16670
+ ("ms", "Microsoft Translator", "mt_microsoft", "microsoft_translate"),
16671
+ ("at", "Amazon Translate", "mt_amazon", "amazon_translate"),
16672
+ ("mmt", "ModernMT", "mt_modernmt", "modernmt"),
16673
+ ("mm", "MyMemory (Free)", "mt_mymemory", None),
16674
+ ]
16675
+
16676
+ for code, name, enabled_key, api_key_name in mt_providers:
16677
+ # Check if provider is available (has API key or doesn't need one)
16678
+ has_key = api_key_name is None or bool(api_keys.get(api_key_name))
16679
+ is_enabled_globally = enabled_providers.get(enabled_key, True)
16680
+
16681
+ checkbox = CheckmarkCheckBox(name)
16682
+ # Default: use global MT enabled state
16683
+ checkbox.setChecked(mt_quick_settings.get(f"mtql_{code}", is_enabled_globally and has_key))
16684
+ checkbox.setEnabled(has_key)
16685
+
16686
+ if not has_key:
16687
+ checkbox.setToolTip(f"API key not configured for {name}")
16688
+ checkbox.setStyleSheet("color: #999;")
16689
+ else:
16690
+ checkbox.setToolTip(f"Include {name} in QuickTrans results")
16691
+
16692
+ self._mtql_checkboxes[f"mtql_{code}"] = checkbox
16693
+ mt_layout.addWidget(checkbox)
16694
+
16695
+ mt_group.setLayout(mt_layout)
16696
+ layout.addWidget(mt_group)
16697
+
16698
+ # ===== LLM Providers Group =====
16699
+ llm_group = QGroupBox("🤖 AI/LLM Providers")
16700
+ llm_layout = QVBoxLayout()
16701
+
16702
+ llm_info = QLabel(
16703
+ "Enable AI models for translation suggestions. LLMs may provide more context-aware translations but are slower.<br>"
16704
+ "<b>Note:</b> LLM calls cost more than MT APIs. Use sparingly for quick lookups."
16705
+ )
16706
+ llm_info.setTextFormat(Qt.TextFormat.RichText)
16707
+ llm_info.setWordWrap(True)
16708
+ llm_info.setStyleSheet("font-size: 8pt; color: #666; padding-bottom: 8px;")
16709
+ llm_layout.addWidget(llm_info)
16710
+
16711
+ # LLM provider checkboxes with model selection
16712
+ self._mtql_llm_combos = {}
16713
+
16714
+ llm_providers = [
16715
+ ("claude", "Claude", "claude", [
16716
+ ("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5 (Recommended)"),
16717
+ ("claude-haiku-4-5-20251001", "Claude Haiku 4.5 (Fast)"),
16718
+ ("claude-opus-4-1-20250924", "Claude Opus 4.1 (Premium)"),
16719
+ ]),
16720
+ ("openai", "OpenAI", "openai", [
16721
+ ("gpt-4o", "GPT-4o (Recommended)"),
16722
+ ("gpt-4o-mini", "GPT-4o Mini (Fast)"),
16723
+ ("gpt-4-turbo", "GPT-4 Turbo"),
16724
+ ("o1", "o1 (Reasoning)"),
16725
+ ]),
16726
+ ("gemini", "Gemini", "gemini", [
16727
+ ("gemini-2.5-flash", "Gemini 2.5 Flash (Recommended)"),
16728
+ ("gemini-2.5-pro", "Gemini 2.5 Pro"),
16729
+ ("gemini-2.0-flash", "Gemini 2.0 Flash"),
16730
+ ]),
16731
+ ]
16732
+
16733
+ for code, name, api_key_name, models in llm_providers:
16734
+ has_key = bool(api_keys.get(api_key_name))
16735
+
16736
+ # Container for checkbox and model combo
16737
+ llm_row = QHBoxLayout()
16738
+
16739
+ checkbox = CheckmarkCheckBox(name)
16740
+ # Default: disabled (LLMs are opt-in)
16741
+ checkbox.setChecked(mt_quick_settings.get(f"mtql_{code}", False))
16742
+ checkbox.setEnabled(has_key)
16743
+
16744
+ if not has_key:
16745
+ checkbox.setToolTip(f"API key not configured for {name}. Add it in AI Settings.")
16746
+ checkbox.setStyleSheet("color: #999;")
16747
+ else:
16748
+ checkbox.setToolTip(f"Include {name} translations in QuickTrans")
16749
+
16750
+ self._mtql_checkboxes[f"mtql_{code}"] = checkbox
16751
+ llm_row.addWidget(checkbox)
16752
+
16753
+ # Model selection combo
16754
+ model_combo = QComboBox()
16755
+ model_combo.setMinimumWidth(200)
16756
+ for model_id, model_name in models:
16757
+ model_combo.addItem(model_name, model_id)
16758
+
16759
+ # Restore saved model selection
16760
+ saved_model = mt_quick_settings.get(f"mtql_{code}_model")
16761
+ if saved_model:
16762
+ idx = model_combo.findData(saved_model)
16763
+ if idx >= 0:
16764
+ model_combo.setCurrentIndex(idx)
16765
+
16766
+ model_combo.setEnabled(has_key)
16767
+ self._mtql_llm_combos[f"mtql_{code}_model"] = model_combo
16768
+ llm_row.addWidget(model_combo)
16769
+
16770
+ llm_row.addStretch()
16771
+ llm_layout.addLayout(llm_row)
16772
+
16773
+ llm_group.setLayout(llm_layout)
16774
+ layout.addWidget(llm_group)
16775
+
16776
+ # Save button
16777
+ save_btn = QPushButton("💾 Save QuickTrans Settings")
16778
+ save_btn.setStyleSheet("font-weight: bold; padding: 8px;")
16779
+ save_btn.clicked.connect(self._save_mt_quick_lookup_settings)
16780
+ layout.addWidget(save_btn)
16781
+
16782
+ layout.addStretch()
16783
+
16784
+ return tab
16785
+
16786
+ def _save_mt_quick_lookup_settings(self):
16787
+ """Save MT Quick Lookup settings"""
16788
+ general_settings = self.load_general_settings()
16789
+
16790
+ mt_quick_settings = {}
16791
+
16792
+ # Save MT provider states
16793
+ for key, checkbox in self._mtql_checkboxes.items():
16794
+ mt_quick_settings[key] = checkbox.isChecked()
16795
+
16796
+ # Save LLM model selections
16797
+ for key, combo in self._mtql_llm_combos.items():
16798
+ mt_quick_settings[key] = combo.currentData()
16799
+
16800
+ general_settings['mt_quick_lookup'] = mt_quick_settings
16801
+ self.save_general_settings(general_settings)
16802
+
16803
+ self.log("✓ QuickTrans settings saved")
16804
+ QMessageBox.information(self, "Settings Saved", "QuickTrans settings have been saved.")
16805
+
16806
+ def open_mt_quick_lookup_settings(self):
16807
+ """Open Settings and navigate to MT Quick Lookup tab"""
16808
+ # Switch to Settings tab
16809
+ if hasattr(self, 'main_tabs'):
16810
+ # Find Settings tab index
16811
+ for i in range(self.main_tabs.count()):
16812
+ if "Settings" in self.main_tabs.tabText(i):
16813
+ self.main_tabs.setCurrentIndex(i)
16814
+ break
16815
+
16816
+ # Navigate to MT Quick Lookup sub-tab
16817
+ if hasattr(self, 'settings_tabs') and hasattr(self, 'mt_quick_lookup_tab_index'):
16818
+ self.settings_tabs.setCurrentIndex(self.mt_quick_lookup_tab_index)
16819
+
16335
16820
  def _find_autohotkey_for_settings(self):
16336
16821
  """Find AutoHotkey executable for settings display (doesn't modify state)"""
16337
16822
  # Standard installation paths
@@ -16971,7 +17456,7 @@ class SupervertalerQt(QMainWindow):
16971
17456
 
16972
17457
  # Cache kill switch
16973
17458
  disable_cache_cb = CheckmarkCheckBox("Disable ALL caches (direct lookups every time)")
16974
- disable_cache_cb.setChecked(general_settings.get('disable_all_caches', True))
17459
+ disable_cache_cb.setChecked(general_settings.get('disable_all_caches', False))
16975
17460
  disable_cache_cb.setToolTip(
16976
17461
  "When enabled, ALL caching is bypassed:\n"
16977
17462
  "• Termbase cache\n"
@@ -18338,7 +18823,7 @@ class SupervertalerQt(QMainWindow):
18338
18823
  background-color: #2E7D32;
18339
18824
  color: white;
18340
18825
  font-weight: bold;
18341
- padding: 4px 8px;
18826
+ padding: 3px 5px;
18342
18827
  border-radius: 3px;
18343
18828
  }
18344
18829
  QPushButton:checked {
@@ -18352,7 +18837,7 @@ class SupervertalerQt(QMainWindow):
18352
18837
  background-color: #C62828;
18353
18838
  color: white;
18354
18839
  font-weight: bold;
18355
- padding: 4px 8px;
18840
+ padding: 3px 5px;
18356
18841
  border-radius: 3px;
18357
18842
  }
18358
18843
  QPushButton:checked {
@@ -18366,7 +18851,7 @@ class SupervertalerQt(QMainWindow):
18366
18851
  background-color: #F57C00;
18367
18852
  color: white;
18368
18853
  font-weight: bold;
18369
- padding: 4px 8px;
18854
+ padding: 3px 5px;
18370
18855
  border-radius: 3px;
18371
18856
  }
18372
18857
  QPushButton:checked {
@@ -18381,7 +18866,7 @@ class SupervertalerQt(QMainWindow):
18381
18866
  background-color: #757575;
18382
18867
  color: white;
18383
18868
  font-weight: bold;
18384
- padding: 4px 8px;
18869
+ padding: 3px 5px;
18385
18870
  border-radius: 3px;
18386
18871
  }
18387
18872
  QPushButton:checked {
@@ -19615,7 +20100,7 @@ class SupervertalerQt(QMainWindow):
19615
20100
  'results_compare_font_size': 9,
19616
20101
  'autohotkey_path': ahk_path_edit.text().strip() if ahk_path_edit is not None else existing_settings.get('autohotkey_path', ''),
19617
20102
  'enable_sound_effects': sound_effects_cb.isChecked() if sound_effects_cb is not None else existing_settings.get('enable_sound_effects', False),
19618
- 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', True)
20103
+ 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', False)
19619
20104
  }
19620
20105
 
19621
20106
  # Keep a fast-access instance value
@@ -19715,6 +20200,27 @@ class SupervertalerQt(QMainWindow):
19715
20200
  grid_font_family_combo=None, termview_font_family_combo=None, termview_font_spin=None, termview_bold_check=None,
19716
20201
  border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None):
19717
20202
  """Save view settings from UI"""
20203
+ # CRITICAL: Suppress TM saves during view settings update
20204
+ # Grid operations (setStyleSheet, rehighlight, etc.) can trigger textChanged events
20205
+ # which would cause mass TM saves for all confirmed segments
20206
+ previous_suppression = getattr(self, '_suppress_target_change_handlers', False)
20207
+ self._suppress_target_change_handlers = True
20208
+
20209
+ try:
20210
+ self._save_view_settings_from_ui_impl(
20211
+ grid_spin, match_spin, compare_spin, show_tags_check, tag_color_btn,
20212
+ alt_colors_check, even_color_btn, odd_color_btn, invisible_char_color_btn,
20213
+ grid_font_family_combo, termview_font_family_combo, termview_font_spin, termview_bold_check,
20214
+ border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check
20215
+ )
20216
+ finally:
20217
+ self._suppress_target_change_handlers = previous_suppression
20218
+
20219
+ def _save_view_settings_from_ui_impl(self, grid_spin, match_spin, compare_spin, show_tags_check=None, tag_color_btn=None,
20220
+ alt_colors_check=None, even_color_btn=None, odd_color_btn=None, invisible_char_color_btn=None,
20221
+ grid_font_family_combo=None, termview_font_family_combo=None, termview_font_spin=None, termview_bold_check=None,
20222
+ border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None):
20223
+ """Implementation of save view settings (called with TM saves suppressed)"""
19718
20224
  general_settings = {
19719
20225
  'restore_last_project': self.load_general_settings().get('restore_last_project', False),
19720
20226
  'auto_propagate_exact_matches': self.auto_propagate_exact_matches, # Keep existing value
@@ -19885,8 +20391,10 @@ class SupervertalerQt(QMainWindow):
19885
20391
  if invisible_char_color_btn and hasattr(self, 'table') and self.table is not None:
19886
20392
  invisible_char_color = invisible_char_color_btn.property('selected_color')
19887
20393
  if invisible_char_color:
19888
- # Update all cell highlighters
20394
+ # Update all cell highlighters (with processEvents to keep UI responsive)
19889
20395
  for row in range(self.table.rowCount()):
20396
+ if row % 50 == 0:
20397
+ QApplication.processEvents()
19890
20398
  for col in [2, 3]: # Source and target columns
19891
20399
  widget = self.table.cellWidget(row, col)
19892
20400
  if widget and hasattr(widget, 'highlighter'):
@@ -19898,8 +20406,10 @@ class SupervertalerQt(QMainWindow):
19898
20406
  border_color = EditableGridTextEditor.focus_border_color
19899
20407
  border_thickness = EditableGridTextEditor.focus_border_thickness
19900
20408
  self.log(f"Applying focus border: color={border_color}, thickness={border_thickness}px")
19901
-
20409
+
19902
20410
  for row in range(self.table.rowCount()):
20411
+ if row % 50 == 0:
20412
+ QApplication.processEvents()
19903
20413
  widget = self.table.cellWidget(row, 3) # Target column
19904
20414
  if widget and isinstance(widget, EditableGridTextEditor):
19905
20415
  # Update the stylesheet with new border settings
@@ -20005,7 +20515,7 @@ class SupervertalerQt(QMainWindow):
20005
20515
  filter_layout.setSpacing(10)
20006
20516
 
20007
20517
  # Source filter
20008
- source_filter_label = QLabel("Filter Source:")
20518
+ source_filter_label = QLabel("Source:")
20009
20519
  self.source_filter = self._ensure_shared_filter(
20010
20520
  'source_filter',
20011
20521
  "Type to filter source segments...",
@@ -20014,7 +20524,7 @@ class SupervertalerQt(QMainWindow):
20014
20524
  )
20015
20525
 
20016
20526
  # Target filter
20017
- target_filter_label = QLabel("Filter Target:")
20527
+ target_filter_label = QLabel("Target:")
20018
20528
  self.target_filter = self._ensure_shared_filter(
20019
20529
  'target_filter',
20020
20530
  "Type to filter target segments...",
@@ -20026,11 +20536,12 @@ class SupervertalerQt(QMainWindow):
20026
20536
  clear_filters_btn = QPushButton("Clear Filters")
20027
20537
  clear_filters_btn.clicked.connect(self.clear_filters)
20028
20538
  clear_filters_btn.setMaximumWidth(100)
20539
+ clear_filters_btn.setStyleSheet("padding: 3px 5px;")
20029
20540
 
20030
20541
  # Show Invisibles button with dropdown menu
20031
20542
  show_invisibles_btn = QPushButton("¶ Show Invisibles")
20032
20543
  show_invisibles_btn.setMaximumWidth(140)
20033
- show_invisibles_btn.setStyleSheet("background-color: #607D8B; color: white; font-weight: bold;")
20544
+ show_invisibles_btn.setStyleSheet("background-color: #607D8B; color: white; font-weight: bold; padding: 3px 5px;")
20034
20545
  show_invisibles_menu = QMenu(show_invisibles_btn)
20035
20546
 
20036
20547
  # Create checkable actions for each invisible character type
@@ -20155,7 +20666,7 @@ class SupervertalerQt(QMainWindow):
20155
20666
  filter_layout.setSpacing(10)
20156
20667
 
20157
20668
  # Source filter
20158
- source_filter_label = QLabel("Filter Source:")
20669
+ source_filter_label = QLabel("Source:")
20159
20670
  self.source_filter = self._ensure_shared_filter(
20160
20671
  'source_filter',
20161
20672
  "Type to filter source segments... (Press Enter or click Filter)",
@@ -20163,7 +20674,7 @@ class SupervertalerQt(QMainWindow):
20163
20674
  )
20164
20675
 
20165
20676
  # Target filter
20166
- target_filter_label = QLabel("Filter Target:")
20677
+ target_filter_label = QLabel("Target:")
20167
20678
  self.target_filter = self._ensure_shared_filter(
20168
20679
  'target_filter',
20169
20680
  "Type to filter target segments... (Press Enter or click Filter)",
@@ -20174,17 +20685,18 @@ class SupervertalerQt(QMainWindow):
20174
20685
  apply_filter_btn = QPushButton("Filter")
20175
20686
  apply_filter_btn.clicked.connect(self.apply_filters)
20176
20687
  apply_filter_btn.setMaximumWidth(80)
20177
- apply_filter_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
20688
+ apply_filter_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 3px 5px;")
20178
20689
 
20179
20690
  # Clear filters button
20180
20691
  clear_filters_btn = QPushButton("Clear Filters")
20181
20692
  clear_filters_btn.clicked.connect(self.clear_filters)
20182
20693
  clear_filters_btn.setMaximumWidth(100)
20694
+ clear_filters_btn.setStyleSheet("padding: 3px 5px;")
20183
20695
 
20184
20696
  # Quick Filters dropdown menu
20185
20697
  quick_filter_btn = QPushButton("⚡ Quick Filters")
20186
20698
  quick_filter_btn.setMaximumWidth(130)
20187
- quick_filter_btn.setStyleSheet("background-color: #D84315; color: white; font-weight: bold;")
20699
+ quick_filter_btn.setStyleSheet("background-color: #D84315; color: white; font-weight: bold; padding: 3px 5px;")
20188
20700
  quick_filter_menu = QMenu(self)
20189
20701
  quick_filter_menu.addAction("🔍 Empty segments", lambda: self.apply_quick_filter("empty"))
20190
20702
  quick_filter_menu.addAction("❌ Not translated", lambda: self.apply_quick_filter("not_translated"))
@@ -20198,8 +20710,69 @@ class SupervertalerQt(QMainWindow):
20198
20710
  advanced_filter_btn = QPushButton("⚙️ Advanced Filters")
20199
20711
  advanced_filter_btn.clicked.connect(self.show_advanced_filters_dialog)
20200
20712
  advanced_filter_btn.setMaximumWidth(160)
20201
- advanced_filter_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;")
20202
-
20713
+ advanced_filter_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; padding: 3px 5px;")
20714
+
20715
+ # Sort dropdown button (similar to memoQ)
20716
+ sort_btn = QPushButton("⇅ Sort")
20717
+ sort_btn.setMaximumWidth(100)
20718
+ sort_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold; padding: 3px 5px;")
20719
+ sort_menu = QMenu(self)
20720
+
20721
+ # Initialize sort state if not exists
20722
+ if not hasattr(self, 'current_sort'):
20723
+ self.current_sort = None # None = document order
20724
+
20725
+ # Sort by source text
20726
+ sort_menu.addAction("📝 Source A → Z", lambda: self.apply_sort('source_asc'))
20727
+ sort_menu.addAction("📝 Source Z → A", lambda: self.apply_sort('source_desc'))
20728
+
20729
+ sort_menu.addSeparator()
20730
+
20731
+ # Sort by target text
20732
+ sort_menu.addAction("📄 Target A → Z", lambda: self.apply_sort('target_asc'))
20733
+ sort_menu.addAction("📄 Target Z → A", lambda: self.apply_sort('target_desc'))
20734
+
20735
+ sort_menu.addSeparator()
20736
+
20737
+ # Sort by text length
20738
+ sort_menu.addAction("📏 Source (longer first)", lambda: self.apply_sort('source_length_desc'))
20739
+ sort_menu.addAction("📏 Source (shorter first)", lambda: self.apply_sort('source_length_asc'))
20740
+ sort_menu.addAction("📏 Target (longer first)", lambda: self.apply_sort('target_length_desc'))
20741
+ sort_menu.addAction("📏 Target (shorter first)", lambda: self.apply_sort('target_length_asc'))
20742
+
20743
+ sort_menu.addSeparator()
20744
+
20745
+ # Sort by match rate
20746
+ sort_menu.addAction("🎯 Match Rate (higher first)", lambda: self.apply_sort('match_desc'))
20747
+ sort_menu.addAction("🎯 Match Rate (lower first)", lambda: self.apply_sort('match_asc'))
20748
+
20749
+ sort_menu.addSeparator()
20750
+
20751
+ # Sort by frequency
20752
+ sort_menu.addAction("📊 Source Frequency (higher first)", lambda: self.apply_sort('source_freq_desc'))
20753
+ sort_menu.addAction("📊 Source Frequency (lower first)", lambda: self.apply_sort('source_freq_asc'))
20754
+ sort_menu.addAction("📊 Target Frequency (higher first)", lambda: self.apply_sort('target_freq_desc'))
20755
+ sort_menu.addAction("📊 Target Frequency (lower first)", lambda: self.apply_sort('target_freq_asc'))
20756
+
20757
+ sort_menu.addSeparator()
20758
+
20759
+ # Sort by last changed
20760
+ sort_menu.addAction("🕒 Last Changed (newest first)", lambda: self.apply_sort('modified_desc'))
20761
+ sort_menu.addAction("🕒 Last Changed (oldest first)", lambda: self.apply_sort('modified_asc'))
20762
+
20763
+ sort_menu.addSeparator()
20764
+
20765
+ # Sort by row status
20766
+ sort_menu.addAction("🚦 Row Status", lambda: self.apply_sort('status'))
20767
+
20768
+ sort_menu.addSeparator()
20769
+
20770
+ # Reset to document order
20771
+ sort_menu.addAction("↩️ Document Order (default)", lambda: self.apply_sort(None))
20772
+
20773
+ sort_btn.setMenu(sort_menu)
20774
+ sort_btn.setToolTip("Sort segments by various criteria")
20775
+
20203
20776
  # File filter dropdown (for multi-file projects)
20204
20777
  self.file_filter_combo = QComboBox()
20205
20778
  self.file_filter_combo.setMinimumWidth(150)
@@ -20212,7 +20785,7 @@ class SupervertalerQt(QMainWindow):
20212
20785
  # Show Invisibles button with dropdown menu
20213
20786
  show_invisibles_btn_home = QPushButton("¶ Show Invisibles")
20214
20787
  show_invisibles_btn_home.setMaximumWidth(140)
20215
- show_invisibles_btn_home.setStyleSheet("background-color: #607D8B; color: white; font-weight: bold;")
20788
+ show_invisibles_btn_home.setStyleSheet("background-color: #607D8B; color: white; font-weight: bold; padding: 3px 5px;")
20216
20789
  show_invisibles_menu_home = QMenu(show_invisibles_btn_home)
20217
20790
 
20218
20791
  # Use the same actions (they're stored as instance variables)
@@ -20293,6 +20866,7 @@ class SupervertalerQt(QMainWindow):
20293
20866
  filter_layout.addWidget(clear_filters_btn)
20294
20867
  filter_layout.addWidget(quick_filter_btn)
20295
20868
  filter_layout.addWidget(advanced_filter_btn)
20869
+ filter_layout.addWidget(sort_btn) # Sort dropdown
20296
20870
  filter_layout.addWidget(self.file_filter_combo) # File filter for multi-file projects
20297
20871
  filter_layout.addWidget(show_invisibles_btn_home)
20298
20872
  filter_layout.addWidget(self.spellcheck_btn)
@@ -20416,30 +20990,80 @@ class SupervertalerQt(QMainWindow):
20416
20990
  tab_seg_info.setStyleSheet("font-weight: bold;")
20417
20991
  toolbar_layout.addWidget(tab_seg_info)
20418
20992
 
20419
- # Tag View toggle button
20420
- tag_view_btn = QPushButton("🏷️ Tags OFF")
20421
- tag_view_btn.setCheckable(True)
20422
- tag_view_btn.setChecked(False) # Default: WYSIWYG mode (tags hidden)
20423
- tag_view_btn.setStyleSheet("""
20993
+ # View mode segmented control (WYSIWYG / Tags)
20994
+ from PyQt6.QtWidgets import QButtonGroup
20995
+
20996
+ view_mode_container = QWidget()
20997
+ view_mode_layout = QHBoxLayout(view_mode_container)
20998
+ view_mode_layout.setContentsMargins(0, 0, 0, 0)
20999
+ view_mode_layout.setSpacing(0)
21000
+
21001
+ # Create button group to ensure only one is checked
21002
+ view_mode_group = QButtonGroup(self)
21003
+ view_mode_group.setExclusive(True)
21004
+
21005
+ # WYSIWYG button (left)
21006
+ wysiwyg_btn = QPushButton("WYSIWYG")
21007
+ wysiwyg_btn.setCheckable(True)
21008
+ wysiwyg_btn.setChecked(False)
21009
+ wysiwyg_btn.setToolTip("WYSIWYG View (Ctrl+Alt+T)\nShows formatted text without raw tags")
21010
+ wysiwyg_btn.setStyleSheet("""
20424
21011
  QPushButton {
20425
21012
  background-color: #757575;
20426
21013
  color: white;
20427
21014
  font-weight: bold;
20428
- padding: 4px 8px;
20429
- border-radius: 3px;
21015
+ padding: 4px 12px;
21016
+ border: none;
21017
+ border-top-left-radius: 3px;
21018
+ border-bottom-left-radius: 3px;
20430
21019
  }
20431
21020
  QPushButton:checked {
20432
21021
  background-color: #9C27B0;
20433
21022
  }
21023
+ QPushButton:hover:!checked {
21024
+ background-color: #858585;
21025
+ }
20434
21026
  """)
20435
- tag_view_btn.setToolTip("Toggle Tag View (Ctrl+Alt+T)\n\nTags OFF: Shows formatted text (WYSIWYG)\nTags ON: Shows raw tags like <b>bold</b>")
20436
- tag_view_btn.clicked.connect(lambda checked: self.toggle_tag_view(checked, tag_view_btn))
20437
- toolbar_layout.addWidget(tag_view_btn)
20438
- self.tag_view_btn = tag_view_btn # Store reference
21027
+ wysiwyg_btn.clicked.connect(lambda: self.toggle_tag_view(False, None))
21028
+ view_mode_group.addButton(wysiwyg_btn, 0)
21029
+ view_mode_layout.addWidget(wysiwyg_btn)
21030
+
21031
+ # Tags button (right)
21032
+ tags_btn = QPushButton("Tags")
21033
+ tags_btn.setCheckable(True)
21034
+ tags_btn.setChecked(True) # Default: Tags mode
21035
+ tags_btn.setToolTip("Tag View (Ctrl+Alt+T)\nShows raw tags like <b>bold</b>")
21036
+ tags_btn.setStyleSheet("""
21037
+ QPushButton {
21038
+ background-color: #757575;
21039
+ color: white;
21040
+ font-weight: bold;
21041
+ padding: 4px 12px;
21042
+ border: none;
21043
+ border-top-right-radius: 3px;
21044
+ border-bottom-right-radius: 3px;
21045
+ }
21046
+ QPushButton:checked {
21047
+ background-color: #9C27B0;
21048
+ }
21049
+ QPushButton:hover:!checked {
21050
+ background-color: #858585;
21051
+ }
21052
+ """)
21053
+ tags_btn.clicked.connect(lambda: self.toggle_tag_view(True, None))
21054
+ view_mode_group.addButton(tags_btn, 1)
21055
+ view_mode_layout.addWidget(tags_btn)
21056
+
21057
+ toolbar_layout.addWidget(view_mode_container)
21058
+
21059
+ # Store references for keyboard shortcut and programmatic access
21060
+ self.wysiwyg_btn = wysiwyg_btn
21061
+ self.tags_btn = tags_btn
21062
+ self.view_mode_group = view_mode_group
20439
21063
 
20440
21064
  # Initialize tag view state
20441
21065
  if not hasattr(self, 'show_tags'):
20442
- self.show_tags = False
21066
+ self.show_tags = True # Default: show tags
20443
21067
 
20444
21068
  # Status selector
20445
21069
  from modules.statuses import get_status, STATUSES
@@ -20457,12 +21081,12 @@ class SupervertalerQt(QMainWindow):
20457
21081
 
20458
21082
  preview_prompt_btn = QPushButton("🧪 Preview Prompts")
20459
21083
  preview_prompt_btn.setToolTip("Preview the complete assembled prompt\n(System Prompt + Custom Prompts + current segment)")
20460
- preview_prompt_btn.setStyleSheet("background-color: #9C27B0; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
21084
+ preview_prompt_btn.setStyleSheet("background-color: #9C27B0; color: white; font-weight: bold; padding: 3px 5px; border: none; outline: none;")
20461
21085
  preview_prompt_btn.clicked.connect(self._preview_combined_prompt_from_grid)
20462
21086
  toolbar_layout.addWidget(preview_prompt_btn)
20463
21087
 
20464
21088
  dictate_btn = QPushButton("🎤 Dictation")
20465
- dictate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
21089
+ dictate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 3px 5px; border: none; outline: none;")
20466
21090
  dictate_btn.clicked.connect(self.start_voice_dictation)
20467
21091
  dictate_btn.setToolTip("Start/stop voice dictation (F9)")
20468
21092
  toolbar_layout.addWidget(dictate_btn)
@@ -20476,7 +21100,7 @@ class SupervertalerQt(QMainWindow):
20476
21100
  background-color: #757575;
20477
21101
  color: white;
20478
21102
  font-weight: bold;
20479
- padding: 4px 8px;
21103
+ padding: 3px 5px;
20480
21104
  border-radius: 3px;
20481
21105
  }
20482
21106
  QPushButton:checked {
@@ -20491,7 +21115,7 @@ class SupervertalerQt(QMainWindow):
20491
21115
  toolbar_layout.addStretch()
20492
21116
 
20493
21117
  save_next_btn = QPushButton("✓ Confirm && Next")
20494
- save_next_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
21118
+ save_next_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; padding: 3px 5px; border: none; outline: none;")
20495
21119
  save_next_btn.clicked.connect(self.confirm_selected_or_next)
20496
21120
  save_next_btn.setToolTip("Confirm current segment and go to next unconfirmed (Ctrl+Enter)")
20497
21121
  toolbar_layout.addWidget(save_next_btn)
@@ -21100,9 +21724,21 @@ class SupervertalerQt(QMainWindow):
21100
21724
  # Configure columns
21101
21725
  self.table.setColumnCount(5)
21102
21726
  self.table.setHorizontalHeaderLabels(["#", "Type", "Source", "Target", "Status"])
21103
-
21104
- # Column widths - Source and Target columns stretch to fill space, others are interactive
21727
+
21728
+ # Explicitly set header font to normal weight (not bold)
21105
21729
  header = self.table.horizontalHeader()
21730
+ header_font = QFont(self.default_font_family, self.default_font_size, QFont.Weight.Normal)
21731
+ header.setFont(header_font)
21732
+
21733
+ # Also set font on individual header items through the model (extra insurance)
21734
+ model = self.table.model()
21735
+ if model:
21736
+ for col in range(5):
21737
+ item = model.headerData(col, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole)
21738
+ model.setHeaderData(col, Qt.Orientation.Horizontal, item, Qt.ItemDataRole.DisplayRole)
21739
+ model.setHeaderData(col, Qt.Orientation.Horizontal, header_font, Qt.ItemDataRole.FontRole)
21740
+
21741
+ # Column widths - Source and Target columns stretch to fill space, others are interactive
21106
21742
  header.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive) # ID
21107
21743
  header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive) # Type
21108
21744
  header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Source - stretch to fill space
@@ -21116,7 +21752,7 @@ class SupervertalerQt(QMainWindow):
21116
21752
  self.table.setColumnWidth(1, 40) # Type - narrower
21117
21753
  self.table.setColumnWidth(2, 400) # Source
21118
21754
  self.table.setColumnWidth(3, 400) # Target
21119
- self.table.setColumnWidth(4, 60) # Status - compact
21755
+ self.table.setColumnWidth(4, 50) # Status - compact width
21120
21756
 
21121
21757
  # Enable word wrap in cells (both display and edit mode)
21122
21758
  self.table.setWordWrap(True)
@@ -21146,6 +21782,51 @@ class SupervertalerQt(QMainWindow):
21146
21782
  QTableWidget::item:last-child {
21147
21783
  border-right: none;
21148
21784
  }
21785
+
21786
+ /* Narrower scrollbar with visible arrow buttons */
21787
+ QScrollBar:vertical {
21788
+ border: none;
21789
+ background: #F0F0F0;
21790
+ width: 12px;
21791
+ margin: 12px 0 12px 0;
21792
+ }
21793
+ QScrollBar::handle:vertical {
21794
+ background: #C0C0C0;
21795
+ min-height: 20px;
21796
+ border-radius: 2px;
21797
+ }
21798
+ QScrollBar::handle:vertical:hover {
21799
+ background: #A0A0A0;
21800
+ }
21801
+ QScrollBar::add-line:vertical {
21802
+ height: 12px;
21803
+ background: #E0E0E0;
21804
+ subcontrol-position: bottom;
21805
+ subcontrol-origin: margin;
21806
+ }
21807
+ QScrollBar::add-line:vertical:hover {
21808
+ background: #2196F3;
21809
+ }
21810
+ QScrollBar::sub-line:vertical {
21811
+ height: 12px;
21812
+ background: #E0E0E0;
21813
+ subcontrol-position: top;
21814
+ subcontrol-origin: margin;
21815
+ }
21816
+ QScrollBar::sub-line:vertical:hover {
21817
+ background: #2196F3;
21818
+ }
21819
+ /* Arrow images */
21820
+ QScrollBar::up-arrow:vertical {
21821
+ image: url(assets/scrollbar_up.png);
21822
+ width: 8px;
21823
+ height: 8px;
21824
+ }
21825
+ QScrollBar::down-arrow:vertical {
21826
+ image: url(assets/scrollbar_down.png);
21827
+ width: 8px;
21828
+ height: 8px;
21829
+ }
21149
21830
  """)
21150
21831
 
21151
21832
  # Simplified editing: Double-click only (no F2 key) - companion tool philosophy
@@ -21168,8 +21849,8 @@ class SupervertalerQt(QMainWindow):
21168
21849
  # Debug: Confirm signal connections
21169
21850
  self.log("🔌 Table signals connected: currentCellChanged, itemClicked, cellDoubleClicked, itemSelectionChanged")
21170
21851
 
21171
- # Add precision scroll buttons (memoQ-style)
21172
- self.add_precision_scroll_buttons()
21852
+ # Precision scroll buttons removed (user preference)
21853
+ # self.add_precision_scroll_buttons()
21173
21854
 
21174
21855
  def add_precision_scroll_buttons(self):
21175
21856
  """Add precision scroll buttons at top/bottom of scrollbar (memoQ-style)"""
@@ -21395,10 +22076,10 @@ class SupervertalerQt(QMainWindow):
21395
22076
  editor_widget.dictate_btn = dictate_btn
21396
22077
 
21397
22078
  save_btn = QPushButton("💾 Save")
21398
- save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
22079
+ save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 3px 5px;")
21399
22080
  save_btn.clicked.connect(self.save_tab_segment)
21400
22081
  save_next_btn = QPushButton("✓ Confirm && Next (Ctrl+Enter)")
21401
- save_next_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;")
22082
+ save_next_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; padding: 3px 5px;")
21402
22083
  save_next_btn.clicked.connect(self.confirm_selected_or_next)
21403
22084
 
21404
22085
  button_layout.addWidget(copy_btn)
@@ -21558,36 +22239,35 @@ class SupervertalerQt(QMainWindow):
21558
22239
 
21559
22240
  # Source language
21560
22241
  source_lang_combo = QComboBox()
21561
- common_langs = [
21562
- ("English", "en"),
21563
- ("Dutch", "nl"),
21564
- ("German", "de"),
21565
- ("French", "fr"),
21566
- ("Spanish", "es"),
21567
- ("Italian", "it"),
21568
- ("Portuguese", "pt"),
21569
- ("Russian", "ru"),
21570
- ("Chinese", "zh"),
21571
- ("Japanese", "ja"),
22242
+ # Full language list matching Settings → Language Pair (with ISO 639-1 codes)
22243
+ available_langs = [
22244
+ ("Afrikaans", "af"), ("Albanian", "sq"), ("Arabic", "ar"), ("Armenian", "hy"),
22245
+ ("Basque", "eu"), ("Bengali", "bn"), ("Bulgarian", "bg"), ("Catalan", "ca"),
22246
+ ("Chinese (Simplified)", "zh-CN"), ("Chinese (Traditional)", "zh-TW"),
22247
+ ("Croatian", "hr"), ("Czech", "cs"), ("Danish", "da"), ("Dutch", "nl"),
22248
+ ("English", "en"), ("Estonian", "et"), ("Finnish", "fi"), ("French", "fr"),
22249
+ ("Galician", "gl"), ("Georgian", "ka"), ("German", "de"), ("Greek", "el"),
22250
+ ("Hebrew", "he"), ("Hindi", "hi"), ("Hungarian", "hu"), ("Icelandic", "is"),
22251
+ ("Indonesian", "id"), ("Irish", "ga"), ("Italian", "it"), ("Japanese", "ja"),
22252
+ ("Korean", "ko"), ("Latvian", "lv"), ("Lithuanian", "lt"), ("Macedonian", "mk"),
22253
+ ("Malay", "ms"), ("Norwegian", "no"), ("Persian", "fa"), ("Polish", "pl"),
22254
+ ("Portuguese", "pt"), ("Romanian", "ro"), ("Russian", "ru"), ("Serbian", "sr"),
22255
+ ("Slovak", "sk"), ("Slovenian", "sl"), ("Spanish", "es"), ("Swahili", "sw"),
22256
+ ("Swedish", "sv"), ("Thai", "th"), ("Turkish", "tr"), ("Ukrainian", "uk"),
22257
+ ("Urdu", "ur"), ("Vietnamese", "vi"), ("Welsh", "cy"),
21572
22258
  ]
21573
- for lang_name, lang_code in common_langs:
22259
+ for lang_name, lang_code in available_langs:
21574
22260
  source_lang_combo.addItem(lang_name, lang_code)
21575
22261
  settings_layout.addRow("Source Language:", source_lang_combo)
21576
-
22262
+
21577
22263
  # Target language
21578
22264
  target_lang_combo = QComboBox()
21579
- for lang_name, lang_code in common_langs:
22265
+ for lang_name, lang_code in available_langs:
21580
22266
  target_lang_combo.addItem(lang_name, lang_code)
21581
-
21582
- # Set defaults based on global language settings (if in common_langs)
21583
- try:
21584
- for lang_name, lang_code in common_langs:
21585
- if lang_name == self.source_language:
21586
- source_lang_combo.setCurrentText(lang_name)
21587
- if lang_name == self.target_language:
21588
- target_lang_combo.setCurrentText(lang_name)
21589
- except:
21590
- target_lang_combo.setCurrentIndex(1) # Fallback to Dutch
22267
+
22268
+ # Set defaults based on global language settings
22269
+ source_lang_combo.setCurrentText(self.source_language)
22270
+ target_lang_combo.setCurrentText(self.target_language)
21591
22271
 
21592
22272
  settings_layout.addRow("Target Language:", target_lang_combo)
21593
22273
 
@@ -21709,7 +22389,11 @@ class SupervertalerQt(QMainWindow):
21709
22389
  target_lang=target_lang,
21710
22390
  segments=[]
21711
22391
  )
21712
-
22392
+
22393
+ # Sync global language settings with new project languages
22394
+ self.source_language = source_lang
22395
+ self.target_language = target_lang
22396
+
21713
22397
  # Process source text if provided
21714
22398
  source_text = text_input.toPlainText().strip()
21715
22399
  if source_text:
@@ -21739,6 +22423,10 @@ class SupervertalerQt(QMainWindow):
21739
22423
  # Update UI
21740
22424
  self.project_file_path = None
21741
22425
  self.project_modified = True # Mark as modified since it hasn't been saved
22426
+
22427
+ # Store original segment order for "Document Order" sort reset
22428
+ self._original_segment_order = self.current_project.segments.copy()
22429
+
21742
22430
  self.update_window_title()
21743
22431
  self.load_segments_to_grid()
21744
22432
  self.initialize_tm_database() # Initialize TM for this project
@@ -21767,16 +22455,16 @@ class SupervertalerQt(QMainWindow):
21767
22455
  if hasattr(self.lookup_tab, '_ahk') and self.lookup_tab._ahk:
21768
22456
  try:
21769
22457
  self.lookup_tab._ahk.stop_hotkeys()
21770
- print("[Superlookup] ahk library hotkeys stopped")
22458
+ print("[SuperLookup] ahk library hotkeys stopped")
21771
22459
  except Exception as e:
21772
- print(f"[Superlookup] Error stopping ahk library: {e}")
22460
+ print(f"[SuperLookup] Error stopping ahk library: {e}")
21773
22461
 
21774
22462
  # Terminate external AutoHotkey process if running (fallback method)
21775
22463
  if hasattr(self, 'lookup_tab') and hasattr(self.lookup_tab, 'ahk_process') and self.lookup_tab.ahk_process:
21776
22464
  try:
21777
22465
  self.lookup_tab.ahk_process.terminate()
21778
22466
  self.lookup_tab.ahk_process.wait(timeout=2)
21779
- print("[Superlookup] AHK process terminated")
22467
+ print("[SuperLookup] AHK process terminated")
21780
22468
  except:
21781
22469
  # Force kill if terminate doesn't work
21782
22470
  try:
@@ -21784,7 +22472,7 @@ class SupervertalerQt(QMainWindow):
21784
22472
  except:
21785
22473
  pass
21786
22474
  except Exception as e:
21787
- print(f"[Superlookup] Error terminating AHK: {e}")
22475
+ print(f"[SuperLookup] Error terminating AHK: {e}")
21788
22476
 
21789
22477
  # Accept the close event
21790
22478
  event.accept()
@@ -21819,7 +22507,19 @@ class SupervertalerQt(QMainWindow):
21819
22507
  self.current_project = Project.from_dict(data)
21820
22508
  self.project_file_path = file_path
21821
22509
  self.project_modified = False
21822
-
22510
+
22511
+ # Store original segment order for "Document Order" sort reset
22512
+ self._original_segment_order = self.current_project.segments.copy()
22513
+
22514
+ # Always reset sort state when loading - project should open in document order
22515
+ self.current_sort = None
22516
+
22517
+ # Sync global language settings with project languages
22518
+ if self.current_project.source_lang:
22519
+ self.source_language = self.current_project.source_lang
22520
+ if self.current_project.target_lang:
22521
+ self.target_language = self.current_project.target_lang
22522
+
21823
22523
  # Restore prompt settings if they exist (unified library)
21824
22524
  if hasattr(self.current_project, 'prompt_settings') and self.current_project.prompt_settings:
21825
22525
  prompt_settings = self.current_project.prompt_settings
@@ -22174,7 +22874,154 @@ class SupervertalerQt(QMainWindow):
22174
22874
  except Exception as e:
22175
22875
  QMessageBox.critical(self, "Error", f"Failed to load project:\n{str(e)}")
22176
22876
  self.log(f"✗ Error loading project: {e}")
22177
-
22877
+
22878
+ def _build_termbase_index(self):
22879
+ """
22880
+ Build in-memory index of ALL terms from activated termbases (v1.9.182).
22881
+
22882
+ This is called ONCE on project load and replaces thousands of per-word
22883
+ database queries with a single bulk load + fast in-memory lookups.
22884
+
22885
+ Performance: Reduces 349-segment termbase search from 365 seconds to <1 second.
22886
+ """
22887
+ import re
22888
+ import time
22889
+ start_time = time.time()
22890
+
22891
+ if not self.current_project or not hasattr(self, 'db_manager') or not self.db_manager:
22892
+ return
22893
+
22894
+ project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
22895
+
22896
+ # Query ALL terms from activated termbases in ONE query
22897
+ # This replaces ~17,500 individual queries (349 segments × 50 words each)
22898
+ query = """
22899
+ SELECT
22900
+ t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
22901
+ t.domain, t.notes, t.project, t.client, t.forbidden,
22902
+ tb.is_project_termbase, tb.name as termbase_name,
22903
+ COALESCE(ta.priority, tb.ranking) as ranking
22904
+ FROM termbase_terms t
22905
+ LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
22906
+ LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id
22907
+ AND ta.project_id = ? AND ta.is_active = 1
22908
+ WHERE (ta.is_active = 1 OR tb.is_project_termbase = 1)
22909
+ """
22910
+
22911
+ new_index = []
22912
+ try:
22913
+ self.db_manager.cursor.execute(query, [project_id or 0])
22914
+ rows = self.db_manager.cursor.fetchall()
22915
+
22916
+ for row in rows:
22917
+ source_term = row[1] # source_term
22918
+ if not source_term:
22919
+ continue
22920
+
22921
+ source_term_lower = source_term.lower().strip()
22922
+ if len(source_term_lower) < 2:
22923
+ continue
22924
+
22925
+ # Pre-compile regex pattern for word-boundary matching
22926
+ # This avoids recompiling the same pattern thousands of times
22927
+ try:
22928
+ # Handle terms with punctuation differently
22929
+ if any(c in source_term_lower for c in '.%,/-'):
22930
+ pattern = re.compile(r'(?<!\w)' + re.escape(source_term_lower) + r'(?!\w)')
22931
+ else:
22932
+ pattern = re.compile(r'\b' + re.escape(source_term_lower) + r'\b')
22933
+ except re.error:
22934
+ # If regex fails, use simple substring matching
22935
+ pattern = None
22936
+
22937
+ new_index.append({
22938
+ 'term_id': row[0],
22939
+ 'source_term': source_term,
22940
+ 'source_term_lower': source_term_lower,
22941
+ 'target_term': row[2],
22942
+ 'termbase_id': row[3],
22943
+ 'priority': row[4],
22944
+ 'domain': row[5],
22945
+ 'notes': row[6],
22946
+ 'project': row[7],
22947
+ 'client': row[8],
22948
+ 'forbidden': row[9],
22949
+ 'is_project_termbase': row[10],
22950
+ 'termbase_name': row[11],
22951
+ 'ranking': row[12],
22952
+ 'pattern': pattern, # Pre-compiled regex
22953
+ })
22954
+
22955
+ # Sort by term length (longest first) for better phrase matching
22956
+ new_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
22957
+
22958
+ # Thread-safe update of the index
22959
+ with self.termbase_index_lock:
22960
+ self.termbase_index = new_index
22961
+
22962
+ elapsed = time.time() - start_time
22963
+ self.log(f"✅ Built termbase index: {len(new_index)} terms in {elapsed:.2f}s")
22964
+
22965
+ except Exception as e:
22966
+ self.log(f"❌ Failed to build termbase index: {e}")
22967
+ import traceback
22968
+ self.log(traceback.format_exc())
22969
+
22970
+ def _search_termbase_in_memory(self, source_text: str) -> dict:
22971
+ """
22972
+ Search termbase using in-memory index (v1.9.182).
22973
+
22974
+ This replaces _search_termbases_thread_safe() for batch operations.
22975
+ Instead of N database queries (one per word), we do:
22976
+ - 1 pass through the index (typically ~1000 terms)
22977
+ - Fast string 'in' check + pre-compiled regex validation
22978
+
22979
+ Performance: <1ms per segment vs 1+ second per segment.
22980
+ """
22981
+ if not source_text:
22982
+ return {}
22983
+
22984
+ with self.termbase_index_lock:
22985
+ if not self.termbase_index:
22986
+ return {}
22987
+ index = self.termbase_index # Local reference for thread safety
22988
+
22989
+ source_lower = source_text.lower()
22990
+ matches = {}
22991
+
22992
+ for term in index:
22993
+ term_lower = term['source_term_lower']
22994
+
22995
+ # Quick substring check first (very fast, implemented in C)
22996
+ if term_lower not in source_lower:
22997
+ continue
22998
+
22999
+ # Word boundary validation using pre-compiled pattern
23000
+ pattern = term.get('pattern')
23001
+ if pattern:
23002
+ if not pattern.search(source_lower):
23003
+ continue
23004
+
23005
+ # Term matches! Add to results
23006
+ term_id = term['term_id']
23007
+ matches[term_id] = {
23008
+ 'source': term['source_term'],
23009
+ 'translation': term['target_term'],
23010
+ 'term_id': term_id,
23011
+ 'termbase_id': term['termbase_id'],
23012
+ 'termbase_name': term['termbase_name'],
23013
+ 'priority': term['priority'],
23014
+ 'ranking': term['ranking'],
23015
+ 'is_project_termbase': term['is_project_termbase'],
23016
+ 'forbidden': term['forbidden'],
23017
+ 'domain': term['domain'],
23018
+ 'notes': term['notes'],
23019
+ 'project': term['project'],
23020
+ 'client': term['client'],
23021
+ }
23022
+
23023
+ return matches
23024
+
22178
23025
  def _start_termbase_batch_worker(self):
22179
23026
  """
22180
23027
  Start background thread to batch-process termbase matches for all segments.
@@ -22182,21 +23029,25 @@ class SupervertalerQt(QMainWindow):
22182
23029
  """
22183
23030
  if not self.current_project or len(self.current_project.segments) == 0:
22184
23031
  return
22185
-
23032
+
23033
+ # Build in-memory termbase index FIRST (v1.9.182)
23034
+ # This is the key optimization: load all terms once, then do fast in-memory lookups
23035
+ self._build_termbase_index()
23036
+
22186
23037
  # 🧪 EXPERIMENTAL: Skip batch worker if cache kill switch is enabled
22187
23038
  if getattr(self, 'disable_all_caches', False):
22188
23039
  self.log("🧪 Termbase batch worker SKIPPED (caches disabled)")
22189
23040
  return
22190
-
23041
+
22191
23042
  # Stop any existing worker thread
22192
23043
  self.termbase_batch_stop_event.set()
22193
23044
  if self.termbase_batch_worker_thread and self.termbase_batch_worker_thread.is_alive():
22194
23045
  self.log("⏹️ Stopping existing termbase batch worker...")
22195
23046
  self.termbase_batch_worker_thread.join(timeout=2)
22196
-
23047
+
22197
23048
  # Reset stop event for new worker
22198
23049
  self.termbase_batch_stop_event.clear()
22199
-
23050
+
22200
23051
  # Start new background worker thread
22201
23052
  segment_count = len(self.current_project.segments)
22202
23053
  self.log(f"🔄 Starting background termbase batch processor for {segment_count} segments...")
@@ -22212,96 +23063,60 @@ class SupervertalerQt(QMainWindow):
22212
23063
  """
22213
23064
  Background worker thread: process all segments and populate termbase cache.
22214
23065
  Runs in separate thread to not block UI.
22215
-
22216
- IMPORTANT: Creates its own database connection to avoid SQLite threading errors.
23066
+
23067
+ v1.9.182: Now uses in-memory termbase index for 1000x faster lookups.
23068
+ Old approach: 365 seconds for 349 segments (1 second/segment)
23069
+ New approach: <1 second for 349 segments (<3ms/segment)
22217
23070
  """
22218
23071
  if not segments:
22219
23072
  return
22220
-
22221
- # Create a separate database connection for this thread
22222
- # SQLite connections are thread-local and cannot be shared across threads
22223
- import sqlite3
22224
- try:
22225
- thread_db_connection = sqlite3.connect(self.db_manager.db_path)
22226
- thread_db_connection.row_factory = sqlite3.Row
22227
- thread_db_cursor = thread_db_connection.cursor()
22228
- except Exception as e:
22229
- self.log(f"❌ Failed to create database connection in batch worker: {e}")
22230
- return
22231
-
23073
+
22232
23074
  try:
22233
23075
  processed = 0
22234
23076
  cached = 0
23077
+ with_matches = 0
22235
23078
  start_time = time.time()
22236
-
23079
+
22237
23080
  for segment in segments:
22238
23081
  # Check if stop event was signaled (user closed project or started new one)
22239
23082
  if self.termbase_batch_stop_event.is_set():
22240
23083
  self.log(f"⏹️ Termbase batch worker stopped by user (processed {processed} segments)")
22241
23084
  break
22242
-
23085
+
22243
23086
  segment_id = segment.id
22244
-
23087
+
22245
23088
  # Skip if already in cache (thread-safe check)
22246
23089
  with self.termbase_cache_lock:
22247
23090
  if segment_id in self.termbase_cache:
22248
23091
  cached += 1
22249
23092
  continue
22250
-
22251
- # Search termbase for this segment using thread-local database connection
23093
+
23094
+ # v1.9.182: Use in-memory index for instant lookup (no database queries!)
22252
23095
  try:
22253
- # Manually query the database using thread-local connection
22254
- # Pass project_id to filter by activated termbases only
22255
- current_project_id = self.current_project.id if (self.current_project and hasattr(self.current_project, 'id')) else None
22256
- matches = self._search_termbases_thread_safe(
22257
- segment.source,
22258
- thread_db_cursor,
22259
- source_lang=self.current_project.source_lang if self.current_project else None,
22260
- target_lang=self.current_project.target_lang if self.current_project else None,
22261
- project_id=current_project_id
22262
- )
22263
-
23096
+ matches = self._search_termbase_in_memory(segment.source)
23097
+
23098
+ # Store in cache (thread-safe) - even empty results to avoid re-lookup
23099
+ with self.termbase_cache_lock:
23100
+ self.termbase_cache[segment_id] = matches
23101
+
23102
+ processed += 1
22264
23103
  if matches:
22265
- # Store in cache (thread-safe)
22266
- with self.termbase_cache_lock:
22267
- self.termbase_cache[segment_id] = matches
22268
-
22269
- processed += 1
22270
-
22271
- # Log progress every 100 segments
22272
- if processed % 100 == 0:
22273
- elapsed = time.time() - start_time
22274
- rate = processed / elapsed if elapsed > 0 else 0
22275
- remaining = len(segments) - processed
22276
- eta_seconds = remaining / rate if rate > 0 else 0
22277
- self.log(f"📊 Batch progress: {processed}/{len(segments)} cached " +
22278
- f"({rate:.1f} seg/sec, ETA: {int(eta_seconds)}s)")
22279
-
23104
+ with_matches += 1
23105
+
22280
23106
  except Exception as e:
22281
23107
  self.log(f"❌ Error processing segment {segment_id} in batch worker: {e}")
22282
23108
  continue
22283
-
22284
- # Small delay to prevent CPU saturation (let UI thread work)
22285
- time.sleep(0.001) # 1ms delay between segments
22286
-
23109
+
22287
23110
  elapsed = time.time() - start_time
22288
23111
  total_cached = len(self.termbase_cache)
22289
- self.log(f"✅ Termbase batch worker complete: {processed} new + {cached} existing = " +
22290
- f"{total_cached} total cached in {elapsed:.1f}s")
22291
-
23112
+ rate = processed / elapsed if elapsed > 0 else 0
23113
+ self.log(f"✅ Termbase batch worker complete: {processed} segments in {elapsed:.2f}s " +
23114
+ f"({rate:.0f} seg/sec, {with_matches} with matches)")
23115
+
22292
23116
  except Exception as e:
22293
23117
  self.log(f"❌ Termbase batch worker error: {e}")
22294
23118
  import traceback
22295
23119
  self.log(traceback.format_exc())
22296
-
22297
- finally:
22298
- # Close thread-local database connection
22299
- try:
22300
- thread_db_cursor.close()
22301
- thread_db_connection.close()
22302
- self.log("✓ Closed thread-local database connection in batch worker")
22303
- except:
22304
- pass
22305
23120
 
22306
23121
  def _search_termbases_thread_safe(self, source_text: str, cursor, source_lang: str = None, target_lang: str = None, project_id: int = None) -> Dict[str, str]:
22307
23122
  """
@@ -22501,11 +23316,8 @@ class SupervertalerQt(QMainWindow):
22501
23316
  Also triggers PROACTIVE HIGHLIGHTING for upcoming segments with glossary matches.
22502
23317
  """
22503
23318
  import json
22504
-
22505
- print(f"[PROACTIVE DEBUG] _trigger_idle_prefetch called for row {current_row}")
22506
-
23319
+
22507
23320
  if not self.current_project or current_row < 0:
22508
- print(f"[PROACTIVE DEBUG] Early exit: no project or invalid row")
22509
23321
  return
22510
23322
 
22511
23323
  try:
@@ -22514,9 +23326,7 @@ class SupervertalerQt(QMainWindow):
22514
23326
  already_cached_ids = []
22515
23327
  start_idx = current_row + 1
22516
23328
  end_idx = min(start_idx + 5, len(self.current_project.segments))
22517
-
22518
- print(f"[PROACTIVE DEBUG] Checking segments {start_idx} to {end_idx}")
22519
-
23329
+
22520
23330
  for seg in self.current_project.segments[start_idx:end_idx]:
22521
23331
  # Check if already cached
22522
23332
  with self.translation_matches_cache_lock:
@@ -22524,23 +23334,19 @@ class SupervertalerQt(QMainWindow):
22524
23334
  next_segment_ids.append(seg.id)
22525
23335
  else:
22526
23336
  already_cached_ids.append(seg.id)
22527
-
22528
- print(f"[PROACTIVE DEBUG] Already cached IDs: {already_cached_ids}, Need prefetch: {next_segment_ids}")
22529
-
23337
+
22530
23338
  # For already-cached segments, trigger proactive highlighting immediately
22531
23339
  # This handles the case where segments were cached earlier but not highlighted
22532
23340
  for seg_id in already_cached_ids:
22533
23341
  try:
22534
23342
  with self.termbase_cache_lock:
22535
23343
  termbase_raw = self.termbase_cache.get(seg_id, {})
22536
- print(f"[PROACTIVE DEBUG] Segment {seg_id} termbase cache: {len(termbase_raw) if termbase_raw else 0} matches")
22537
23344
  if termbase_raw:
22538
23345
  termbase_json = json.dumps(termbase_raw)
22539
23346
  # Apply highlighting on main thread (we're already on main thread here)
22540
- print(f"[PROACTIVE DEBUG] Calling _apply_proactive_highlighting for seg {seg_id}")
22541
23347
  self._apply_proactive_highlighting(seg_id, termbase_json)
22542
- except Exception as e:
22543
- print(f"[PROACTIVE DEBUG] Error for seg {seg_id}: {e}")
23348
+ except Exception:
23349
+ pass # Silent failure for proactive highlighting
22544
23350
 
22545
23351
  if next_segment_ids:
22546
23352
  # Start prefetch in background (silent, no logging)
@@ -22622,43 +23428,35 @@ class SupervertalerQt(QMainWindow):
22622
23428
 
22623
23429
  # Fetch TM/termbase matches (pass cursor for thread-safe termbase lookups)
22624
23430
  matches = self._fetch_all_matches_for_segment(segment, thread_db_cursor)
22625
-
22626
- # Only cache if we got at least one match (don't cache empty results)
22627
- # This prevents "empty cache hits" when TM database is still empty
23431
+
23432
+ # Count matches for logging and proactive highlighting
22628
23433
  tm_count = len(matches.get("TM", []))
22629
23434
  tb_count = len(matches.get("Termbases", []))
22630
23435
  mt_count = len(matches.get("MT", []))
22631
23436
  llm_count = len(matches.get("LLM", []))
22632
23437
  total_matches = tm_count + tb_count + mt_count + llm_count
22633
23438
 
22634
- print(f"[PREFETCH DEBUG] Segment {segment_id}: TM={tm_count}, TB={tb_count}, MT={mt_count}, LLM={llm_count}")
22635
-
23439
+ # Only cache results if we found something
23440
+ # Don't cache empty results - let main thread do fresh lookup
22636
23441
  if total_matches > 0:
22637
- # Store in cache only if we have results
22638
23442
  with self.translation_matches_cache_lock:
22639
23443
  self.translation_matches_cache[segment_id] = matches
22640
-
22641
- # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
22642
- # This makes upcoming segments show their glossary matches immediately
22643
- if tb_count > 0:
22644
- try:
22645
- # Extract raw termbase matches from cache for highlighting
22646
- with self.termbase_cache_lock:
22647
- termbase_raw = self.termbase_cache.get(segment_id, {})
22648
-
22649
- print(f"[PREFETCH DEBUG] Segment {segment_id}: termbase_raw has {len(termbase_raw) if termbase_raw else 0} entries")
22650
-
22651
- if termbase_raw:
22652
- # Convert to JSON for thread-safe signal transfer
22653
- termbase_json = json.dumps(termbase_raw)
22654
- # Emit signal - will be handled on main thread
22655
- print(f"[PREFETCH DEBUG] Emitting proactive highlight signal for segment {segment_id}")
22656
- self._proactive_highlight_signal.emit(segment_id, termbase_json)
22657
- else:
22658
- print(f"[PREFETCH DEBUG] WARNING: tb_count={tb_count} but termbase_raw is empty!")
22659
- except Exception as e:
22660
- print(f"[PREFETCH DEBUG] ERROR emitting signal: {e}")
22661
- # else: Don't cache empty results - let it fall through to slow lookup next time
23444
+
23445
+ # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
23446
+ # This makes upcoming segments show their glossary matches immediately
23447
+ if tb_count > 0:
23448
+ try:
23449
+ # Extract raw termbase matches from cache for highlighting
23450
+ with self.termbase_cache_lock:
23451
+ termbase_raw = self.termbase_cache.get(segment_id, {})
23452
+
23453
+ if termbase_raw:
23454
+ # Convert to JSON for thread-safe signal transfer
23455
+ termbase_json = json.dumps(termbase_raw)
23456
+ # Emit signal - will be handled on main thread
23457
+ self._proactive_highlight_signal.emit(segment_id, termbase_json)
23458
+ except Exception:
23459
+ pass # Silent fail for proactive highlighting
22662
23460
 
22663
23461
  except Exception as e:
22664
23462
  self.log(f"Error in prefetch worker: {e}")
@@ -22708,31 +23506,9 @@ class SupervertalerQt(QMainWindow):
22708
23506
  source_lang_code = self._convert_language_to_code(source_lang)
22709
23507
  target_lang_code = self._convert_language_to_code(target_lang)
22710
23508
 
22711
- # 1. TM matches (if enabled) - thread-safe check
22712
- enable_tm = getattr(self, 'enable_tm_matching', True) # Default to True if not set
22713
- if enable_tm and hasattr(self, 'db_manager') and self.db_manager:
22714
- try:
22715
- tm_results = self.db_manager.search_translation_memory(
22716
- segment.source,
22717
- source_lang,
22718
- target_lang,
22719
- limit=5
22720
- )
22721
-
22722
- if tm_results: # Only add if we got results
22723
- for tm_match in tm_results:
22724
- match_obj = TranslationMatch(
22725
- source=tm_match.get('source', ''),
22726
- target=tm_match.get('target', ''),
22727
- relevance=tm_match.get('similarity', 0),
22728
- metadata={'tm_name': tm_match.get('tm_id', 'project')},
22729
- match_type='TM',
22730
- compare_source=tm_match.get('source', ''),
22731
- provider_code='TM'
22732
- )
22733
- matches_dict["TM"].append(match_obj)
22734
- except Exception as e:
22735
- pass # Silently continue
23509
+ # 1. TM matches - SKIP in prefetch worker (TM search not thread-safe)
23510
+ # TM will be fetched on-demand when user navigates to segment
23511
+ pass
22736
23512
 
22737
23513
  # 2. MT matches (if enabled)
22738
23514
  if self.enable_mt_matching:
@@ -22907,8 +23683,9 @@ class SupervertalerQt(QMainWindow):
22907
23683
  mode_note = " (overwrite)" if overwrite_mode else ""
22908
23684
  msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
22909
23685
  self._queue_tm_save_log(msg)
22910
- # Invalidate cache so prefetched segments get fresh TM matches
22911
- self.invalidate_translation_cache()
23686
+ # NOTE: Removed cache invalidation here - it was destroying batch worker's cache
23687
+ # on every Ctrl+Enter, making navigation extremely slow. The small chance of
23688
+ # seeing stale TM matches is far less important than responsive navigation.
22912
23689
 
22913
23690
  def invalidate_translation_cache(self, smart_invalidation=True):
22914
23691
  """
@@ -23088,9 +23865,21 @@ class SupervertalerQt(QMainWindow):
23088
23865
  original_path = getattr(self, 'original_docx', None) or getattr(self, 'current_document_path', None)
23089
23866
  if original_path and os.path.exists(original_path):
23090
23867
  self.current_project.original_docx_path = original_path
23091
-
23868
+
23869
+ # IMPORTANT: Always save segments in original document order, not sorted order
23870
+ # Store current sort state and temporarily restore original order
23871
+ current_sort_state = getattr(self, 'current_sort', None)
23872
+ current_segments = self.current_project.segments.copy() # Save current (possibly sorted) order
23873
+
23874
+ # Restore original order for saving
23875
+ if hasattr(self, '_original_segment_order') and self._original_segment_order:
23876
+ self.current_project.segments = self._original_segment_order.copy()
23877
+
23092
23878
  with open(file_path, 'w', encoding='utf-8') as f:
23093
23879
  json.dump(self.current_project.to_dict(), f, indent=2, ensure_ascii=False)
23880
+
23881
+ # Restore the current (sorted) order after saving
23882
+ self.current_project.segments = current_segments
23094
23883
 
23095
23884
  self.project_modified = False
23096
23885
  self.update_window_title()
@@ -23734,9 +24523,9 @@ class SupervertalerQt(QMainWindow):
23734
24523
  # Initialize TM for this project
23735
24524
  self.initialize_tm_database()
23736
24525
 
23737
- # Deactivate all resources for new project (user explicitly activates what they need)
24526
+ # Deactivate all resources for new project, then auto-activate language-matching ones
23738
24527
  self._deactivate_all_resources_for_new_project()
23739
-
24528
+
23740
24529
  # Auto-resize rows for better initial display
23741
24530
  self.auto_resize_rows()
23742
24531
 
@@ -26210,7 +26999,11 @@ class SupervertalerQt(QMainWindow):
26210
26999
 
26211
27000
  # Store memoQ source path in project for persistence across saves
26212
27001
  self.current_project.memoq_source_path = file_path
26213
-
27002
+
27003
+ # Sync global language settings with imported project languages
27004
+ self.source_language = source_lang
27005
+ self.target_language = target_lang
27006
+
26214
27007
  # Create segments with simple sequential IDs
26215
27008
  for idx, source_text in enumerate(source_segments):
26216
27009
  existing_target = target_segments[idx] if idx < len(target_segments) else ""
@@ -26234,15 +27027,15 @@ class SupervertalerQt(QMainWindow):
26234
27027
  self.load_segments_to_grid()
26235
27028
  self.initialize_tm_database()
26236
27029
 
26237
- # Deactivate all resources for new project (user explicitly activates what they need)
27030
+ # Deactivate all resources for new project, then auto-activate language-matching ones
26238
27031
  self._deactivate_all_resources_for_new_project()
26239
-
27032
+
26240
27033
  # Auto-resize rows for better initial display
26241
27034
  self.auto_resize_rows()
26242
-
27035
+
26243
27036
  # Initialize spellcheck for target language
26244
27037
  self._initialize_spellcheck_for_target_language(target_lang)
26245
-
27038
+
26246
27039
  # If smart formatting was used, auto-enable Tags view so user sees the tags
26247
27040
  if self.memoq_smart_formatting:
26248
27041
  self._enable_tag_view_after_import()
@@ -26731,7 +27524,11 @@ class SupervertalerQt(QMainWindow):
26731
27524
 
26732
27525
  # Store memoQ XLIFF source path in project for persistence across saves
26733
27526
  self.current_project.mqxliff_source_path = file_path
26734
-
27527
+
27528
+ # Sync global language settings with imported project languages
27529
+ self.source_language = source_lang
27530
+ self.target_language = target_lang
27531
+
26735
27532
  # Update UI
26736
27533
  self.project_file_path = None
26737
27534
  self.project_modified = True
@@ -26739,15 +27536,15 @@ class SupervertalerQt(QMainWindow):
26739
27536
  self.load_segments_to_grid()
26740
27537
  self.initialize_tm_database()
26741
27538
 
26742
- # Deactivate all resources for new project (user explicitly activates what they need)
27539
+ # Deactivate all resources for new project, then auto-activate language-matching ones
26743
27540
  self._deactivate_all_resources_for_new_project()
26744
-
27541
+
26745
27542
  # Auto-resize rows for better initial display
26746
27543
  self.auto_resize_rows()
26747
-
27544
+
26748
27545
  # Initialize spellcheck for target language
26749
27546
  self._initialize_spellcheck_for_target_language(target_lang)
26750
-
27547
+
26751
27548
  # Log success
26752
27549
  self.log(f"✓ Imported {len(segments)} segments from memoQ XLIFF: {Path(file_path).name}")
26753
27550
  self.log(f" Source: {source_lang}, Target: {target_lang}")
@@ -27006,7 +27803,11 @@ class SupervertalerQt(QMainWindow):
27006
27803
 
27007
27804
  # Store CafeTran source path in project for persistence across saves
27008
27805
  self.current_project.cafetran_source_path = file_path
27009
-
27806
+
27807
+ # Sync global language settings with imported project languages
27808
+ self.source_language = self.current_project.source_lang
27809
+ self.target_language = self.current_project.target_lang
27810
+
27010
27811
  # Update UI
27011
27812
  self.project_file_path = None
27012
27813
  self.project_modified = True
@@ -27014,16 +27815,16 @@ class SupervertalerQt(QMainWindow):
27014
27815
  self.load_segments_to_grid()
27015
27816
  self.initialize_tm_database()
27016
27817
 
27017
- # Deactivate all resources for new project (user explicitly activates what they need)
27818
+ # Deactivate all resources for new project, then auto-activate language-matching ones
27018
27819
  self._deactivate_all_resources_for_new_project()
27019
-
27820
+
27020
27821
  # Auto-resize rows for better initial display
27021
27822
  self.auto_resize_rows()
27022
-
27823
+
27023
27824
  # Initialize spellcheck for target language
27024
27825
  target_lang = self.current_project.target_lang if self.current_project else 'nl'
27025
27826
  self._initialize_spellcheck_for_target_language(target_lang)
27026
-
27827
+
27027
27828
  # Log success
27028
27829
  self.log(f"✓ Imported {len(segments)} segments from CafeTran bilingual DOCX: {Path(file_path).name}")
27029
27830
 
@@ -27230,7 +28031,11 @@ class SupervertalerQt(QMainWindow):
27230
28031
 
27231
28032
  # Store Trados source path in project for persistence across saves
27232
28033
  self.current_project.trados_source_path = file_path
27233
-
28034
+
28035
+ # Sync global language settings with imported project languages
28036
+ self.source_language = source_lang
28037
+ self.target_language = target_lang
28038
+
27234
28039
  # Update UI
27235
28040
  self.project_file_path = None
27236
28041
  self.project_modified = True
@@ -27238,15 +28043,15 @@ class SupervertalerQt(QMainWindow):
27238
28043
  self.load_segments_to_grid()
27239
28044
  self.initialize_tm_database()
27240
28045
 
27241
- # Deactivate all resources for new project (user explicitly activates what they need)
28046
+ # Deactivate all resources for new project, then auto-activate language-matching ones
27242
28047
  self._deactivate_all_resources_for_new_project()
27243
-
28048
+
27244
28049
  # Auto-resize rows for better initial display
27245
28050
  self.auto_resize_rows()
27246
-
28051
+
27247
28052
  # Initialize spellcheck for target language
27248
28053
  self._initialize_spellcheck_for_target_language(target_lang)
27249
-
28054
+
27250
28055
  # Count segments with tags
27251
28056
  tagged_count = sum(1 for s in trados_segments if s.source_tags)
27252
28057
 
@@ -27590,7 +28395,11 @@ class SupervertalerQt(QMainWindow):
27590
28395
  self.sdlppx_handler = handler
27591
28396
  self.sdlppx_source_file = file_path
27592
28397
  self.current_project.sdlppx_source_path = file_path
27593
-
28398
+
28399
+ # Sync global language settings with imported project languages
28400
+ self.source_language = source_lang
28401
+ self.target_language = target_lang
28402
+
27594
28403
  # Update UI
27595
28404
  self.project_file_path = None
27596
28405
  self.project_modified = True
@@ -27923,6 +28732,10 @@ class SupervertalerQt(QMainWindow):
27923
28732
  # Store Phrase source path in project for persistence across saves
27924
28733
  self.current_project.phrase_source_path = file_path
27925
28734
 
28735
+ # Sync global language settings with imported project languages
28736
+ self.source_language = source_lang
28737
+ self.target_language = target_lang
28738
+
27926
28739
  # Update UI
27927
28740
  self.project_file_path = None
27928
28741
  self.project_modified = True
@@ -28203,7 +29016,11 @@ class SupervertalerQt(QMainWindow):
28203
29016
 
28204
29017
  # Store Déjà Vu source path in project for persistence
28205
29018
  self.current_project.dejavu_source_path = file_path
28206
-
29019
+
29020
+ # Sync global language settings with imported project languages
29021
+ self.source_language = source_lang
29022
+ self.target_language = target_lang
29023
+
28207
29024
  # Create segments
28208
29025
  for idx, seg_data in enumerate(segments_data):
28209
29026
  segment = Segment(
@@ -28777,7 +29594,11 @@ class SupervertalerQt(QMainWindow):
28777
29594
  def load_segments_to_grid(self):
28778
29595
  """Load segments into the grid with termbase highlighting"""
28779
29596
  self.log(f"🔄🔄🔄 load_segments_to_grid CALLED - this will RELOAD grid from segment data!")
28780
-
29597
+
29598
+ # Ensure original segment order is stored (for Document Order sort)
29599
+ if self.current_project and not hasattr(self, '_original_segment_order'):
29600
+ self._original_segment_order = self.current_project.segments.copy()
29601
+
28781
29602
  # Clear row color settings cache to ensure fresh settings are loaded
28782
29603
  if hasattr(self, '_row_color_settings_cached'):
28783
29604
  delattr(self, '_row_color_settings_cached')
@@ -28853,6 +29674,9 @@ class SupervertalerQt(QMainWindow):
28853
29674
  segment_num_color = "black" if theme.name in ["Light (Default)", "Soft Gray", "Warm Cream", "Sepia", "High Contrast"] else theme.text
28854
29675
  id_item.setForeground(QColor(segment_num_color))
28855
29676
  id_item.setBackground(QColor()) # Default background from theme
29677
+ # Smaller font for segment numbers
29678
+ seg_num_font = QFont(self.default_font_family, max(9, self.default_font_size - 1))
29679
+ id_item.setFont(seg_num_font)
28856
29680
  self.table.setItem(row, 0, id_item)
28857
29681
 
28858
29682
  # Type - show segment type based on style and content
@@ -28935,13 +29759,17 @@ class SupervertalerQt(QMainWindow):
28935
29759
  type_item = QTableWidgetItem(type_display)
28936
29760
  type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable) # Read-only
28937
29761
  type_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
28938
-
29762
+
28939
29763
  # Color-code by type for better visibility
28940
29764
  if type_display in ("H1", "H2", "H3", "H4", "Title"):
28941
29765
  type_item.setForeground(QColor("#1976D2")) # Blue for headings (works in both themes)
28942
29766
  elif type_display.startswith("#") or type_display in ("•", "li"):
28943
29767
  type_item.setForeground(QColor("#388E3C")) # Green for list items (works in both themes)
28944
-
29768
+
29769
+ # Smaller font for type symbols
29770
+ type_font = QFont(self.default_font_family, max(9, self.default_font_size - 1))
29771
+ type_item.setFont(type_font)
29772
+
28945
29773
  self.table.setItem(row, 1, type_item)
28946
29774
 
28947
29775
  # Source - Use read-only QTextEdit widget for easy text selection
@@ -29218,9 +30046,13 @@ class SupervertalerQt(QMainWindow):
29218
30046
  termview_layout.setContentsMargins(4, 4, 4, 0)
29219
30047
  termview_layout.setSpacing(2)
29220
30048
 
29221
- # Termview header label
30049
+ # Termview header label (theme-aware)
29222
30050
  termview_header = QLabel("📖 Termview")
29223
- termview_header.setStyleSheet("font-weight: bold; font-size: 9px; color: #666;")
30051
+ if hasattr(self, 'theme_manager') and self.theme_manager:
30052
+ header_color = self.theme_manager.current_theme.text_disabled
30053
+ else:
30054
+ header_color = "#666"
30055
+ termview_header.setStyleSheet(f"font-weight: bold; font-size: 9px; color: {header_color};")
29224
30056
  termview_layout.addWidget(termview_header)
29225
30057
 
29226
30058
  # Third Termview instance for Match Panel
@@ -29244,11 +30076,19 @@ class SupervertalerQt(QMainWindow):
29244
30076
  tm_layout = QHBoxLayout(tm_container)
29245
30077
  tm_layout.setContentsMargins(0, 0, 0, 0)
29246
30078
  tm_layout.setSpacing(0)
29247
-
29248
- # Hardcode the green color for TM boxes (same as TM Target in Compare Panel)
29249
- tm_box_bg = "#d4edda" # Green (same as TM Target in Compare Panel)
29250
- text_color = "#333"
29251
- border_color = "#ddd"
30079
+
30080
+ # Get theme-aware colors for TM boxes (same as Compare Panel)
30081
+ if hasattr(self, 'theme_manager') and self.theme_manager:
30082
+ theme = self.theme_manager.current_theme
30083
+ is_dark = getattr(theme, 'is_dark', 'dark' in theme.name.lower())
30084
+ border_color = theme.border
30085
+ text_color = theme.text
30086
+ # Green background - theme-appropriate shade
30087
+ tm_box_bg = theme.panel_success if hasattr(theme, 'panel_success') else ("#1e3a2f" if is_dark else "#d4edda")
30088
+ else:
30089
+ tm_box_bg = "#d4edda" # Green (light mode)
30090
+ text_color = "#333"
30091
+ border_color = "#ddd"
29252
30092
 
29253
30093
  # TM Source box (GREEN, with navigation)
29254
30094
  self.match_panel_tm_matches = [] # Separate match list
@@ -29377,7 +30217,10 @@ class SupervertalerQt(QMainWindow):
29377
30217
  if shortcut_badge_tooltip:
29378
30218
  badge_label.setToolTip(f"Press {shortcut_badge_tooltip} to insert")
29379
30219
  header_layout.addWidget(badge_label)
29380
-
30220
+
30221
+ # Add stretch to push navigation controls to the right (aligned with scrollbar)
30222
+ header_layout.addStretch()
30223
+
29381
30224
  nav_label = None
29382
30225
  nav_buttons = None
29383
30226
 
@@ -29390,45 +30233,60 @@ class SupervertalerQt(QMainWindow):
29390
30233
  header_layout.addWidget(nav_label)
29391
30234
 
29392
30235
  if has_navigation:
29393
-
29394
- # Prev button
29395
- prev_btn = QPushButton("")
29396
- prev_btn.setFixedSize(18, 16)
29397
- prev_btn.setStyleSheet(f"""
29398
- QPushButton {{
29399
- font-size: 9px;
29400
- padding: 0px;
29401
- background: transparent;
29402
- border: 1px solid {border_color};
29403
- border-radius: 2px;
29404
- color: {text_color};
29405
- }}
29406
- QPushButton:hover {{
29407
- background: rgba(128,128,128,0.2);
29408
- }}
29409
- """)
30236
+ # Detect theme for arrow color
30237
+ is_dark_theme = hasattr(self, 'theme_manager') and self.theme_manager and 'dark' in self.theme_manager.current_theme.name.lower()
30238
+ arrow_color = "#FFFFFF" if is_dark_theme else "#333333"
30239
+
30240
+ # Create clickable label class with theme update capability
30241
+ from PyQt6.QtCore import pyqtSignal
30242
+
30243
+ class ClickableArrow(QLabel):
30244
+ clicked = pyqtSignal()
30245
+
30246
+ def __init__(self, arrow_symbol, parent=None):
30247
+ """arrow_symbol: '◀' or '▶'"""
30248
+ self.arrow_symbol = arrow_symbol
30249
+ super().__init__("", parent)
30250
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
30251
+
30252
+ def set_color(self, color):
30253
+ """Update arrow color for current theme"""
30254
+ self.setStyleSheet(f"""
30255
+ QLabel {{
30256
+ color: {color};
30257
+ background: transparent;
30258
+ border: none;
30259
+ font-size: 11px;
30260
+ font-weight: bold;
30261
+ }}
30262
+ """)
30263
+ self.setText(self.arrow_symbol)
30264
+
30265
+ def mousePressEvent(self, event):
30266
+ self.clicked.emit()
30267
+ super().mousePressEvent(event)
30268
+
30269
+ # Prev arrow - using ◀
30270
+ prev_btn = ClickableArrow("◀")
30271
+ prev_btn.set_color(arrow_color)
30272
+ prev_btn.setFixedSize(16, 16)
30273
+ prev_btn.setAlignment(Qt.AlignmentFlag.AlignCenter)
29410
30274
  header_layout.addWidget(prev_btn)
29411
-
29412
- # Next button
29413
- next_btn = QPushButton("")
29414
- next_btn.setFixedSize(18, 16)
29415
- next_btn.setStyleSheet(f"""
29416
- QPushButton {{
29417
- font-size: 9px;
29418
- padding: 0px;
29419
- background: transparent;
29420
- border: 1px solid {border_color};
29421
- border-radius: 2px;
29422
- color: {text_color};
29423
- }}
29424
- QPushButton:hover {{
29425
- background: rgba(128,128,128,0.2);
29426
- }}
29427
- """)
30275
+
30276
+ # Next arrow - using ▶
30277
+ next_btn = ClickableArrow("")
30278
+ next_btn.set_color(arrow_color)
30279
+ next_btn.setFixedSize(16, 16)
30280
+ next_btn.setAlignment(Qt.AlignmentFlag.AlignCenter)
29428
30281
  header_layout.addWidget(next_btn)
30282
+
30283
+ # Store reference for theme updates
30284
+ if not hasattr(self, 'theme_aware_arrows'):
30285
+ self.theme_aware_arrows = []
30286
+ self.theme_aware_arrows.extend([prev_btn, next_btn])
30287
+
29429
30288
  nav_buttons = [prev_btn, next_btn]
29430
-
29431
- header_layout.addStretch()
30289
+
29432
30290
  main_layout.addLayout(header_layout)
29433
30291
 
29434
30292
  # Text area
@@ -30351,19 +31209,24 @@ class SupervertalerQt(QMainWindow):
30351
31209
  widget.setToolTip(f"Notes: {segment.notes.strip()}")
30352
31210
 
30353
31211
  status_def = get_status(segment.status)
30354
- status_label = QLabel(status_def.icon)
31212
+
31213
+ # Status icons: ✔ (green) and ❌ (naturally red emoji)
31214
+ icon_text = status_def.icon
31215
+ if segment.status == "confirmed":
31216
+ icon_html = f'<font color="#2e7d32" size="2">{icon_text}</font>' # Green checkmark
31217
+ else:
31218
+ icon_html = f'<font size="2">{icon_text}</font>' # Other icons (including ❌ emoji)
31219
+
31220
+ status_label = QLabel(icon_html)
31221
+ status_label.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
30355
31222
  status_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
31223
+ status_label.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) # Expand vertically to match row height
30356
31224
  status_label.setToolTip(status_def.label)
30357
- # Slightly smaller X for "not_started" to match other icons better
30358
- font_size = "10px" if segment.status == "not_started" else "14px"
30359
- # Make confirmed checkmark green
30360
- color = "color: #2e7d32;" if segment.status == "confirmed" else ""
30361
-
30362
- # Apply orange background highlight to status icon if segment has notes
31225
+
31226
+ # Add orange background if segment has notes (using stylesheet for background only)
30363
31227
  if has_notes:
30364
- status_label.setStyleSheet(f"font-size: {font_size}; {color} padding: 2px 4px; background-color: rgba(255, 152, 0, 0.35); border-radius: 3px;")
30365
- else:
30366
- status_label.setStyleSheet(f"font-size: {font_size}; {color} padding-right: 4px;")
31228
+ status_label.setStyleSheet("padding: 2px 4px; background-color: rgba(255, 152, 0, 0.35); border-radius: 3px;")
31229
+ # Note: No stylesheet for non-notes case to avoid interfering with HTML color
30367
31230
  layout.addWidget(status_label)
30368
31231
 
30369
31232
  # Only add match label if there's a match percentage
@@ -30475,14 +31338,17 @@ class SupervertalerQt(QMainWindow):
30475
31338
  """Auto-resize all rows to fit content - Compact version"""
30476
31339
  if not hasattr(self, 'table') or not self.table:
30477
31340
  return
30478
-
31341
+
30479
31342
  # Reduce width slightly to account for padding and prevent text cut-off
30480
31343
  width_reduction = 8
30481
-
31344
+
30482
31345
  # Manually calculate and set row heights for compact display
30483
31346
  for row in range(self.table.rowCount()):
31347
+ # Keep UI responsive during large grid updates
31348
+ if row % 50 == 0:
31349
+ QApplication.processEvents()
30484
31350
  self._auto_resize_single_row(row, width_reduction)
30485
-
31351
+
30486
31352
  self.log("✓ Auto-resized rows to fit content (compact)")
30487
31353
  self._enforce_status_row_heights()
30488
31354
 
@@ -30529,26 +31395,30 @@ class SupervertalerQt(QMainWindow):
30529
31395
  def apply_font_to_grid(self):
30530
31396
  """Apply selected font to all grid cells"""
30531
31397
  font = QFont(self.default_font_family, self.default_font_size)
30532
-
31398
+
30533
31399
  self.table.setFont(font)
30534
-
30535
- # Also update header font
30536
- header_font = QFont(self.default_font_family, self.default_font_size, QFont.Weight.Bold)
31400
+
31401
+ # Also update header font - same size as grid content, normal weight
31402
+ header_font = QFont(self.default_font_family, self.default_font_size, QFont.Weight.Normal)
30537
31403
  self.table.horizontalHeader().setFont(header_font)
30538
-
31404
+
30539
31405
  # Update fonts in QTextEdit widgets (source and target columns)
30540
31406
  if hasattr(self, 'table') and self.table:
30541
31407
  for row in range(self.table.rowCount()):
31408
+ # Keep UI responsive during large grid updates
31409
+ if row % 50 == 0:
31410
+ QApplication.processEvents()
31411
+
30542
31412
  # Source column (2) - ReadOnlyGridTextEditor
30543
31413
  source_widget = self.table.cellWidget(row, 2)
30544
31414
  if source_widget and isinstance(source_widget, ReadOnlyGridTextEditor):
30545
31415
  source_widget.setFont(font)
30546
-
31416
+
30547
31417
  # Target column (3) - EditableGridTextEditor
30548
31418
  target_widget = self.table.cellWidget(row, 3)
30549
31419
  if target_widget and isinstance(target_widget, EditableGridTextEditor):
30550
31420
  target_widget.setFont(font)
30551
-
31421
+
30552
31422
  # Adjust segment number column width based on font size
30553
31423
  self._update_segment_column_width()
30554
31424
 
@@ -30572,12 +31442,12 @@ class SupervertalerQt(QMainWindow):
30572
31442
  # Measure the width of the largest number (as string)
30573
31443
  text_width = fm.horizontalAdvance(str(max_segment))
30574
31444
 
30575
- # Add padding (10px on each side = 20px total)
30576
- new_width = text_width + 20
30577
-
30578
- # Ensure minimum width for very small numbers
30579
- new_width = max(30, new_width)
30580
-
31445
+ # Add padding (6px on each side = 12px total)
31446
+ new_width = text_width + 12
31447
+
31448
+ # Ensure minimum width for very small numbers, cap at width for ~1000
31449
+ new_width = max(30, min(new_width, 55))
31450
+
30581
31451
  self.table.setColumnWidth(0, new_width)
30582
31452
 
30583
31453
  def set_font_family(self, family_name: str):
@@ -30609,14 +31479,18 @@ class SupervertalerQt(QMainWindow):
30609
31479
  """Refresh tag highlight colors in all grid cells"""
30610
31480
  if not hasattr(self, 'table') or not self.table:
30611
31481
  return
30612
-
31482
+
30613
31483
  for row in range(self.table.rowCount()):
31484
+ # Keep UI responsive during large grid updates
31485
+ if row % 50 == 0:
31486
+ QApplication.processEvents()
31487
+
30614
31488
  # Source column (2) - ReadOnlyGridTextEditor
30615
31489
  source_widget = self.table.cellWidget(row, 2)
30616
31490
  if source_widget and isinstance(source_widget, ReadOnlyGridTextEditor):
30617
31491
  if hasattr(source_widget, 'highlighter'):
30618
31492
  source_widget.highlighter.set_tag_color(EditableGridTextEditor.tag_highlight_color)
30619
-
31493
+
30620
31494
  # Target column (3) - EditableGridTextEditor
30621
31495
  target_widget = self.table.cellWidget(row, 3)
30622
31496
  if target_widget and isinstance(target_widget, EditableGridTextEditor):
@@ -30681,18 +31555,22 @@ class SupervertalerQt(QMainWindow):
30681
31555
  """Apply alternating row colors to all source and target cells in the grid"""
30682
31556
  if not hasattr(self, 'table') or not self.table:
30683
31557
  return
30684
-
31558
+
30685
31559
  # Clear cached settings to force reload
30686
31560
  if hasattr(self, '_row_color_settings_cached'):
30687
31561
  delattr(self, '_row_color_settings_cached')
30688
-
31562
+
30689
31563
  for row in range(self.table.rowCount()):
31564
+ # Keep UI responsive during large grid updates
31565
+ if row % 50 == 0:
31566
+ QApplication.processEvents()
31567
+
30690
31568
  source_widget = self.table.cellWidget(row, 2)
30691
31569
  target_widget = self.table.cellWidget(row, 3)
30692
-
31570
+
30693
31571
  if source_widget and target_widget:
30694
31572
  self._apply_row_color(row, source_widget, target_widget)
30695
-
31573
+
30696
31574
  self.log("✓ Alternating row colors applied")
30697
31575
 
30698
31576
  def on_font_changed(self):
@@ -30864,8 +31742,8 @@ class SupervertalerQt(QMainWindow):
30864
31742
  self.show_translation_results_pane = settings.get('show_translation_results_pane', False)
30865
31743
  self.show_compare_panel = settings.get('show_compare_panel', True)
30866
31744
 
30867
- # 🧪 EXPERIMENTAL: Load cache kill switch setting (default: True = caches disabled for stability)
30868
- self.disable_all_caches = settings.get('disable_all_caches', True)
31745
+ # Load cache kill switch setting (default: False = caches ENABLED for performance)
31746
+ self.disable_all_caches = settings.get('disable_all_caches', False)
30869
31747
 
30870
31748
  # Load LLM provider settings for AI Assistant
30871
31749
  llm_settings = self.load_llm_settings()
@@ -31278,7 +32156,7 @@ class SupervertalerQt(QMainWindow):
31278
32156
  """Handle cell selection change"""
31279
32157
  if self.debug_mode_enabled:
31280
32158
  self.log(f"🎯 on_cell_selected called: row {current_row}, col {current_col}")
31281
-
32159
+
31282
32160
  # 🚫 GUARD: Don't re-run lookups if we're staying on the same row
31283
32161
  # This prevents lookups when user edits text (focus changes within same row)
31284
32162
  if hasattr(self, '_last_selected_row') and self._last_selected_row == current_row:
@@ -31286,34 +32164,35 @@ class SupervertalerQt(QMainWindow):
31286
32164
  self.log(f"⏭️ Skipping lookup - already on row {current_row}")
31287
32165
  return
31288
32166
  self._last_selected_row = current_row
31289
-
31290
- # ⚡ FAST PATH: For arrow key OR Ctrl+Enter navigation, defer heavy lookups
31291
- # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
31292
- is_arrow_nav = getattr(self, '_arrow_key_navigation', False)
31293
- is_ctrl_enter_nav = getattr(self, '_ctrl_enter_navigation', False)
31294
-
31295
- if is_arrow_nav or is_ctrl_enter_nav:
31296
- self._arrow_key_navigation = False # Reset flags
31297
- self._ctrl_enter_navigation = False
31298
-
31299
- # Schedule deferred lookup with short delay (150ms) for rapid navigation
31300
- if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
31301
- self._deferred_lookup_timer.stop()
31302
- from PyQt6.QtCore import QTimer
31303
- self._deferred_lookup_timer = QTimer()
31304
- self._deferred_lookup_timer.setSingleShot(True)
31305
- self._deferred_lookup_timer.timeout.connect(
31306
- lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
31307
- self._on_cell_selected_full(r, c, pr, pc)
31308
- )
31309
- self._deferred_lookup_timer.start(150) # 150ms debounce
31310
-
31311
- # Do minimal UI update immediately (orange highlight, scroll)
32167
+
32168
+ # ⚡ FILTER MODE: Skip ALL heavy lookups when text filters are active
32169
+ # User is quickly navigating through filtered results - don't slow them down
32170
+ is_filtering = getattr(self, 'filtering_active', False)
32171
+ if is_filtering:
32172
+ # Only do minimal UI update (orange highlight) - no TM/termbase lookups
31312
32173
  self._on_cell_selected_minimal(current_row, previous_row)
31313
32174
  return
31314
-
31315
- # Full processing for non-arrow-key navigation (click, etc.)
31316
- self._on_cell_selected_full(current_row, current_col, previous_row, previous_col)
32175
+
32176
+ # FAST PATH: Defer heavy lookups for ALL navigation (arrow keys, Ctrl+Enter, AND mouse clicks)
32177
+ # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
32178
+ # Reset any navigation flags
32179
+ self._arrow_key_navigation = False
32180
+ self._ctrl_enter_navigation = False
32181
+
32182
+ # Schedule deferred lookup with short delay - debounce prevents hammering during rapid navigation
32183
+ if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
32184
+ self._deferred_lookup_timer.stop()
32185
+ from PyQt6.QtCore import QTimer
32186
+ self._deferred_lookup_timer = QTimer()
32187
+ self._deferred_lookup_timer.setSingleShot(True)
32188
+ self._deferred_lookup_timer.timeout.connect(
32189
+ lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
32190
+ self._on_cell_selected_full(r, c, pr, pc)
32191
+ )
32192
+ self._deferred_lookup_timer.start(10) # 10ms - just enough to batch rapid arrow key holding
32193
+
32194
+ # Do minimal UI update immediately (orange highlight, scroll)
32195
+ self._on_cell_selected_minimal(current_row, previous_row)
31317
32196
 
31318
32197
  def _center_row_in_viewport(self, row: int):
31319
32198
  """Center the given row vertically in the visible table viewport.
@@ -31535,9 +32414,12 @@ class SupervertalerQt(QMainWindow):
31535
32414
  ]
31536
32415
  # Also get NT matches (fresh, not cached - they may have changed)
31537
32416
  nt_matches = self.find_nt_matches_in_source(segment.source)
31538
-
32417
+
32418
+ # Get status hint for termbase activation
32419
+ status_hint = self._get_termbase_status_hint()
32420
+
31539
32421
  # Update both Termview widgets (left and right)
31540
- self._update_both_termviews(segment.source, termbase_matches, nt_matches)
32422
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
31541
32423
  except Exception as e:
31542
32424
  self.log(f"Error updating termview from cache: {e}")
31543
32425
 
@@ -31570,9 +32452,25 @@ class SupervertalerQt(QMainWindow):
31570
32452
  if has_fuzzy_match and not has_100_match:
31571
32453
  self._play_sound_effect('tm_fuzzy_match')
31572
32454
 
31573
- # Skip the slow lookup below, we already have everything
31574
- # Continue to prefetch trigger at the end
32455
+ # Skip the slow TERMBASE lookup below, we already have termbase matches cached
32456
+ # But TM lookup was skipped in prefetch (not thread-safe), so schedule it now
31575
32457
  matches_dict = cached_matches # Set for later use
32458
+
32459
+ # v1.9.182: Schedule TM lookup even on cache hit (prefetch skips TM - not thread-safe)
32460
+ tm_count = len(cached_matches.get("TM", []))
32461
+ if tm_count == 0 and self.enable_tm_matching:
32462
+ find_replace_active = getattr(self, 'find_replace_active', False)
32463
+ if not find_replace_active:
32464
+ # Get termbase matches for the lookup
32465
+ termbase_matches_for_tm = [
32466
+ {
32467
+ 'source_term': match.source,
32468
+ 'target_term': match.target,
32469
+ 'termbase_name': match.metadata.get('termbase_name', '') if match.metadata else '',
32470
+ }
32471
+ for match in cached_matches.get("Termbases", [])
32472
+ ]
32473
+ self._schedule_mt_and_llm_matches(segment, termbase_matches_for_tm)
31576
32474
 
31577
32475
  # Check if TM/Termbase matching is enabled
31578
32476
  if not matches_dict and (not self.enable_tm_matching and not self.enable_termbase_matching):
@@ -31621,9 +32519,12 @@ class SupervertalerQt(QMainWindow):
31621
32519
  ] if stored_matches else []
31622
32520
  # Also get NT matches
31623
32521
  nt_matches = self.find_nt_matches_in_source(segment.source)
31624
-
32522
+
32523
+ # Get status hint for termbase activation
32524
+ status_hint = self._get_termbase_status_hint()
32525
+
31625
32526
  # Update both Termview widgets (left and right)
31626
- self._update_both_termviews(segment.source, termbase_matches, nt_matches)
32527
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
31627
32528
  except Exception as e:
31628
32529
  self.log(f"Error refreshing termview: {e}")
31629
32530
 
@@ -31790,15 +32691,19 @@ class SupervertalerQt(QMainWindow):
31790
32691
 
31791
32692
  # Schedule expensive searches (TM, MT, LLM) with debouncing to prevent UI blocking
31792
32693
  # ONLY schedule if:
31793
- # 1. Cache miss (no prefetched matches)
32694
+ # 1. Cache miss OR cache hit with no TM matches (prefetch doesn't include TM - not thread-safe)
31794
32695
  # 2. TM matching is enabled
31795
32696
  # 3. Find/Replace is not active (to avoid slowdowns during navigation)
32697
+ needs_tm_lookup = True
31796
32698
  with self.translation_matches_cache_lock:
31797
- cache_hit = segment_id in self.translation_matches_cache
31798
-
32699
+ if segment_id in self.translation_matches_cache:
32700
+ cached = self.translation_matches_cache[segment_id]
32701
+ # v1.9.182: Check if TM matches exist - prefetch worker skips TM lookups
32702
+ needs_tm_lookup = len(cached.get("TM", [])) == 0
32703
+
31799
32704
  find_replace_active = getattr(self, 'find_replace_active', False)
31800
-
31801
- if not cache_hit and self.enable_tm_matching and not find_replace_active:
32705
+
32706
+ if needs_tm_lookup and self.enable_tm_matching and not find_replace_active:
31802
32707
  # Get termbase matches if they exist (could be None or empty)
31803
32708
  termbase_matches = matches_dict.get('Termbases', []) if matches_dict else []
31804
32709
  self._schedule_mt_and_llm_matches(segment, termbase_matches)
@@ -31810,9 +32715,7 @@ class SupervertalerQt(QMainWindow):
31810
32715
  next_segment_ids = []
31811
32716
  start_idx = current_row + 1
31812
32717
  end_idx = min(start_idx + 20, len(self.current_project.segments))
31813
-
31814
- print(f"[PROACTIVE NAV DEBUG] Navigation to row {current_row}, checking segments {start_idx} to {end_idx}")
31815
-
32718
+
31816
32719
  for seg in self.current_project.segments[start_idx:end_idx]:
31817
32720
  # Check if already cached
31818
32721
  with self.translation_matches_cache_lock:
@@ -31826,15 +32729,12 @@ class SupervertalerQt(QMainWindow):
31826
32729
  try:
31827
32730
  with self.termbase_cache_lock:
31828
32731
  termbase_raw = self.termbase_cache.get(seg.id, {})
31829
- print(f"[PROACTIVE NAV DEBUG] Seg {seg.id}: cached, termbase_raw has {len(termbase_raw) if termbase_raw else 0} matches")
31830
32732
  if termbase_raw:
31831
32733
  termbase_json = json.dumps(termbase_raw)
31832
- print(f"[PROACTIVE NAV DEBUG] Calling _apply_proactive_highlighting for seg {seg.id}")
31833
32734
  self._apply_proactive_highlighting(seg.id, termbase_json)
31834
- except Exception as e:
31835
- print(f"[PROACTIVE NAV DEBUG] Error for seg {seg.id}: {e}")
31836
-
31837
- print(f"[PROACTIVE NAV DEBUG] Need to prefetch: {len(next_segment_ids)} segments")
32735
+ except Exception:
32736
+ pass # Silent failure for proactive highlighting
32737
+
31838
32738
  if next_segment_ids:
31839
32739
  self._start_prefetch_worker(next_segment_ids)
31840
32740
 
@@ -33765,7 +34665,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33765
34665
  self.nt_manager.set_list_active(list_name, False)
33766
34666
 
33767
34667
  self.log("📋 New project: All TMs, glossaries, and NT lists deactivated (start clean)")
33768
-
34668
+ self.log("💡 Tip: Go to Resources tab to activate TMs and glossaries for this project")
34669
+
33769
34670
  def search_and_display_tm_matches(self, source_text: str):
33770
34671
  """Search TM and Termbases and display matches with visual diff for fuzzy matches"""
33771
34672
  self.log(f"🚨 search_and_display_tm_matches called with source_text: '{source_text[:50]}...'")
@@ -33788,11 +34689,34 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33788
34689
  try:
33789
34690
  # Get activated TM IDs for current project
33790
34691
  tm_ids = None
34692
+ no_tms_activated = False
34693
+ tms_wrong_language = False
33791
34694
  if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr and self.current_project:
33792
34695
  project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
33793
34696
  if project_id:
33794
34697
  tm_ids = self.tm_metadata_mgr.get_active_tm_ids(project_id)
33795
-
34698
+ # Check if no TMs are activated for this project
34699
+ if tm_ids is not None and len(tm_ids) == 0:
34700
+ no_tms_activated = True
34701
+ elif tm_ids and len(tm_ids) > 0:
34702
+ # Check if any activated TMs match the project's language pair
34703
+ project_source = (self.current_project.source_lang or '').lower()
34704
+ project_target = (self.current_project.target_lang or '').lower()
34705
+ all_tms = self.tm_metadata_mgr.get_all_tms()
34706
+ has_matching_language = False
34707
+ for tm in all_tms:
34708
+ if tm['id'] in tm_ids:
34709
+ tm_source = (tm.get('source_lang') or '').lower()
34710
+ tm_target = (tm.get('target_lang') or '').lower()
34711
+ # Match if languages align (bidirectional) or TM has no language set
34712
+ if (not tm_source and not tm_target) or \
34713
+ (tm_source == project_source and tm_target == project_target) or \
34714
+ (tm_source == project_target and tm_target == project_source):
34715
+ has_matching_language = True
34716
+ break
34717
+ if not has_matching_language:
34718
+ tms_wrong_language = True
34719
+
33796
34720
  # Search for matches (using activated TMs if available)
33797
34721
  matches = self.tm_database.search_all(source_text, tm_ids=tm_ids, max_matches=5)
33798
34722
 
@@ -33839,10 +34763,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33839
34763
 
33840
34764
  if not matches:
33841
34765
  if hasattr(self, 'tm_display'):
33842
- self.tm_display.setHtml(
33843
- f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
33844
- f"<p style='color: #999;'><i>No translation memory matches found</i></p>"
33845
- )
34766
+ if no_tms_activated:
34767
+ # Show helpful message when no TMs are activated
34768
+ self.tm_display.setHtml(
34769
+ f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
34770
+ f"<p style='color: #E65100;'><i>No TMs activated for this project.</i></p>"
34771
+ f"<p style='color: #999; font-size: 9pt;'>Go to <b>Resources → TM</b> to activate translation memories.</p>"
34772
+ )
34773
+ elif tms_wrong_language:
34774
+ # Show message when TMs are activated but don't match project language pair
34775
+ project_lang_pair = f"{self.current_project.source_lang} → {self.current_project.target_lang}" if self.current_project else ""
34776
+ self.tm_display.setHtml(
34777
+ f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
34778
+ f"<p style='color: #E65100;'><i>Activated TMs don't match project language ({project_lang_pair}).</i></p>"
34779
+ f"<p style='color: #999; font-size: 9pt;'>Go to <b>Resources → TM</b> to activate TMs for this language pair.</p>"
34780
+ )
34781
+ else:
34782
+ self.tm_display.setHtml(
34783
+ f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
34784
+ f"<p style='color: #999;'><i>No translation memory matches found</i></p>"
34785
+ )
33846
34786
  return
33847
34787
 
33848
34788
  # If using TranslationResultsPanel, populate it with TM and Termbase results
@@ -34197,18 +35137,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34197
35137
  """
34198
35138
  Find all termbase matches in source text
34199
35139
  Returns dict of {term: translation} for all matches found
35140
+
35141
+ v1.9.182: Uses in-memory index for instant lookup when available.
35142
+ Falls back to per-word database queries if index not built.
34200
35143
  """
34201
35144
  if not source_text or not hasattr(self, 'db_manager') or not self.db_manager:
34202
35145
  return {}
34203
35146
 
34204
35147
  try:
35148
+ # v1.9.182: Use in-memory index for instant lookup (1000x faster)
35149
+ # The index is built on project load by _build_termbase_index()
35150
+ with self.termbase_index_lock:
35151
+ has_index = bool(self.termbase_index)
35152
+
35153
+ if has_index:
35154
+ # Fast path: use pre-built in-memory index
35155
+ return self._search_termbase_in_memory(source_text)
35156
+
35157
+ # Fallback: original per-word database query approach
35158
+ # (only used if index not yet built, e.g., during startup)
34205
35159
  source_lang = self.current_project.source_lang if self.current_project else None
34206
35160
  target_lang = self.current_project.target_lang if self.current_project else None
34207
-
35161
+
34208
35162
  # Convert language names to codes for termbase search
34209
35163
  source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
34210
35164
  target_lang_code = self._convert_language_to_code(target_lang) if target_lang else None
34211
-
35165
+
34212
35166
  # Strip HTML/XML/CAT tool tags from source text before word splitting
34213
35167
  # This handles <b>, </b>, <i>, memoQ {1}, [2}, Trados <1>, Déjà Vu {00001}, etc.
34214
35168
  import re
@@ -34218,7 +35172,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34218
35172
  # memoQ content tags: [uicontrol id="..."} or {uicontrol] or [tagname ...} or {tagname]
34219
35173
  clean_source_text = re.sub(r'\[[^\[\]]*\}', '', clean_source_text) # Opening: [anything}
34220
35174
  clean_source_text = re.sub(r'\{[^\{\}]*\]', '', clean_source_text) # Closing: {anything]
34221
-
35175
+
34222
35176
  # Search termbases for all terms that appear in the source text
34223
35177
  # Split source text into words and search for each one
34224
35178
  words = clean_source_text.split()
@@ -34440,23 +35394,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34440
35394
  termbase_matches_json: JSON-encoded termbase matches dict (thread-safe transfer)
34441
35395
  """
34442
35396
  import json
34443
-
34444
- print(f"[PROACTIVE DEBUG] _apply_proactive_highlighting called for segment {segment_id}")
34445
-
35397
+
34446
35398
  if not self.current_project or not self.table:
34447
- print(f"[PROACTIVE DEBUG] Early exit: no project or table")
34448
35399
  return
34449
-
35400
+
34450
35401
  try:
34451
35402
  # Decode the matches from JSON
34452
35403
  termbase_matches = json.loads(termbase_matches_json) if termbase_matches_json else {}
34453
-
34454
- print(f"[PROACTIVE DEBUG] Decoded {len(termbase_matches)} termbase matches")
34455
-
35404
+
34456
35405
  if not termbase_matches:
34457
- print(f"[PROACTIVE DEBUG] No matches to highlight, returning")
34458
35406
  return # Nothing to highlight
34459
-
35407
+
34460
35408
  # Find the row for this segment ID
34461
35409
  row = -1
34462
35410
  for r in range(self.table.rowCount()):
@@ -34469,44 +35417,25 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34469
35417
  break
34470
35418
  except ValueError:
34471
35419
  continue
34472
-
34473
- print(f"[PROACTIVE DEBUG] Found row {row} for segment {segment_id}")
34474
-
35420
+
34475
35421
  if row < 0:
34476
- print(f"[PROACTIVE DEBUG] Segment not visible in current page")
34477
35422
  return # Segment not visible in current page
34478
-
35423
+
34479
35424
  # Get segment source text
34480
35425
  segment = None
34481
35426
  for seg in self.current_project.segments:
34482
35427
  if seg.id == segment_id:
34483
35428
  segment = seg
34484
35429
  break
34485
-
35430
+
34486
35431
  if not segment:
34487
- print(f"[PROACTIVE DEBUG] Segment object not found")
34488
35432
  return
34489
-
34490
- print(f"[PROACTIVE DEBUG] Applying highlight_source_with_termbase to row {row}")
34491
- print(f"[PROACTIVE DEBUG] Source text: {segment.source[:80]}...")
34492
- print(f"[PROACTIVE DEBUG] Matches keys: {list(termbase_matches.keys())[:5]}")
34493
- if termbase_matches:
34494
- first_key = list(termbase_matches.keys())[0]
34495
- print(f"[PROACTIVE DEBUG] Sample match: {first_key} => {termbase_matches[first_key]}")
34496
-
34497
- # Check if the source widget exists and is the right type
34498
- source_widget = self.table.cellWidget(row, 2)
34499
- print(f"[PROACTIVE DEBUG] Source widget type: {type(source_widget).__name__ if source_widget else 'None'}")
34500
- print(f"[PROACTIVE DEBUG] Has highlight method: {hasattr(source_widget, 'highlight_termbase_matches') if source_widget else 'N/A'}")
34501
-
35433
+
34502
35434
  # Apply highlighting (this updates the source cell widget)
34503
35435
  self.highlight_source_with_termbase(row, segment.source, termbase_matches)
34504
- print(f"[PROACTIVE DEBUG] ✅ Highlighting applied successfully")
34505
-
34506
- except Exception as e:
34507
- print(f"[PROACTIVE DEBUG] ERROR: {e}")
34508
- import traceback
34509
- print(f"[PROACTIVE DEBUG] Traceback: {traceback.format_exc()}")
35436
+
35437
+ except Exception:
35438
+ pass # Silent failure for proactive highlighting
34510
35439
 
34511
35440
  def insert_term_translation(self, row: int, translation: str):
34512
35441
  """
@@ -35494,8 +36423,11 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
35494
36423
  'termbase_id': match_info.get('termbase_id'),
35495
36424
  'notes': match_info.get('notes', '')
35496
36425
  })
36426
+ # Get status hint for termbase activation
36427
+ status_hint = self._get_termbase_status_hint()
36428
+
35497
36429
  # Update both Termview widgets
35498
- self._update_both_termviews(segment.source, tb_list, nt_matches)
36430
+ self._update_both_termviews(segment.source, tb_list, nt_matches, status_hint)
35499
36431
  self.log(" ✓ TermView updated")
35500
36432
  except Exception as e:
35501
36433
  self.log(f" ⚠️ TermView update error: {e}")
@@ -36512,9 +37444,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36512
37444
  """Update spellcheck button style based on enabled state"""
36513
37445
  if hasattr(self, 'spellcheck_btn'):
36514
37446
  if self.spellcheck_enabled:
36515
- self.spellcheck_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
37447
+ self.spellcheck_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 3px 5px;")
36516
37448
  else:
36517
- self.spellcheck_btn.setStyleSheet("background-color: #9E9E9E; color: white; font-weight: bold;")
37449
+ self.spellcheck_btn.setStyleSheet("background-color: #9E9E9E; color: white; font-weight: bold; padding: 3px 5px;")
36518
37450
 
36519
37451
  def _toggle_spellcheck(self, checked=None):
36520
37452
  """Toggle spellcheck on/off"""
@@ -37214,7 +38146,154 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37214
38146
  self.table.setUpdatesEnabled(True)
37215
38147
 
37216
38148
  self.log(f"🔍 Advanced filters: showing {visible_count} of {len(self.current_project.segments)} segments")
37217
-
38149
+
38150
+ def apply_sort(self, sort_type: str = None):
38151
+ """Sort segments by various criteria (similar to memoQ)"""
38152
+ if not self.current_project or not hasattr(self, 'table') or self.table is None:
38153
+ return
38154
+
38155
+ if not self.current_project.segments:
38156
+ return
38157
+
38158
+ # Show progress dialog during sorting
38159
+ from PyQt6.QtWidgets import QProgressDialog
38160
+ from PyQt6.QtCore import Qt
38161
+
38162
+ progress = QProgressDialog("Sorting segments, please wait...", None, 0, 0, self)
38163
+ progress.setWindowTitle("Sorting")
38164
+ progress.setWindowModality(Qt.WindowModality.WindowModal)
38165
+ progress.setMinimumDuration(0) # Show immediately
38166
+ progress.show()
38167
+ QApplication.processEvents() # Force UI update
38168
+
38169
+ try:
38170
+ # Store original document order if not already stored
38171
+ if not hasattr(self, '_original_segment_order'):
38172
+ self._original_segment_order = self.current_project.segments.copy()
38173
+
38174
+ # Update current sort state
38175
+ self.current_sort = sort_type
38176
+
38177
+ # If sort_type is None, restore document order
38178
+ if sort_type is None:
38179
+ # Restore document order by sorting by segment ID (original position)
38180
+ # This works even if the stored original order is wrong
38181
+ self.current_project.segments.sort(key=lambda seg: int(seg.id))
38182
+
38183
+ # Update stored original order to this correct order
38184
+ self._original_segment_order = self.current_project.segments.copy()
38185
+
38186
+ # Set pagination to "All" to show all segments
38187
+ if hasattr(self, 'page_size_combo') and self._widget_is_alive(self.page_size_combo):
38188
+ self.page_size_combo.blockSignals(True)
38189
+ self.page_size_combo.setCurrentText("All")
38190
+ self.page_size_combo.blockSignals(False)
38191
+ # Update the internal page size variable
38192
+ if hasattr(self, 'grid_page_size'):
38193
+ self.grid_page_size = 999999
38194
+
38195
+ self.load_segments_to_grid()
38196
+ self.log("↩️ Restored document order (showing all segments)")
38197
+ return
38198
+
38199
+ # Helper function to get text without tags for more accurate sorting
38200
+ def strip_tags(text: str) -> str:
38201
+ """Remove HTML/XML tags from text for sorting"""
38202
+ import re
38203
+ return re.sub(r'<[^>]+>', '', text).strip()
38204
+
38205
+ # Calculate frequency maps if needed
38206
+ frequency_cache = {}
38207
+ if 'freq' in sort_type:
38208
+ from collections import Counter
38209
+ if 'source' in sort_type:
38210
+ counter = Counter(strip_tags(seg.source).lower() for seg in self.current_project.segments)
38211
+ frequency_cache = {strip_tags(seg.source).lower(): counter[strip_tags(seg.source).lower()]
38212
+ for seg in self.current_project.segments}
38213
+ else: # target frequency
38214
+ counter = Counter(strip_tags(seg.target).lower() for seg in self.current_project.segments if seg.target)
38215
+ frequency_cache = {strip_tags(seg.target).lower(): counter[strip_tags(seg.target).lower()]
38216
+ for seg in self.current_project.segments if seg.target}
38217
+
38218
+ # Sort based on selected criterion
38219
+ if sort_type == 'source_asc':
38220
+ self.current_project.segments.sort(key=lambda s: strip_tags(s.source).lower())
38221
+ sort_name = "Source A → Z"
38222
+ elif sort_type == 'source_desc':
38223
+ self.current_project.segments.sort(key=lambda s: strip_tags(s.source).lower(), reverse=True)
38224
+ sort_name = "Source Z → A"
38225
+ elif sort_type == 'target_asc':
38226
+ self.current_project.segments.sort(key=lambda s: strip_tags(s.target).lower() if s.target else "")
38227
+ sort_name = "Target A → Z"
38228
+ elif sort_type == 'target_desc':
38229
+ self.current_project.segments.sort(key=lambda s: strip_tags(s.target).lower() if s.target else "", reverse=True)
38230
+ sort_name = "Target Z → A"
38231
+ elif sort_type == 'source_length_asc':
38232
+ self.current_project.segments.sort(key=lambda s: len(strip_tags(s.source)))
38233
+ sort_name = "Source (shorter first)"
38234
+ elif sort_type == 'source_length_desc':
38235
+ self.current_project.segments.sort(key=lambda s: len(strip_tags(s.source)), reverse=True)
38236
+ sort_name = "Source (longer first)"
38237
+ elif sort_type == 'target_length_asc':
38238
+ self.current_project.segments.sort(key=lambda s: len(strip_tags(s.target)) if s.target else 0)
38239
+ sort_name = "Target (shorter first)"
38240
+ elif sort_type == 'target_length_desc':
38241
+ self.current_project.segments.sort(key=lambda s: len(strip_tags(s.target)) if s.target else 0, reverse=True)
38242
+ sort_name = "Target (longer first)"
38243
+ elif sort_type == 'match_asc':
38244
+ self.current_project.segments.sort(key=lambda s: getattr(s, 'match_percent', 0) or 0)
38245
+ sort_name = "Match Rate (lower first)"
38246
+ elif sort_type == 'match_desc':
38247
+ self.current_project.segments.sort(key=lambda s: getattr(s, 'match_percent', 0) or 0, reverse=True)
38248
+ sort_name = "Match Rate (higher first)"
38249
+ elif sort_type == 'source_freq_asc':
38250
+ self.current_project.segments.sort(key=lambda s: frequency_cache.get(strip_tags(s.source).lower(), 0))
38251
+ sort_name = "Source Frequency (lower first)"
38252
+ elif sort_type == 'source_freq_desc':
38253
+ self.current_project.segments.sort(key=lambda s: frequency_cache.get(strip_tags(s.source).lower(), 0), reverse=True)
38254
+ sort_name = "Source Frequency (higher first)"
38255
+ elif sort_type == 'target_freq_asc':
38256
+ self.current_project.segments.sort(key=lambda s: frequency_cache.get(strip_tags(s.target).lower(), 0) if s.target else 0)
38257
+ sort_name = "Target Frequency (lower first)"
38258
+ elif sort_type == 'target_freq_desc':
38259
+ self.current_project.segments.sort(key=lambda s: frequency_cache.get(strip_tags(s.target).lower(), 0) if s.target else 0, reverse=True)
38260
+ sort_name = "Target Frequency (higher first)"
38261
+ elif sort_type == 'modified_asc':
38262
+ self.current_project.segments.sort(key=lambda s: s.modified_at if s.modified_at else "")
38263
+ sort_name = "Last Changed (oldest first)"
38264
+ elif sort_type == 'modified_desc':
38265
+ self.current_project.segments.sort(key=lambda s: s.modified_at if s.modified_at else "", reverse=True)
38266
+ sort_name = "Last Changed (newest first)"
38267
+ elif sort_type == 'status':
38268
+ # Sort by status in a logical order: not_started, draft, translated, confirmed
38269
+ status_order = {'not_started': 0, 'draft': 1, 'translated': 2, 'confirmed': 3}
38270
+ self.current_project.segments.sort(key=lambda s: status_order.get(s.status, 99))
38271
+ sort_name = "Row Status"
38272
+ else:
38273
+ self.log(f"⚠️ Unknown sort type: {sort_type}")
38274
+ return
38275
+
38276
+ # Set pagination to "All" to show all sorted segments
38277
+ if hasattr(self, 'page_size_combo') and self._widget_is_alive(self.page_size_combo):
38278
+ self.page_size_combo.blockSignals(True)
38279
+ self.page_size_combo.setCurrentText("All")
38280
+ self.page_size_combo.blockSignals(False)
38281
+ # Update the internal page size variable
38282
+ if hasattr(self, 'grid_page_size'):
38283
+ self.grid_page_size = 999999
38284
+
38285
+ # Reload grid to reflect new order
38286
+ self.load_segments_to_grid()
38287
+ self.log(f"⇅ Sorted by: {sort_name} (showing all segments)")
38288
+
38289
+ except Exception as e:
38290
+ self.log(f"❌ Error sorting segments: {e}")
38291
+ import traceback
38292
+ traceback.print_exc()
38293
+ finally:
38294
+ # Close progress dialog
38295
+ progress.close()
38296
+
37218
38297
  # ========================================================================
37219
38298
  # TABBED SEGMENT EDITOR METHODS (for Grid view)
37220
38299
  # ========================================================================
@@ -38673,95 +39752,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38673
39752
 
38674
39753
  self.table.clearSelection()
38675
39754
  self.table.setCurrentCell(row, 3) # Column 3 = Target (widget column)
39755
+ self.table.selectRow(row) # v1.9.182: Ensure row is visually selected
39756
+ # Ensure the row is visible by scrolling to it
39757
+ self.table.scrollToItem(self.table.item(row, 0), QTableWidget.ScrollHint.PositionAtCenter)
38676
39758
  self.log(f"⏭️ Moved to next unconfirmed segment {seg.id}")
38677
-
38678
- # Auto-confirm 100% TM matches if setting is enabled
38679
- if self.auto_confirm_100_percent_matches:
38680
- # Get TM matches for this segment
38681
- exact_match = None
38682
- if self.enable_tm_matching and hasattr(self, 'db_manager') and self.db_manager:
38683
- # Get activated TM IDs from project settings
38684
- activated_tm_ids = []
38685
- if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
38686
- activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
38687
-
38688
- if activated_tm_ids:
38689
- # Use get_exact_match for 100% matches instead of fuzzy search
38690
- source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
38691
- target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
38692
- exact_match = self.db_manager.get_exact_match(
38693
- seg.source,
38694
- tm_ids=activated_tm_ids,
38695
- source_lang=source_lang,
38696
- target_lang=target_lang
38697
- )
38698
-
38699
- # Check if there's a 100% match and (target is empty OR overwrite is enabled)
38700
- target_is_empty = not seg.target.strip()
38701
- can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
38702
-
38703
- if exact_match and can_auto_confirm:
38704
- match_target = exact_match.get('target_text', '')
38705
- overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
38706
- self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
38707
-
38708
- # Insert the match into the target cell
38709
- target_widget = self.table.cellWidget(row, 3)
38710
- if target_widget and match_target:
38711
- target_widget.setPlainText(match_target)
38712
- seg.target = match_target
38713
- seg.status = 'confirmed'
38714
- self.update_status_icon(row, 'confirmed')
38715
- self.project_modified = True
38716
-
38717
- # Save to TM
38718
- try:
38719
- self.save_segment_to_activated_tms(seg.source, seg.target)
38720
- self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
38721
- except Exception as e:
38722
- self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
38723
-
38724
- # Continue to the NEXT unconfirmed segment (skip this one)
38725
- for next_row in range(row + 1, self.table.rowCount()):
38726
- if next_row < len(self.current_project.segments):
38727
- next_seg = self.current_project.segments[next_row]
38728
- if next_seg.status not in ['confirmed', 'approved']:
38729
- # Check pagination
38730
- if self.table.isRowHidden(next_row):
38731
- if hasattr(self, 'grid_page_size') and hasattr(self, 'grid_current_page'):
38732
- target_page = next_row // self.grid_page_size
38733
- if target_page != self.grid_current_page:
38734
- self.grid_current_page = target_page
38735
- self._update_pagination_ui()
38736
- self._apply_pagination_to_grid()
38737
-
38738
- # ⚡ INSTANT NAVIGATION
38739
- self._ctrl_enter_navigation = True
38740
-
38741
- self.table.clearSelection()
38742
- self.table.setCurrentCell(next_row, 3)
38743
- self.log(f"⏭️ Auto-skipped to next unconfirmed segment {next_seg.id}")
38744
- next_target_widget = self.table.cellWidget(next_row, 3)
38745
- if next_target_widget:
38746
- next_target_widget.setFocus()
38747
- next_target_widget.moveCursor(QTextCursor.MoveOperation.End)
38748
-
38749
- # Recursively check if this next segment also has a 100% match
38750
- self.confirm_and_next_unconfirmed()
38751
- return
38752
-
38753
- # No more unconfirmed segments after this one
38754
- self.log("✅ No more unconfirmed segments after auto-confirm")
38755
- # Update status bar after auto-confirming
38756
- self.update_progress_stats()
38757
- return
38758
-
38759
- # Get the target cell widget and set focus to it (normal behavior without auto-confirm)
39759
+
39760
+ # v1.9.182: Explicitly update termview (don't rely on deferred signal)
39761
+ self._update_termview_for_segment(seg)
39762
+
39763
+ # v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
39764
+ if self.enable_tm_matching:
39765
+ find_replace_active = getattr(self, 'find_replace_active', False)
39766
+ if not find_replace_active:
39767
+ self._schedule_mt_and_llm_matches(seg, [])
39768
+
39769
+ # Get the target cell widget and set focus to it IMMEDIATELY
39770
+ # (moved BEFORE auto-confirm check for instant responsiveness)
38760
39771
  target_widget = self.table.cellWidget(row, 3)
38761
39772
  if target_widget:
38762
39773
  target_widget.setFocus()
38763
39774
  # Move cursor to end of text
38764
39775
  target_widget.moveCursor(QTextCursor.MoveOperation.End)
39776
+
39777
+ # v1.9.182: Defer auto-confirm check to not block navigation
39778
+ # The TM lookup is slow - do it asynchronously after navigation completes
39779
+ if self.auto_confirm_100_percent_matches:
39780
+ QTimer.singleShot(50, lambda r=row, s=seg: self._check_auto_confirm_100_percent(r, s))
38765
39781
  return
38766
39782
 
38767
39783
  # No more unconfirmed segments, just go to next
@@ -38783,14 +39799,106 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38783
39799
 
38784
39800
  self.table.clearSelection()
38785
39801
  self.table.setCurrentCell(next_row, 3) # Column 3 = Target (widget column)
39802
+ self.table.selectRow(next_row) # v1.9.182: Ensure row is visually selected
39803
+ # Ensure the row is visible by scrolling to it
39804
+ self.table.scrollToItem(self.table.item(next_row, 0), QTableWidget.ScrollHint.PositionAtCenter)
38786
39805
  self.log(f"⏭️ Moved to next segment (all remaining confirmed)")
39806
+
39807
+ # v1.9.182: Explicitly update termview (don't rely on deferred signal)
39808
+ if next_row < len(self.current_project.segments):
39809
+ next_seg = self.current_project.segments[next_row]
39810
+ self._update_termview_for_segment(next_seg)
39811
+
39812
+ # v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
39813
+ if self.enable_tm_matching:
39814
+ find_replace_active = getattr(self, 'find_replace_active', False)
39815
+ if not find_replace_active:
39816
+ self._schedule_mt_and_llm_matches(next_seg, [])
39817
+
38787
39818
  # Get the target cell widget and set focus to it
38788
39819
  target_widget = self.table.cellWidget(next_row, 3)
38789
39820
  if target_widget:
38790
39821
  target_widget.setFocus()
38791
39822
  # Move cursor to end of text
38792
39823
  target_widget.moveCursor(QTextCursor.MoveOperation.End)
38793
-
39824
+
39825
+ def _check_auto_confirm_100_percent(self, row: int, seg):
39826
+ """
39827
+ v1.9.182: Deferred auto-confirm check for 100% TM matches.
39828
+
39829
+ This is called asynchronously after Ctrl+Enter navigation to avoid blocking
39830
+ the UI thread with slow TM database queries.
39831
+ """
39832
+ try:
39833
+ # Verify we're still on the same segment (user may have navigated away)
39834
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
39835
+ if current_row != row:
39836
+ return # User has moved - don't auto-confirm wrong segment
39837
+
39838
+ if not self.enable_tm_matching or not hasattr(self, 'db_manager') or not self.db_manager:
39839
+ return
39840
+
39841
+ # Get activated TM IDs from project settings
39842
+ activated_tm_ids = []
39843
+ if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
39844
+ activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
39845
+
39846
+ if not activated_tm_ids:
39847
+ return
39848
+
39849
+ # Use get_exact_match for 100% matches
39850
+ source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
39851
+ target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
39852
+ exact_match = self.db_manager.get_exact_match(
39853
+ seg.source,
39854
+ tm_ids=activated_tm_ids,
39855
+ source_lang=source_lang,
39856
+ target_lang=target_lang
39857
+ )
39858
+
39859
+ if not exact_match:
39860
+ return
39861
+
39862
+ # Check if there's a 100% match and (target is empty OR overwrite is enabled)
39863
+ target_is_empty = not seg.target.strip()
39864
+ can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
39865
+
39866
+ if not can_auto_confirm:
39867
+ return
39868
+
39869
+ # Verify AGAIN that we're still on the same segment (TM query may have taken time)
39870
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
39871
+ if current_row != row:
39872
+ return # User has moved during TM lookup
39873
+
39874
+ match_target = exact_match.get('target_text', '')
39875
+ if not match_target:
39876
+ return
39877
+
39878
+ overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
39879
+ self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
39880
+
39881
+ # Insert the match into the target cell
39882
+ target_widget = self.table.cellWidget(row, 3)
39883
+ if target_widget:
39884
+ target_widget.setPlainText(match_target)
39885
+ seg.target = match_target
39886
+ seg.status = 'confirmed'
39887
+ self.update_status_icon(row, 'confirmed')
39888
+ self.project_modified = True
39889
+
39890
+ # Save to TM
39891
+ try:
39892
+ self.save_segment_to_activated_tms(seg.source, seg.target)
39893
+ self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
39894
+ except Exception as e:
39895
+ self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
39896
+
39897
+ # Continue to the NEXT unconfirmed segment (skip this one)
39898
+ self.confirm_and_next_unconfirmed()
39899
+ except Exception as e:
39900
+ self.log(f"⚠️ Error in auto-confirm check: {e}")
39901
+
38794
39902
  def confirm_selected_or_next(self):
38795
39903
  """Smart confirm: if multiple segments selected, confirm all; otherwise confirm and go to next.
38796
39904
 
@@ -39179,32 +40287,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
39179
40287
 
39180
40288
  def _toggle_tag_view_via_shortcut(self):
39181
40289
  """Toggle tag view using keyboard shortcut (Ctrl+Alt+T)"""
39182
- if hasattr(self, 'tag_view_btn'):
39183
- # Toggle the button state (which triggers toggle_tag_view)
39184
- new_state = not self.tag_view_btn.isChecked()
39185
- self.tag_view_btn.setChecked(new_state)
39186
- self.toggle_tag_view(new_state, self.tag_view_btn)
40290
+ if hasattr(self, 'wysiwyg_btn') and hasattr(self, 'tags_btn'):
40291
+ # Toggle between the two modes
40292
+ new_state = not self.show_tags
40293
+ self.toggle_tag_view(new_state, None)
39187
40294
 
39188
40295
  def _enable_tag_view_after_import(self):
39189
40296
  """Auto-enable Tag View after importing a document with formatting tags"""
39190
- if hasattr(self, 'tag_view_btn'):
39191
- self.tag_view_btn.setChecked(True)
39192
- self.toggle_tag_view(True, self.tag_view_btn)
40297
+ if hasattr(self, 'tags_btn'):
40298
+ self.toggle_tag_view(True, None)
39193
40299
  self.log("🏷️ Tag View auto-enabled (formatting tags detected in import)")
39194
40300
 
39195
40301
  def toggle_tag_view(self, checked: bool, button: QPushButton = None):
39196
40302
  """Toggle between Tag View (showing raw tags) and WYSIWYG View (formatted display)"""
39197
40303
  self.show_tags = checked
39198
-
39199
- # Update button text
39200
- if button:
40304
+
40305
+ # Update segmented control buttons if they exist
40306
+ if hasattr(self, 'wysiwyg_btn') and hasattr(self, 'tags_btn'):
39201
40307
  if checked:
39202
- button.setText("🏷️ Tags ON")
40308
+ self.tags_btn.setChecked(True)
39203
40309
  else:
39204
- button.setText("🏷️ Tags OFF")
39205
-
40310
+ self.wysiwyg_btn.setChecked(True)
40311
+
39206
40312
  self.log(f"{'🏷️ Tag View ENABLED - showing raw tags' if checked else '✨ WYSIWYG View ENABLED - showing formatted text'}")
39207
-
40313
+
39208
40314
  # Refresh the grid to update display
39209
40315
  if hasattr(self, 'table') and self.current_project:
39210
40316
  self._refresh_grid_display_mode()
@@ -40577,45 +41683,12 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
40577
41683
  event.accept()
40578
41684
 
40579
41685
  def _cleanup_web_views(self):
40580
- """Clean up WebEngine views to prevent 'Release of profile requested' warning"""
40581
- try:
40582
- # Close any SuperBrowser tabs
40583
- if hasattr(self, 'superbrowser_tabs'):
40584
- for tab in self.superbrowser_tabs.values():
40585
- try:
40586
- if hasattr(tab, 'web_view'):
40587
- # Stop any loading/rendering
40588
- tab.web_view.stop()
40589
- # Clear page to release resources
40590
- tab.web_view.setPage(None)
40591
- tab.web_view.setUrl(QUrl('about:blank'))
40592
- tab.web_view.deleteLater()
40593
- except:
40594
- pass
40595
-
40596
- # Close Superlookup web views (from Web Resources tab)
40597
- if hasattr(self, 'web_views'):
40598
- for resource_id, web_view in list(self.web_views.items()):
40599
- try:
40600
- web_view.stop()
40601
- web_view.setPage(None)
40602
- web_view.setUrl(QUrl('about:blank'))
40603
- web_view.deleteLater()
40604
- except:
40605
- pass
40606
- self.web_views.clear()
40607
-
40608
- # Process events multiple times to ensure cleanup completes
40609
- from PyQt6.QtWidgets import QApplication
40610
- from PyQt6.QtCore import QUrl
40611
- for _ in range(3):
40612
- QApplication.processEvents()
40613
-
40614
- # Small delay to allow Qt to finish cleanup
40615
- import time
40616
- time.sleep(0.1)
40617
- except:
40618
- pass
41686
+ """Clean up WebEngine views - DISABLED to prevent crash"""
41687
+ # WebEngine cleanup has been disabled because it was causing Python crashes
41688
+ # on program exit. Qt will handle WebEngine cleanup automatically, though
41689
+ # you may see a "Release of profile requested" warning which is harmless.
41690
+ print("[WebEngine Cleanup] Skipping manual cleanup - letting Qt handle it")
41691
+ pass
40619
41692
 
40620
41693
  def _close_detached_log_windows(self):
40621
41694
  """Close all detached log windows when main window closes"""
@@ -43160,23 +44233,58 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43160
44233
  """Call DeepL API"""
43161
44234
  try:
43162
44235
  import deepl
43163
-
44236
+
43164
44237
  if not api_key:
43165
44238
  api_keys = self.load_api_keys()
43166
44239
  api_key = api_keys.get("deepl")
43167
-
44240
+
43168
44241
  if not api_key:
43169
44242
  return "[DeepL requires API key]"
43170
-
44243
+
43171
44244
  translator = deepl.Translator(api_key)
43172
-
43173
- # Convert language codes (DeepL uses uppercase)
44245
+
44246
+ # Convert source language code (DeepL uses uppercase, no variant needed for source)
43174
44247
  src_code = source_lang.split('-')[0].split('_')[0].upper()
43175
- tgt_code = target_lang.split('-')[0].split('_')[0].upper()
43176
-
44248
+
44249
+ # Convert target language code - DeepL requires variants for some languages
44250
+ # Handle full codes like "en-US", "en-GB", "pt-BR", "pt-PT"
44251
+ tgt_upper = target_lang.upper().replace('_', '-')
44252
+
44253
+ # DeepL target language mapping - some require specific variants
44254
+ deepl_target_map = {
44255
+ # English variants (EN alone is deprecated)
44256
+ 'EN': 'EN-US', # Default to US English
44257
+ 'EN-US': 'EN-US',
44258
+ 'EN-GB': 'EN-GB',
44259
+ 'EN-AU': 'EN-GB', # Map Australian to British
44260
+ 'EN-CA': 'EN-US', # Map Canadian to US
44261
+ # Portuguese variants
44262
+ 'PT': 'PT-PT', # Default to European Portuguese
44263
+ 'PT-PT': 'PT-PT',
44264
+ 'PT-BR': 'PT-BR',
44265
+ # Chinese variants
44266
+ 'ZH': 'ZH-HANS', # Default to Simplified
44267
+ 'ZH-CN': 'ZH-HANS',
44268
+ 'ZH-TW': 'ZH-HANT',
44269
+ 'ZH-HANS': 'ZH-HANS',
44270
+ 'ZH-HANT': 'ZH-HANT',
44271
+ }
44272
+
44273
+ # Check if full code matches first, then base code
44274
+ if tgt_upper in deepl_target_map:
44275
+ tgt_code = deepl_target_map[tgt_upper]
44276
+ else:
44277
+ # Extract base code and check
44278
+ base_code = tgt_upper.split('-')[0]
44279
+ if base_code in deepl_target_map:
44280
+ tgt_code = deepl_target_map[base_code]
44281
+ else:
44282
+ # Use base code as-is for other languages
44283
+ tgt_code = base_code
44284
+
43177
44285
  result = translator.translate_text(text, source_lang=src_code, target_lang=tgt_code)
43178
44286
  return result.text
43179
-
44287
+
43180
44288
  except ImportError:
43181
44289
  return "[DeepL requires: pip install deepl]"
43182
44290
  except Exception as e:
@@ -43608,14 +44716,25 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43608
44716
  # Reapply alternating row colors with new theme
43609
44717
  if hasattr(self, 'apply_alternating_row_colors'):
43610
44718
  self.apply_alternating_row_colors()
43611
-
44719
+
44720
+ # Update navigation arrow colors based on theme
44721
+ if hasattr(self, 'theme_aware_arrows'):
44722
+ is_dark = theme.name == "Dark"
44723
+ arrow_color = "#FFFFFF" if is_dark else "#333333"
44724
+ for arrow in self.theme_aware_arrows:
44725
+ if hasattr(arrow, 'set_color'):
44726
+ arrow.set_color(arrow_color)
44727
+
43612
44728
  # Refresh segment numbers color
43613
44729
  if hasattr(self, 'table') and self.table:
43614
44730
  # Determine segment number color based on theme
43615
44731
  is_dark_theme = theme.name == "Dark"
43616
44732
  segment_num_color = theme.text if is_dark_theme else "black"
43617
-
44733
+
43618
44734
  for row in range(self.table.rowCount()):
44735
+ # Keep UI responsive during large grid updates
44736
+ if row % 50 == 0:
44737
+ QApplication.processEvents()
43619
44738
  id_item = self.table.item(row, 0)
43620
44739
  if id_item:
43621
44740
  # Don't change currently highlighted row (orange background)
@@ -43645,6 +44764,11 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43645
44764
  if hasattr(self.termview_widget, 'apply_theme'):
43646
44765
  self.termview_widget.apply_theme()
43647
44766
 
44767
+ # Also refresh Match Panel TermView (right panel)
44768
+ if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
44769
+ if hasattr(self.termview_widget_match, 'apply_theme'):
44770
+ self.termview_widget_match.apply_theme()
44771
+
43648
44772
  def show_file_progress_dialog(self):
43649
44773
  """Show File Progress dialog for multi-file projects.
43650
44774
 
@@ -43941,12 +45065,12 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43941
45065
  self._pending_mt_llm_segment = segment
43942
45066
  self._pending_termbase_matches = termbase_matches or []
43943
45067
 
43944
- # Start debounced timer - only call APIs after user stops clicking for 0.3 seconds
45068
+ # Start debounced timer - only call APIs after user stops navigating
43945
45069
  from PyQt6.QtCore import QTimer
43946
45070
  self._mt_llm_timer = QTimer()
43947
45071
  self._mt_llm_timer.setSingleShot(True)
43948
45072
  self._mt_llm_timer.timeout.connect(lambda: self._execute_mt_llm_lookup())
43949
- self._mt_llm_timer.start(300) # Wait 0.3 seconds of inactivity
45073
+ self._mt_llm_timer.start(150) # Wait 150ms of inactivity before external API calls
43950
45074
 
43951
45075
  except Exception as e:
43952
45076
  self.log(f"Error scheduling MT/LLM search: {e}")
@@ -43971,7 +45095,21 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43971
45095
  """Search for TM, MT and LLM matches - called only after debounce delay"""
43972
45096
  try:
43973
45097
  from modules.translation_results_panel import TranslationMatch
43974
-
45098
+
45099
+ # v1.9.182: Validate we're still on the same segment before displaying results
45100
+ # This prevents stale results from showing when user navigates quickly
45101
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
45102
+ if current_row >= 0:
45103
+ id_item = self.table.item(current_row, 0)
45104
+ if id_item:
45105
+ try:
45106
+ current_segment_id = int(id_item.text())
45107
+ if current_segment_id != segment.id:
45108
+ # User has moved to a different segment - abort this lookup
45109
+ return
45110
+ except (ValueError, AttributeError):
45111
+ pass
45112
+
43975
45113
  # Get current project languages for all translation services
43976
45114
  source_lang = getattr(self.current_project, 'source_lang', None) if self.current_project else None
43977
45115
  target_lang = getattr(self.current_project, 'target_lang', None) if self.current_project else None
@@ -44044,6 +45182,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44044
45182
 
44045
45183
  # Show TM matches immediately (progressive loading)
44046
45184
  if matches_dict["TM"]:
45185
+ # v1.9.182: Re-validate we're still on same segment before displaying
45186
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
45187
+ if current_row >= 0:
45188
+ id_item = self.table.item(current_row, 0)
45189
+ if id_item:
45190
+ try:
45191
+ current_segment_id = int(id_item.text())
45192
+ if current_segment_id != segment.id:
45193
+ # User moved - still cache results but don't display
45194
+ with self.translation_matches_cache_lock:
45195
+ if segment.id in self.translation_matches_cache:
45196
+ self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
45197
+ return # Don't display stale results
45198
+ except (ValueError, AttributeError):
45199
+ pass
45200
+
44047
45201
  tm_only = {"TM": matches_dict["TM"]}
44048
45202
  if hasattr(self, 'results_panels') and self.results_panels:
44049
45203
  for panel in self.results_panels:
@@ -44097,6 +45251,16 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44097
45251
  has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in matches_dict["TM"])
44098
45252
  if has_fuzzy_match and not has_100_match:
44099
45253
  self._play_sound_effect('tm_fuzzy_match')
45254
+
45255
+ # v1.9.182: Update cache with TM results so subsequent visits are instant
45256
+ if matches_dict["TM"]:
45257
+ with self.translation_matches_cache_lock:
45258
+ if segment.id in self.translation_matches_cache:
45259
+ # Merge TM results into existing cache entry
45260
+ self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
45261
+ else:
45262
+ # Create new cache entry with TM results
45263
+ self.translation_matches_cache[segment.id] = matches_dict
44100
45264
  except Exception as e:
44101
45265
  self.log(f"Error in delayed TM search: {e}")
44102
45266
 
@@ -44659,7 +45823,7 @@ class SuperlookupTab(QWidget):
44659
45823
  self.main_window = parent # Store reference to main window for database access
44660
45824
  self.user_data_path = user_data_path # Store user data path for web cache
44661
45825
 
44662
- print("[Superlookup] SuperlookupTab.__init__ called")
45826
+ print("[SuperLookup] SuperlookupTab.__init__ called")
44663
45827
 
44664
45828
  # Get theme manager from main window (try parent first, then parent's parent for dialogs)
44665
45829
  self.theme_manager = getattr(parent, 'theme_manager', None)
@@ -44668,16 +45832,16 @@ class SuperlookupTab(QWidget):
44668
45832
  parent_parent = getattr(parent, 'parent', lambda: None)()
44669
45833
  if parent_parent:
44670
45834
  self.theme_manager = getattr(parent_parent, 'theme_manager', None)
44671
- print(f"[Superlookup] theme_manager: {self.theme_manager is not None}")
45835
+ print(f"[SuperLookup] theme_manager: {self.theme_manager is not None}")
44672
45836
 
44673
45837
  # Import lookup engine
44674
45838
  try:
44675
45839
  from modules.superlookup import SuperlookupEngine, LookupResult
44676
45840
  self.SuperlookupEngine = SuperlookupEngine
44677
45841
  self.LookupResult = LookupResult
44678
- print("[Superlookup] Successfully imported SuperlookupEngine")
45842
+ print("[SuperLookup] Successfully imported SuperlookupEngine")
44679
45843
  except ImportError as e:
44680
- print(f"[Superlookup] IMPORT ERROR: {e}")
45844
+ print(f"[SuperLookup] IMPORT ERROR: {e}")
44681
45845
  QMessageBox.critical(
44682
45846
  self,
44683
45847
  "Missing Module",
@@ -44759,7 +45923,7 @@ class SuperlookupTab(QWidget):
44759
45923
  if self.db_manager or self.termbase_mgr:
44760
45924
  self.populate_language_dropdowns()
44761
45925
  self._languages_populated = True
44762
- print("[Superlookup] Languages populated on first show")
45926
+ print("[SuperLookup] Languages populated on first show")
44763
45927
 
44764
45928
  def init_ui(self):
44765
45929
  """Initialize the UI"""
@@ -44768,7 +45932,7 @@ class SuperlookupTab(QWidget):
44768
45932
  layout.setSpacing(5) # Reduced from 10 to 5 for consistency
44769
45933
 
44770
45934
  # Header
44771
- header = QLabel("🔍 Superlookup")
45935
+ header = QLabel("🔍 SuperLookup")
44772
45936
  header.setStyleSheet("font-size: 16pt; font-weight: bold; color: #1976D2;")
44773
45937
  layout.addWidget(header, 0) # 0 = no stretch, stays compact
44774
45938
 
@@ -45262,7 +46426,7 @@ class SuperlookupTab(QWidget):
45262
46426
  })
45263
46427
 
45264
46428
  except Exception as e:
45265
- print(f"[Superlookup] MT error ({provider_name}): {e}")
46429
+ print(f"[SuperLookup] MT error ({provider_name}): {e}")
45266
46430
  results.append({
45267
46431
  'provider': provider_name,
45268
46432
  'translation': f"[Error: {str(e)}]",
@@ -45353,9 +46517,9 @@ class SuperlookupTab(QWidget):
45353
46517
  self.web_profile = QWebEngineProfile("SuperlookupProfile", self)
45354
46518
  self.web_profile.setPersistentStoragePath(storage_path)
45355
46519
  self.web_profile.setPersistentCookiesPolicy(QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies)
45356
- print(f"[Superlookup] QWebEngineView available - embedded browser enabled with persistent storage at {storage_path}")
46520
+ print(f"[SuperLookup] QWebEngineView available - embedded browser enabled with persistent storage at {storage_path}")
45357
46521
  except ImportError:
45358
- print("[Superlookup] QWebEngineView not available - external browser only")
46522
+ print("[SuperLookup] QWebEngineView not available - external browser only")
45359
46523
  self.QWebEngineView = None
45360
46524
  self.QWebEngineProfile = None
45361
46525
 
@@ -45807,7 +46971,7 @@ class SuperlookupTab(QWidget):
45807
46971
  self.web_view_stack.addWidget(web_view)
45808
46972
  self.web_views[resource['id']] = web_view
45809
46973
 
45810
- print(f"[Superlookup] Created web view for {resource['name']} (lazy load)")
46974
+ print(f"[SuperLookup] Created web view for {resource['name']} (lazy load)")
45811
46975
 
45812
46976
  def _get_web_view_index(self, resource_id):
45813
46977
  """Get the stack index for a web view by resource ID"""
@@ -45824,7 +46988,7 @@ class SuperlookupTab(QWidget):
45824
46988
 
45825
46989
  self._update_web_view_for_mode()
45826
46990
  self._show_web_welcome_message()
45827
- print(f"[Superlookup] Web browser mode changed to: {self.web_browser_mode}")
46991
+ print(f"[SuperLookup] Web browser mode changed to: {self.web_browser_mode}")
45828
46992
 
45829
46993
  def _update_web_view_for_mode(self):
45830
46994
  """Update the view stack based on current mode"""
@@ -46457,11 +47621,11 @@ class SuperlookupTab(QWidget):
46457
47621
  if index == 2:
46458
47622
  # Initialize Supermemory when tab is first viewed
46459
47623
  if not self.supermemory_engine:
46460
- print("[Superlookup] Supermemory tab viewed - initializing engine")
47624
+ print("[SuperLookup] Supermemory tab viewed - initializing engine")
46461
47625
  self.init_supermemory()
46462
47626
  # Settings tab is at index 5
46463
47627
  elif index == 5:
46464
- print("[Superlookup] Settings tab viewed - refreshing resource lists")
47628
+ print("[SuperLookup] Settings tab viewed - refreshing resource lists")
46465
47629
  self.refresh_tm_list()
46466
47630
  self.refresh_termbase_list()
46467
47631
  self.populate_language_dropdowns()
@@ -46469,19 +47633,19 @@ class SuperlookupTab(QWidget):
46469
47633
  def on_tm_search_toggled(self, state):
46470
47634
  """Handle TM search checkbox toggle"""
46471
47635
  self.search_tm_enabled = (state == Qt.CheckState.Checked.value)
46472
- print(f"[Superlookup] TM search {'enabled' if self.search_tm_enabled else 'disabled'}")
47636
+ print(f"[SuperLookup] TM search {'enabled' if self.search_tm_enabled else 'disabled'}")
46473
47637
 
46474
47638
  def on_termbase_search_toggled(self, state):
46475
47639
  """Handle termbase search checkbox toggle"""
46476
47640
  self.search_termbase_enabled = (state == Qt.CheckState.Checked.value)
46477
- print(f"[Superlookup] Termbase search {'enabled' if self.search_termbase_enabled else 'disabled'}")
47641
+ print(f"[SuperLookup] Termbase search {'enabled' if self.search_termbase_enabled else 'disabled'}")
46478
47642
 
46479
47643
  def _on_web_resource_checkbox_changed(self, index: int, state: int):
46480
47644
  """Handle web resource checkbox change - show/hide corresponding sidebar button"""
46481
47645
  is_checked = (state == Qt.CheckState.Checked.value)
46482
47646
  if hasattr(self, 'web_resource_buttons') and index < len(self.web_resource_buttons):
46483
47647
  self.web_resource_buttons[index].setVisible(is_checked)
46484
- print(f"[Superlookup] Web resource {index} {'shown' if is_checked else 'hidden'}")
47648
+ print(f"[SuperLookup] Web resource {index} {'shown' if is_checked else 'hidden'}")
46485
47649
 
46486
47650
  # If the hidden resource was selected, select the first visible one
46487
47651
  if not is_checked and hasattr(self, 'current_web_resource_index') and self.current_web_resource_index == index:
@@ -46499,18 +47663,18 @@ class SuperlookupTab(QWidget):
46499
47663
  checkbox.deleteLater()
46500
47664
  self.tm_checkboxes.clear()
46501
47665
 
46502
- print(f"[Superlookup] refresh_tm_list called")
46503
- print(f"[Superlookup] main_window exists: {self.main_window is not None}")
47666
+ print(f"[SuperLookup] refresh_tm_list called")
47667
+ print(f"[SuperLookup] main_window exists: {self.main_window is not None}")
46504
47668
 
46505
47669
  # Get TMs from main window's database
46506
47670
  if self.main_window and hasattr(self.main_window, 'db_manager') and self.main_window.db_manager:
46507
47671
  try:
46508
- print(f"[Superlookup] db_manager found, querying TMs...")
47672
+ print(f"[SuperLookup] db_manager found, querying TMs...")
46509
47673
  cursor = self.main_window.db_manager.cursor
46510
47674
  cursor.execute("SELECT id, name, tm_id FROM translation_memories ORDER BY name")
46511
47675
  tms = cursor.fetchall()
46512
47676
 
46513
- print(f"[Superlookup] Query returned {len(tms)} TMs")
47677
+ print(f"[SuperLookup] Query returned {len(tms)} TMs")
46514
47678
 
46515
47679
  for db_id, tm_name, tm_id_str in tms:
46516
47680
  checkbox = CheckmarkCheckBox(f"{tm_name} (ID: {db_id})")
@@ -46521,13 +47685,13 @@ class SuperlookupTab(QWidget):
46521
47685
  # Insert before the stretch at the end
46522
47686
  self.tm_scroll_layout.insertWidget(len(self.tm_checkboxes) - 1, checkbox)
46523
47687
 
46524
- print(f"[Superlookup] ✓ Loaded {len(tms)} TMs")
47688
+ print(f"[SuperLookup] ✓ Loaded {len(tms)} TMs")
46525
47689
  except Exception as e:
46526
- print(f"[Superlookup] ✗ Error loading TMs: {e}")
47690
+ print(f"[SuperLookup] ✗ Error loading TMs: {e}")
46527
47691
  import traceback
46528
47692
  traceback.print_exc()
46529
47693
  else:
46530
- print(f"[Superlookup] db_manager not available")
47694
+ print(f"[SuperLookup] db_manager not available")
46531
47695
  # Add placeholder label
46532
47696
  placeholder = QLabel("No database connection - TMs unavailable")
46533
47697
  placeholder.setStyleSheet("color: #999; font-style: italic;")
@@ -46541,16 +47705,16 @@ class SuperlookupTab(QWidget):
46541
47705
  checkbox.deleteLater()
46542
47706
  self.tb_checkboxes.clear()
46543
47707
 
46544
- print(f"[Superlookup] refresh_termbase_list called")
46545
- print(f"[Superlookup] main_window exists: {self.main_window is not None}")
47708
+ print(f"[SuperLookup] refresh_termbase_list called")
47709
+ print(f"[SuperLookup] main_window exists: {self.main_window is not None}")
46546
47710
 
46547
47711
  # Try termbase_mgr first (preferred method)
46548
47712
  if self.main_window and hasattr(self.main_window, 'termbase_mgr') and self.main_window.termbase_mgr:
46549
47713
  try:
46550
- print(f"[Superlookup] termbase_mgr found, querying termbases...")
47714
+ print(f"[SuperLookup] termbase_mgr found, querying termbases...")
46551
47715
  termbases = self.main_window.termbase_mgr.get_all_termbases()
46552
47716
 
46553
- print(f"[Superlookup] get_all_termbases() returned {len(termbases)} termbases")
47717
+ print(f"[SuperLookup] get_all_termbases() returned {len(termbases)} termbases")
46554
47718
 
46555
47719
  for tb in termbases:
46556
47720
  tb_id = tb.get('id')
@@ -46562,22 +47726,22 @@ class SuperlookupTab(QWidget):
46562
47726
  # Insert before the stretch at the end
46563
47727
  self.tb_scroll_layout.insertWidget(len(self.tb_checkboxes) - 1, checkbox)
46564
47728
 
46565
- print(f"[Superlookup] ✓ Loaded {len(termbases)} termbases via termbase_mgr")
47729
+ print(f"[SuperLookup] ✓ Loaded {len(termbases)} termbases via termbase_mgr")
46566
47730
  return
46567
47731
  except Exception as e:
46568
- print(f"[Superlookup] ✗ Error loading termbases via termbase_mgr: {e}")
47732
+ print(f"[SuperLookup] ✗ Error loading termbases via termbase_mgr: {e}")
46569
47733
  import traceback
46570
47734
  traceback.print_exc()
46571
47735
 
46572
47736
  # Fallback to direct database query
46573
47737
  if self.main_window and hasattr(self.main_window, 'db_manager') and self.main_window.db_manager:
46574
47738
  try:
46575
- print(f"[Superlookup] db_manager found, querying termbases...")
47739
+ print(f"[SuperLookup] db_manager found, querying termbases...")
46576
47740
  cursor = self.main_window.db_manager.cursor
46577
47741
  cursor.execute("SELECT id, name FROM termbases ORDER BY name")
46578
47742
  termbases = cursor.fetchall()
46579
47743
 
46580
- print(f"[Superlookup] Query returned {len(termbases)} termbases")
47744
+ print(f"[SuperLookup] Query returned {len(termbases)} termbases")
46581
47745
 
46582
47746
  for tb_id, tb_name in termbases:
46583
47747
  checkbox = CheckmarkCheckBox(f"{tb_name} (ID: {tb_id})")
@@ -46587,13 +47751,13 @@ class SuperlookupTab(QWidget):
46587
47751
  # Insert before the stretch at the end
46588
47752
  self.tb_scroll_layout.insertWidget(len(self.tb_checkboxes) - 1, checkbox)
46589
47753
 
46590
- print(f"[Superlookup] ✓ Loaded {len(termbases)} termbases via db_manager")
47754
+ print(f"[SuperLookup] ✓ Loaded {len(termbases)} termbases via db_manager")
46591
47755
  except Exception as e:
46592
- print(f"[Superlookup] ✗ Error loading termbases via db_manager: {e}")
47756
+ print(f"[SuperLookup] ✗ Error loading termbases via db_manager: {e}")
46593
47757
  import traceback
46594
47758
  traceback.print_exc()
46595
47759
  else:
46596
- print(f"[Superlookup] Neither termbase_mgr nor db_manager available")
47760
+ print(f"[SuperLookup] Neither termbase_mgr nor db_manager available")
46597
47761
  # Add placeholder label
46598
47762
  placeholder = QLabel("No database connection - Glossaries unavailable")
46599
47763
  placeholder.setStyleSheet("color: #999; font-style: italic;")
@@ -46675,10 +47839,8 @@ class SuperlookupTab(QWidget):
46675
47839
  for row in db_manager.cursor.fetchall():
46676
47840
  if row[0]:
46677
47841
  all_languages.add(row[0])
46678
- except Exception as e:
46679
- print(f"[DEBUG] Error getting languages from TMs: {e}")
46680
- else:
46681
- print(f"[DEBUG] No db_manager available for language population")
47842
+ except Exception:
47843
+ pass # Silent failure for language population
46682
47844
 
46683
47845
  # Get languages from termbases
46684
47846
  if termbase_mgr:
@@ -46689,8 +47851,8 @@ class SuperlookupTab(QWidget):
46689
47851
  all_languages.add(tb['source_lang'])
46690
47852
  if tb.get('target_lang'):
46691
47853
  all_languages.add(tb['target_lang'])
46692
- except Exception as e:
46693
- print(f"[DEBUG] Error getting languages from termbases: {e}")
47854
+ except Exception:
47855
+ pass # Silent failure for language population
46694
47856
 
46695
47857
  # Group languages by their base language name
46696
47858
  # E.g., "en", "en-US", "en-GB", "English" all map to "English"
@@ -46720,8 +47882,6 @@ class SuperlookupTab(QWidget):
46720
47882
  # Store variants list as the data for this item
46721
47883
  self.lang_from_combo.addItem(base_name, variants)
46722
47884
  self.lang_to_combo.addItem(base_name, variants)
46723
-
46724
- print(f"[DEBUG] Populated language dropdowns with {len(sorted_base_langs)} base languages (from {len(all_languages)} variants)")
46725
47885
 
46726
47886
  def _get_base_language_name(self, lang_code):
46727
47887
  """Extract the base language name from any language code or name.
@@ -46908,37 +48068,20 @@ class SuperlookupTab(QWidget):
46908
48068
  selected_tm_ids = self.get_selected_tm_ids()
46909
48069
  search_direction = self.get_search_direction()
46910
48070
  from_lang, to_lang = self.get_language_filters()
46911
-
46912
- # Write language info to debug file
46913
- with open('superlookup_debug.txt', 'a') as f:
46914
- f.write(f"Language filters: from_lang='{from_lang}', to_lang='{to_lang}'\\n")
46915
- f.write(f"Search direction: {search_direction}\\n")
46916
-
46917
- print(f"[DEBUG] Superlookup: Selected TM IDs: {selected_tm_ids}, direction: {search_direction}", flush=True)
46918
- print(f"[DEBUG] Superlookup: Language filters: from={from_lang}, to={to_lang}", flush=True)
46919
- print(f"[DEBUG] Superlookup: tm_database = {self.tm_database}", flush=True)
48071
+
46920
48072
  if self.engine:
46921
48073
  self.engine.set_enabled_tm_ids(selected_tm_ids if selected_tm_ids else None)
46922
48074
 
46923
48075
  # Perform TM lookup with direction and language filters
46924
48076
  tm_results = []
46925
48077
  if self.tm_database:
46926
- print(f"[DEBUG] Superlookup: Searching TM for '{text[:50]}...'", flush=True)
46927
- tm_results = self.engine.search_tm(text, direction=search_direction,
48078
+ tm_results = self.engine.search_tm(text, direction=search_direction,
46928
48079
  source_lang=from_lang, target_lang=to_lang)
46929
- print(f"[DEBUG] Superlookup: Got {len(tm_results)} TM results", flush=True)
46930
- else:
46931
- print(f"[DEBUG] Superlookup: tm_database is None, skipping TM search!", flush=True)
46932
-
48080
+
46933
48081
  # Perform termbase lookup (search Supervertaler termbases directly)
46934
- print(f"[DEBUG] About to call search_termbases with from_lang='{from_lang}', to_lang='{to_lang}'", flush=True)
46935
48082
  try:
46936
48083
  termbase_results = self.search_termbases(text, source_lang=from_lang, target_lang=to_lang)
46937
- print(f"[DEBUG] search_termbases returned {len(termbase_results)} results", flush=True)
46938
- except Exception as e:
46939
- print(f"[DEBUG] ERROR in search_termbases: {e}", flush=True)
46940
- import traceback
46941
- traceback.print_exc()
48084
+ except Exception:
46942
48085
  termbase_results = []
46943
48086
 
46944
48087
  # Perform Supermemory semantic search
@@ -47425,7 +48568,7 @@ class SuperlookupTab(QWidget):
47425
48568
  self.status_label.setText(f"Navigated to glossary entry: {source_term}")
47426
48569
 
47427
48570
  except Exception as e:
47428
- print(f"[Superlookup] Error navigating to glossary: {e}")
48571
+ print(f"[SuperLookup] Error navigating to glossary: {e}")
47429
48572
  self.status_label.setText(f"Error navigating to glossary: {e}")
47430
48573
 
47431
48574
  def _select_first_term_in_table(self, main):
@@ -47439,7 +48582,7 @@ class SuperlookupTab(QWidget):
47439
48582
  if item:
47440
48583
  table.scrollToItem(item)
47441
48584
  except Exception as e:
47442
- print(f"[Superlookup] Error selecting term: {e}")
48585
+ print(f"[SuperLookup] Error selecting term: {e}")
47443
48586
 
47444
48587
  def display_mt_results(self, results):
47445
48588
  """Display MT results in the table"""
@@ -47974,7 +49117,7 @@ class SuperlookupTab(QWidget):
47974
49117
  if self.main_window and hasattr(self.main_window, 'general_settings'):
47975
49118
  saved_path = self.main_window.general_settings.get('autohotkey_path', '')
47976
49119
  if saved_path and os.path.exists(saved_path):
47977
- print(f"[Superlookup] Using saved AutoHotkey path: {saved_path}")
49120
+ print(f"[SuperLookup] Using saved AutoHotkey path: {saved_path}")
47978
49121
  return saved_path, 'saved'
47979
49122
 
47980
49123
  # Standard installation paths
@@ -47992,7 +49135,7 @@ class SuperlookupTab(QWidget):
47992
49135
 
47993
49136
  for path in ahk_paths:
47994
49137
  if os.path.exists(path):
47995
- print(f"[Superlookup] Detected AutoHotkey at: {path}")
49138
+ print(f"[SuperLookup] Detected AutoHotkey at: {path}")
47996
49139
  return path, 'detected'
47997
49140
 
47998
49141
  return None, None
@@ -48119,7 +49262,7 @@ class SuperlookupTab(QWidget):
48119
49262
  if self.main_window and hasattr(self.main_window, 'general_settings'):
48120
49263
  self.main_window.general_settings['autohotkey_path'] = file_path
48121
49264
  self.main_window.save_general_settings()
48122
- print(f"[Superlookup] Saved AutoHotkey path: {file_path}")
49265
+ print(f"[SuperLookup] Saved AutoHotkey path: {file_path}")
48123
49266
 
48124
49267
  self._ahk_setup_status.setText(f"✓ Saved: {file_path}\n\nRestart Supervertaler to use this path.")
48125
49268
 
@@ -48140,11 +49283,11 @@ class SuperlookupTab(QWidget):
48140
49283
  Note: AutoHotkey is Windows-only, so skip on Linux/Mac.
48141
49284
  """
48142
49285
  global _ahk_process
48143
- print("[Superlookup] register_global_hotkey called")
49286
+ print("[SuperLookup] register_global_hotkey called")
48144
49287
 
48145
49288
  # AutoHotkey is Windows-only - skip on other platforms
48146
49289
  if sys.platform != 'win32':
48147
- print("[Superlookup] Skipping AutoHotkey registration (not Windows)")
49290
+ print("[SuperLookup] Skipping AutoHotkey registration (not Windows)")
48148
49291
  self.hotkey_registered = False
48149
49292
  return
48150
49293
 
@@ -48158,24 +49301,24 @@ class SuperlookupTab(QWidget):
48158
49301
  """
48159
49302
  try:
48160
49303
  from ahk import AHK
48161
- print("[Superlookup] ahk library available, attempting to use it...")
49304
+ print("[SuperLookup] ahk library available, attempting to use it...")
48162
49305
 
48163
49306
  # Find AutoHotkey executable using shared function
48164
49307
  ahk_exe, source = self._find_autohotkey_executable()
48165
49308
 
48166
49309
  # Create AHK instance (with executable path if found)
48167
49310
  if ahk_exe:
48168
- print(f"[Superlookup] Using AutoHotkey at: {ahk_exe} (source: {source})")
49311
+ print(f"[SuperLookup] Using AutoHotkey at: {ahk_exe} (source: {source})")
48169
49312
  self._ahk = AHK(executable_path=ahk_exe)
48170
49313
  else:
48171
49314
  # Let it try to find AHK on PATH (may fail)
48172
49315
  self._ahk = AHK()
48173
- print(f"[Superlookup] AHK instance created: {self._ahk}")
49316
+ print(f"[SuperLookup] AHK instance created: {self._ahk}")
48174
49317
 
48175
49318
  # Define hotkey callback
48176
49319
  def on_hotkey():
48177
49320
  """Called when Ctrl+Alt+L is pressed"""
48178
- print("[Superlookup] Hotkey triggered via ahk library!")
49321
+ print("[SuperLookup] Hotkey triggered via ahk library!")
48179
49322
  try:
48180
49323
  # Copy selection to clipboard
48181
49324
  self._ahk.send('^c') # Ctrl+C
@@ -48188,7 +49331,7 @@ class SuperlookupTab(QWidget):
48188
49331
  try:
48189
49332
  self._ahk.win_activate('Supervertaler')
48190
49333
  except Exception as e:
48191
- print(f"[Superlookup] win_activate error (non-critical): {e}")
49334
+ print(f"[SuperLookup] win_activate error (non-critical): {e}")
48192
49335
 
48193
49336
  # Trigger lookup in main thread
48194
49337
  if text:
@@ -48196,22 +49339,22 @@ class SuperlookupTab(QWidget):
48196
49339
  QTimer.singleShot(0, lambda: self.on_ahk_capture(text))
48197
49340
 
48198
49341
  except Exception as e:
48199
- print(f"[Superlookup] Error in hotkey callback: {e}")
49342
+ print(f"[SuperLookup] Error in hotkey callback: {e}")
48200
49343
 
48201
49344
  # Register the hotkey
48202
49345
  self._ahk.add_hotkey('^!l', callback=on_hotkey) # Ctrl+Alt+L
48203
49346
  self._ahk.start_hotkeys()
48204
49347
 
48205
- print("[Superlookup] ✓ Hotkey registered via ahk library: Ctrl+Alt+L")
49348
+ print("[SuperLookup] ✓ Hotkey registered via ahk library: Ctrl+Alt+L")
48206
49349
  self.hotkey_registered = True
48207
49350
  self._using_ahk_library = True
48208
49351
  return True
48209
49352
 
48210
49353
  except ImportError:
48211
- print("[Superlookup] ahk library not installed (pip install ahk)")
49354
+ print("[SuperLookup] ahk library not installed (pip install ahk)")
48212
49355
  return False
48213
49356
  except Exception as e:
48214
- print(f"[Superlookup] ahk library method failed: {e}")
49357
+ print(f"[SuperLookup] ahk library method failed: {e}")
48215
49358
  # Clean up on failure
48216
49359
  if hasattr(self, '_ahk'):
48217
49360
  try:
@@ -48292,11 +49435,13 @@ class SuperlookupTab(QWidget):
48292
49435
  self.hotkey_registered = False
48293
49436
 
48294
49437
  def start_file_watcher(self):
48295
- """Watch for signal file from AHK"""
49438
+ """Watch for signal files from AHK (Superlookup and MT Quick Lookup)"""
48296
49439
  self.signal_file = Path(__file__).parent / "lookup_signal.txt"
48297
49440
  self.capture_file = Path(__file__).parent / "temp_capture.txt"
48298
-
48299
- print(f"[Superlookup] File watcher started, watching: {self.signal_file}")
49441
+ self.mt_lookup_signal_file = Path(__file__).parent / "mt_lookup_signal.txt"
49442
+
49443
+ print(f"[SuperLookup] File watcher started, watching: {self.signal_file}")
49444
+ print(f"[QuickTrans] File watcher started, watching: {self.mt_lookup_signal_file}")
48300
49445
 
48301
49446
  # Create timer to check for signal file
48302
49447
  self.file_check_timer = QTimer()
@@ -48305,27 +49450,57 @@ class SuperlookupTab(QWidget):
48305
49450
 
48306
49451
  def check_for_signal(self):
48307
49452
  """Check if AHK wrote a signal file"""
49453
+ # Check for Superlookup signal
48308
49454
  if self.signal_file.exists():
48309
- print(f"[Superlookup] Signal file detected!")
49455
+ print(f"[SuperLookup] Signal file detected!")
48310
49456
  try:
48311
49457
  # Delete signal file
48312
49458
  self.signal_file.unlink()
48313
- print(f"[Superlookup] Signal file deleted")
48314
-
49459
+ print(f"[SuperLookup] Signal file deleted")
49460
+
48315
49461
  # Get text from clipboard (AHK already copied it)
48316
49462
  time.sleep(0.1) # Give clipboard a moment
48317
49463
  text = pyperclip.paste()
48318
-
49464
+
48319
49465
  # Trigger lookup
48320
49466
  if text:
48321
49467
  self.on_ahk_capture(text)
48322
49468
  except Exception as e:
48323
- print(f"[Superlookup] Error reading capture: {e}")
49469
+ print(f"[SuperLookup] Error reading capture: {e}")
49470
+
49471
+ # Check for MT Quick Lookup signal
49472
+ if hasattr(self, 'mt_lookup_signal_file') and self.mt_lookup_signal_file.exists():
49473
+ print(f"[QuickTrans] Signal file detected!")
49474
+ try:
49475
+ # Small delay to let AHK finish writing/close the file
49476
+ time.sleep(0.05)
49477
+
49478
+ # Delete signal file with retry for file lock
49479
+ for attempt in range(3):
49480
+ try:
49481
+ self.mt_lookup_signal_file.unlink()
49482
+ print(f"[QuickTrans] Signal file deleted")
49483
+ break
49484
+ except PermissionError:
49485
+ if attempt < 2:
49486
+ time.sleep(0.05)
49487
+ else:
49488
+ raise
49489
+
49490
+ # Get text from clipboard (AHK already copied it)
49491
+ time.sleep(0.1) # Give clipboard a moment
49492
+ text = pyperclip.paste()
49493
+
49494
+ # Trigger MT Quick Lookup
49495
+ if text:
49496
+ self.on_ahk_mt_lookup_capture(text)
49497
+ except Exception as e:
49498
+ print(f"[QuickTrans] Error reading capture: {e}")
48324
49499
 
48325
49500
  def on_ahk_capture(self, text):
48326
49501
  """Handle text captured by AHK"""
48327
49502
  try:
48328
- print(f"[Superlookup] on_ahk_capture called with text: {text[:50]}...")
49503
+ print(f"[SuperLookup] on_ahk_capture called with text: {text[:50]}...")
48329
49504
 
48330
49505
  # Bring Supervertaler to foreground
48331
49506
  main_window = self.window()
@@ -48355,39 +49530,96 @@ class SuperlookupTab(QWidget):
48355
49530
  QTimer.singleShot(250, lambda: self.show_superlookup(text))
48356
49531
 
48357
49532
  except Exception as e:
48358
- print(f"[Superlookup] Error handling capture: {e}")
48359
-
49533
+ print(f"[SuperLookup] Error handling capture: {e}")
49534
+
49535
+ def on_ahk_mt_lookup_capture(self, text):
49536
+ """Handle MT Quick Lookup text captured by AHK (Ctrl+Alt+M)"""
49537
+ try:
49538
+ print(f"[QuickTrans] on_ahk_mt_lookup_capture called with text: {text[:50]}...")
49539
+
49540
+ # Show popup directly without bringing Supervertaler to foreground
49541
+ # The popup has WindowStaysOnTopHint so it will appear over any app
49542
+ QTimer.singleShot(100, lambda: self.show_mt_quick_lookup_from_ahk(text))
49543
+
49544
+ except Exception as e:
49545
+ print(f"[QuickTrans] Error handling capture: {e}")
49546
+
49547
+ def show_mt_quick_lookup_from_ahk(self, text):
49548
+ """Show MT Quick Lookup popup with text from AHK capture"""
49549
+ try:
49550
+ print(f"[QuickTrans] show_mt_quick_lookup_from_ahk called with text: {text[:50]}...")
49551
+
49552
+ # Get main window reference for settings access
49553
+ main_window = self.main_window
49554
+ if not main_window:
49555
+ main_window = self.window()
49556
+
49557
+ if not main_window:
49558
+ print("[QuickTrans] ERROR: Could not find main window")
49559
+ return
49560
+
49561
+ # Get language settings
49562
+ source_lang = getattr(main_window, 'source_language', 'English')
49563
+ target_lang = getattr(main_window, 'target_language', 'Dutch')
49564
+
49565
+ # Import and show MT Quick Lookup popup
49566
+ from modules.mt_quick_popup import MTQuickPopup
49567
+
49568
+ # Create popup without Qt parent so it can appear independently over any app
49569
+ # Pass main_window as parent_app for API access
49570
+ popup = MTQuickPopup(
49571
+ parent_app=main_window,
49572
+ source_text=text,
49573
+ source_lang=source_lang,
49574
+ target_lang=target_lang,
49575
+ parent=None # No Qt parent - allows independent window
49576
+ )
49577
+
49578
+ # Store reference to prevent garbage collection
49579
+ self._ahk_mt_popup = popup
49580
+
49581
+ # Show and ensure it's on top
49582
+ popup.show()
49583
+ popup.raise_()
49584
+ popup.activateWindow()
49585
+ print(f"[QuickTrans] Popup shown for text: {text[:30]}...")
49586
+
49587
+ except Exception as e:
49588
+ print(f"[QuickTrans] Error showing popup: {e}")
49589
+ import traceback
49590
+ traceback.print_exc()
49591
+
48360
49592
  def show_superlookup(self, text):
48361
49593
  """Show Superlookup with pre-filled text"""
48362
49594
  try:
48363
- print(f"[Superlookup] show_superlookup called with text: {text[:50]}...")
49595
+ print(f"[SuperLookup] show_superlookup called with text: {text[:50]}...")
48364
49596
 
48365
49597
  # Get main window reference
48366
49598
  main_window = self.main_window
48367
49599
  if not main_window:
48368
49600
  main_window = self.window()
48369
49601
 
48370
- print(f"[Superlookup] Main window found: {main_window is not None}")
48371
- print(f"[Superlookup] Main window type: {type(main_window).__name__}")
48372
- print(f"[Superlookup] Has main_tabs: {hasattr(main_window, 'main_tabs')}")
49602
+ print(f"[SuperLookup] Main window found: {main_window is not None}")
49603
+ print(f"[SuperLookup] Main window type: {type(main_window).__name__}")
49604
+ print(f"[SuperLookup] Has main_tabs: {hasattr(main_window, 'main_tabs')}")
48373
49605
 
48374
49606
  # Switch to Tools tab (main_tabs index 3)
48375
49607
  # Tab structure: Grid=0, Resources=1, QuickMenu=2, Tools=3, Settings=4
48376
49608
  if hasattr(main_window, 'main_tabs'):
48377
- print(f"[Superlookup] Current main_tab index: {main_window.main_tabs.currentIndex()}")
49609
+ print(f"[SuperLookup] Current main_tab index: {main_window.main_tabs.currentIndex()}")
48378
49610
  main_window.main_tabs.setCurrentIndex(3) # Tools tab
48379
- print(f"[Superlookup] Switched to Tools tab (index 2)")
49611
+ print(f"[SuperLookup] Switched to Tools tab (index 2)")
48380
49612
  QApplication.processEvents() # Force GUI update
48381
49613
  else:
48382
- print(f"[Superlookup] WARNING: Main window has no main_tabs attribute!")
49614
+ print(f"[SuperLookup] WARNING: Main window has no main_tabs attribute!")
48383
49615
 
48384
49616
  # Switch to Superlookup within modules_tabs
48385
49617
  if hasattr(main_window, 'modules_tabs'):
48386
- print(f"[Superlookup] Current modules_tab index: {main_window.modules_tabs.currentIndex()}")
49618
+ print(f"[SuperLookup] Current modules_tab index: {main_window.modules_tabs.currentIndex()}")
48387
49619
  for i in range(main_window.modules_tabs.count()):
48388
49620
  if "Superlookup" in main_window.modules_tabs.tabText(i):
48389
49621
  main_window.modules_tabs.setCurrentIndex(i)
48390
- print(f"[Superlookup] Switched to Superlookup tab (index {i})")
49622
+ print(f"[SuperLookup] Switched to Superlookup tab (index {i})")
48391
49623
  QApplication.processEvents() # Force GUI update
48392
49624
  break
48393
49625
 
@@ -48395,23 +49627,23 @@ class SuperlookupTab(QWidget):
48395
49627
  QTimer.singleShot(100, lambda: self._fill_and_search(text))
48396
49628
 
48397
49629
  except Exception as e:
48398
- print(f"[Superlookup] Error showing lookup: {e}")
49630
+ print(f"[SuperLookup] Error showing lookup: {e}")
48399
49631
 
48400
49632
  def _fill_and_search(self, text):
48401
49633
  """Fill in text and trigger search (called after tab switching completes)"""
48402
49634
  try:
48403
- print(f"[Superlookup] _fill_and_search called")
49635
+ print(f"[SuperLookup] _fill_and_search called")
48404
49636
  # Fill in text and trigger lookup
48405
49637
  if hasattr(self, 'source_text'):
48406
49638
  self.source_text.setCurrentText(text)
48407
- print(f"[Superlookup] Text filled in source_text field")
49639
+ print(f"[SuperLookup] Text filled in source_text field")
48408
49640
  # Trigger lookup by calling perform_lookup directly
48409
49641
  self.perform_lookup()
48410
- print(f"[Superlookup] perform_lookup() called")
49642
+ print(f"[SuperLookup] perform_lookup() called")
48411
49643
  else:
48412
- print(f"[Superlookup] ERROR: source_text widget not found!")
49644
+ print(f"[SuperLookup] ERROR: source_text widget not found!")
48413
49645
  except Exception as e:
48414
- print(f"[Superlookup] Error in _fill_and_search: {e}")
49646
+ print(f"[SuperLookup] Error in _fill_and_search: {e}")
48415
49647
  import traceback
48416
49648
  traceback.print_exc()
48417
49649