supervertaler 1.9.181__py3-none-any.whl → 1.9.184__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.181"
35
+ __version__ = "1.9.184"
36
36
  __phase__ = "0.9"
37
- __release_date__ = "2026-01-30"
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
  """
@@ -2739,10 +2731,15 @@ class EditableGridTextEditor(QTextEdit):
2739
2731
  self.setPalette(palette)
2740
2732
 
2741
2733
  # Add syntax highlighter for tags (with spellcheck enabled for target cells)
2742
- # Get invisible char color from main window if available
2734
+ # Get invisible char color and tag color from main window (theme-aware)
2743
2735
  main_window = self._get_main_window()
2744
2736
  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)
2737
+
2738
+ # Use theme-aware tag color (light pink in dark mode, dark red in light mode)
2739
+ is_dark = main_window and hasattr(main_window, 'theme_manager') and main_window.theme_manager and main_window.theme_manager.current_theme.name == "Dark"
2740
+ tag_color = '#FFB6C1' if is_dark else self.tag_highlight_color # Light pink in dark mode
2741
+
2742
+ self.highlighter = TagHighlighter(self.document(), tag_color, invisible_char_color, enable_spellcheck=True)
2746
2743
 
2747
2744
  # Style to look like a normal cell with subtle selection
2748
2745
  # Background and text colors now managed by theme system
@@ -6224,6 +6221,12 @@ class SupervertalerQt(QMainWindow):
6224
6221
  self.termbase_cache_lock = threading.Lock() # Thread-safe cache access
6225
6222
  self.termbase_batch_worker_thread = None # Background worker thread
6226
6223
  self.termbase_batch_stop_event = threading.Event() # Signal to stop background worker
6224
+
6225
+ # In-memory termbase index for instant lookups (v1.9.182)
6226
+ # Loaded once on project load, contains ALL terms from activated termbases
6227
+ # Structure: list of term dicts with pre-compiled regex patterns
6228
+ self.termbase_index = []
6229
+ self.termbase_index_lock = threading.Lock()
6227
6230
 
6228
6231
  # TM/MT/LLM prefetch cache for instant segment switching (like memoQ)
6229
6232
  # Maps segment ID → {"TM": [...], "MT": [...], "LLM": [...]}
@@ -6237,9 +6240,9 @@ class SupervertalerQt(QMainWindow):
6237
6240
  self.idle_prefetch_timer = None # QTimer for triggering prefetch after typing pause
6238
6241
  self.idle_prefetch_delay_ms = 1500 # Start prefetch 1.5s after user stops typing
6239
6242
 
6240
- # 🧪 EXPERIMENTAL: Cache kill switch for performance testing
6243
+ # Cache kill switch for performance testing
6241
6244
  # When True, all caches are bypassed - direct lookups every time
6242
- self.disable_all_caches = True
6245
+ self.disable_all_caches = False # v1.9.183: Default to False (caches ENABLED)
6243
6246
 
6244
6247
  # Undo/Redo stack for grid edits
6245
6248
  self.undo_stack = [] # List of (segment_id, old_target, new_target, old_status, new_status)
@@ -6367,7 +6370,13 @@ class SupervertalerQt(QMainWindow):
6367
6370
  self.termview_widget.theme_manager = self.theme_manager
6368
6371
  if hasattr(self.termview_widget, 'apply_theme'):
6369
6372
  self.termview_widget.apply_theme()
6370
-
6373
+
6374
+ # Also update the Match Panel TermView (right panel)
6375
+ if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
6376
+ self.termview_widget_match.theme_manager = self.theme_manager
6377
+ if hasattr(self.termview_widget_match, 'apply_theme'):
6378
+ self.termview_widget_match.apply_theme()
6379
+
6371
6380
  if hasattr(self, 'translation_results_panel') and self.translation_results_panel:
6372
6381
  self.translation_results_panel.theme_manager = self.theme_manager
6373
6382
  # Also update class-level theme_manager for CompactMatchItem
@@ -10335,12 +10344,9 @@ class SupervertalerQt(QMainWindow):
10335
10344
 
10336
10345
  # Superdocs removed (online GitBook will be used instead)
10337
10346
 
10338
- print("[DEBUG] About to create SuperlookupTab...")
10339
10347
  lookup_tab = SuperlookupTab(self, user_data_path=self.user_data_path)
10340
- print("[DEBUG] SuperlookupTab created successfully")
10341
10348
  self.lookup_tab = lookup_tab # Store reference for later use
10342
10349
  modules_tabs.addTab(lookup_tab, "🔍 Superlookup")
10343
- print("[DEBUG] Superlookup tab added to modules_tabs")
10344
10350
 
10345
10351
  # Supervoice - Voice Commands & Dictation
10346
10352
  supervoice_tab = self._create_voice_dictation_settings_tab()
@@ -12231,6 +12237,46 @@ class SupervertalerQt(QMainWindow):
12231
12237
  except Exception as e:
12232
12238
  self.log(f"Error updating Match Panel termview: {e}")
12233
12239
 
12240
+ def _update_termview_for_segment(self, segment):
12241
+ """Explicitly update termview for a segment (v1.9.182).
12242
+
12243
+ This is called directly from Ctrl+Enter navigation to ensure
12244
+ the termview updates immediately, bypassing the deferred timer approach.
12245
+ """
12246
+ if not segment or not hasattr(self, 'termview_widget'):
12247
+ return
12248
+
12249
+ try:
12250
+ # Use in-memory index for fast lookup
12251
+ stored_matches = self.find_termbase_matches_in_source(segment.source)
12252
+
12253
+ # Convert dict format to list format for termview
12254
+ termbase_matches = [
12255
+ {
12256
+ 'source_term': match_data.get('source', ''),
12257
+ 'target_term': match_data.get('translation', ''),
12258
+ 'termbase_name': match_data.get('termbase_name', ''),
12259
+ 'ranking': match_data.get('ranking', 99),
12260
+ 'is_project_termbase': match_data.get('is_project_termbase', False),
12261
+ 'term_id': match_data.get('term_id'),
12262
+ 'termbase_id': match_data.get('termbase_id'),
12263
+ 'notes': match_data.get('notes', '')
12264
+ }
12265
+ for match_data in stored_matches.values()
12266
+ ] if stored_matches else []
12267
+
12268
+ # Get NT matches
12269
+ nt_matches = self.find_nt_matches_in_source(segment.source)
12270
+
12271
+ # Get status hint
12272
+ status_hint = self._get_termbase_status_hint()
12273
+
12274
+ # Update both Termview widgets
12275
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
12276
+
12277
+ except Exception as e:
12278
+ self.log(f"Error in _update_termview_for_segment: {e}")
12279
+
12234
12280
  def _get_termbase_status_hint(self) -> str:
12235
12281
  """Check termbase activation status and return appropriate hint.
12236
12282
 
@@ -12263,7 +12309,7 @@ class SupervertalerQt(QMainWindow):
12263
12309
  project_target = (self.current_project.target_lang or '').lower()
12264
12310
 
12265
12311
  # Get all termbases and check language pairs
12266
- all_termbases = self.termbase_mgr.list_termbases()
12312
+ all_termbases = self.termbase_mgr.get_all_termbases()
12267
12313
  has_matching_language = False
12268
12314
 
12269
12315
  for tb in all_termbases:
@@ -12818,6 +12864,39 @@ class SupervertalerQt(QMainWindow):
12818
12864
  # Use term_id as key to avoid duplicates
12819
12865
  self.termbase_cache[segment_id][term_id] = new_match
12820
12866
  self.log(f"⚡ Added term directly to cache (instant update)")
12867
+
12868
+ # v1.9.182: Also add to in-memory termbase index for future lookups
12869
+ import re
12870
+ source_lower = source_text.lower().strip()
12871
+ try:
12872
+ if any(c in source_lower for c in '.%,/-'):
12873
+ pattern = re.compile(r'(?<!\w)' + re.escape(source_lower) + r'(?!\w)')
12874
+ else:
12875
+ pattern = re.compile(r'\b' + re.escape(source_lower) + r'\b')
12876
+ except re.error:
12877
+ pattern = None
12878
+
12879
+ index_entry = {
12880
+ 'term_id': term_id,
12881
+ 'source_term': source_text,
12882
+ 'source_term_lower': source_lower,
12883
+ 'target_term': target_text,
12884
+ 'termbase_id': target_termbase['id'],
12885
+ 'priority': 99,
12886
+ 'domain': '',
12887
+ 'notes': '',
12888
+ 'project': '',
12889
+ 'client': '',
12890
+ 'forbidden': False,
12891
+ 'is_project_termbase': False,
12892
+ 'termbase_name': target_termbase['name'],
12893
+ 'ranking': glossary_rank,
12894
+ 'pattern': pattern,
12895
+ }
12896
+ with self.termbase_index_lock:
12897
+ self.termbase_index.append(index_entry)
12898
+ # Re-sort by length (longest first) for proper phrase matching
12899
+ self.termbase_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
12821
12900
 
12822
12901
  # Update TermView widget with the new term
12823
12902
  if hasattr(self, 'termview_widget') and self.termview_widget:
@@ -13703,15 +13782,16 @@ class SupervertalerQt(QMainWindow):
13703
13782
  # Use 0 (global) when no project is loaded - allows Superlookup to work
13704
13783
  curr_proj = self.current_project if hasattr(self, 'current_project') else None
13705
13784
  curr_proj_id = curr_proj.id if (curr_proj and hasattr(curr_proj, 'id')) else 0 # 0 = global
13706
-
13785
+
13707
13786
  if checked:
13708
13787
  termbase_mgr.activate_termbase(tb_id, curr_proj_id)
13709
13788
  else:
13710
13789
  termbase_mgr.deactivate_termbase(tb_id, curr_proj_id)
13711
-
13712
- # Clear cache and refresh
13790
+
13791
+ # Clear cache and rebuild in-memory index (v1.9.182)
13713
13792
  with self.termbase_cache_lock:
13714
13793
  self.termbase_cache.clear()
13794
+ self._build_termbase_index() # Rebuild index with new activation state
13715
13795
  refresh_termbase_list()
13716
13796
 
13717
13797
  read_checkbox.toggled.connect(on_read_toggle)
@@ -17040,7 +17120,7 @@ class SupervertalerQt(QMainWindow):
17040
17120
 
17041
17121
  # Cache kill switch
17042
17122
  disable_cache_cb = CheckmarkCheckBox("Disable ALL caches (direct lookups every time)")
17043
- disable_cache_cb.setChecked(general_settings.get('disable_all_caches', True))
17123
+ disable_cache_cb.setChecked(general_settings.get('disable_all_caches', False))
17044
17124
  disable_cache_cb.setToolTip(
17045
17125
  "When enabled, ALL caching is bypassed:\n"
17046
17126
  "• Termbase cache\n"
@@ -19684,7 +19764,7 @@ class SupervertalerQt(QMainWindow):
19684
19764
  'results_compare_font_size': 9,
19685
19765
  'autohotkey_path': ahk_path_edit.text().strip() if ahk_path_edit is not None else existing_settings.get('autohotkey_path', ''),
19686
19766
  'enable_sound_effects': sound_effects_cb.isChecked() if sound_effects_cb is not None else existing_settings.get('enable_sound_effects', False),
19687
- 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', True)
19767
+ 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', False)
19688
19768
  }
19689
19769
 
19690
19770
  # Keep a fast-access instance value
@@ -22252,7 +22332,154 @@ class SupervertalerQt(QMainWindow):
22252
22332
  except Exception as e:
22253
22333
  QMessageBox.critical(self, "Error", f"Failed to load project:\n{str(e)}")
22254
22334
  self.log(f"✗ Error loading project: {e}")
22255
-
22335
+
22336
+ def _build_termbase_index(self):
22337
+ """
22338
+ Build in-memory index of ALL terms from activated termbases (v1.9.182).
22339
+
22340
+ This is called ONCE on project load and replaces thousands of per-word
22341
+ database queries with a single bulk load + fast in-memory lookups.
22342
+
22343
+ Performance: Reduces 349-segment termbase search from 365 seconds to <1 second.
22344
+ """
22345
+ import re
22346
+ import time
22347
+ start_time = time.time()
22348
+
22349
+ if not self.current_project or not hasattr(self, 'db_manager') or not self.db_manager:
22350
+ return
22351
+
22352
+ project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
22353
+
22354
+ # Query ALL terms from activated termbases in ONE query
22355
+ # This replaces ~17,500 individual queries (349 segments × 50 words each)
22356
+ query = """
22357
+ SELECT
22358
+ t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
22359
+ t.domain, t.notes, t.project, t.client, t.forbidden,
22360
+ tb.is_project_termbase, tb.name as termbase_name,
22361
+ COALESCE(ta.priority, tb.ranking) as ranking
22362
+ FROM termbase_terms t
22363
+ LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
22364
+ LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id
22365
+ AND ta.project_id = ? AND ta.is_active = 1
22366
+ WHERE (ta.is_active = 1 OR tb.is_project_termbase = 1)
22367
+ """
22368
+
22369
+ new_index = []
22370
+ try:
22371
+ self.db_manager.cursor.execute(query, [project_id or 0])
22372
+ rows = self.db_manager.cursor.fetchall()
22373
+
22374
+ for row in rows:
22375
+ source_term = row[1] # source_term
22376
+ if not source_term:
22377
+ continue
22378
+
22379
+ source_term_lower = source_term.lower().strip()
22380
+ if len(source_term_lower) < 2:
22381
+ continue
22382
+
22383
+ # Pre-compile regex pattern for word-boundary matching
22384
+ # This avoids recompiling the same pattern thousands of times
22385
+ try:
22386
+ # Handle terms with punctuation differently
22387
+ if any(c in source_term_lower for c in '.%,/-'):
22388
+ pattern = re.compile(r'(?<!\w)' + re.escape(source_term_lower) + r'(?!\w)')
22389
+ else:
22390
+ pattern = re.compile(r'\b' + re.escape(source_term_lower) + r'\b')
22391
+ except re.error:
22392
+ # If regex fails, use simple substring matching
22393
+ pattern = None
22394
+
22395
+ new_index.append({
22396
+ 'term_id': row[0],
22397
+ 'source_term': source_term,
22398
+ 'source_term_lower': source_term_lower,
22399
+ 'target_term': row[2],
22400
+ 'termbase_id': row[3],
22401
+ 'priority': row[4],
22402
+ 'domain': row[5],
22403
+ 'notes': row[6],
22404
+ 'project': row[7],
22405
+ 'client': row[8],
22406
+ 'forbidden': row[9],
22407
+ 'is_project_termbase': row[10],
22408
+ 'termbase_name': row[11],
22409
+ 'ranking': row[12],
22410
+ 'pattern': pattern, # Pre-compiled regex
22411
+ })
22412
+
22413
+ # Sort by term length (longest first) for better phrase matching
22414
+ new_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
22415
+
22416
+ # Thread-safe update of the index
22417
+ with self.termbase_index_lock:
22418
+ self.termbase_index = new_index
22419
+
22420
+ elapsed = time.time() - start_time
22421
+ self.log(f"✅ Built termbase index: {len(new_index)} terms in {elapsed:.2f}s")
22422
+
22423
+ except Exception as e:
22424
+ self.log(f"❌ Failed to build termbase index: {e}")
22425
+ import traceback
22426
+ self.log(traceback.format_exc())
22427
+
22428
+ def _search_termbase_in_memory(self, source_text: str) -> dict:
22429
+ """
22430
+ Search termbase using in-memory index (v1.9.182).
22431
+
22432
+ This replaces _search_termbases_thread_safe() for batch operations.
22433
+ Instead of N database queries (one per word), we do:
22434
+ - 1 pass through the index (typically ~1000 terms)
22435
+ - Fast string 'in' check + pre-compiled regex validation
22436
+
22437
+ Performance: <1ms per segment vs 1+ second per segment.
22438
+ """
22439
+ if not source_text:
22440
+ return {}
22441
+
22442
+ with self.termbase_index_lock:
22443
+ if not self.termbase_index:
22444
+ return {}
22445
+ index = self.termbase_index # Local reference for thread safety
22446
+
22447
+ source_lower = source_text.lower()
22448
+ matches = {}
22449
+
22450
+ for term in index:
22451
+ term_lower = term['source_term_lower']
22452
+
22453
+ # Quick substring check first (very fast, implemented in C)
22454
+ if term_lower not in source_lower:
22455
+ continue
22456
+
22457
+ # Word boundary validation using pre-compiled pattern
22458
+ pattern = term.get('pattern')
22459
+ if pattern:
22460
+ if not pattern.search(source_lower):
22461
+ continue
22462
+
22463
+ # Term matches! Add to results
22464
+ term_id = term['term_id']
22465
+ matches[term_id] = {
22466
+ 'source': term['source_term'],
22467
+ 'translation': term['target_term'],
22468
+ 'term_id': term_id,
22469
+ 'termbase_id': term['termbase_id'],
22470
+ 'termbase_name': term['termbase_name'],
22471
+ 'priority': term['priority'],
22472
+ 'ranking': term['ranking'],
22473
+ 'is_project_termbase': term['is_project_termbase'],
22474
+ 'forbidden': term['forbidden'],
22475
+ 'domain': term['domain'],
22476
+ 'notes': term['notes'],
22477
+ 'project': term['project'],
22478
+ 'client': term['client'],
22479
+ }
22480
+
22481
+ return matches
22482
+
22256
22483
  def _start_termbase_batch_worker(self):
22257
22484
  """
22258
22485
  Start background thread to batch-process termbase matches for all segments.
@@ -22260,21 +22487,25 @@ class SupervertalerQt(QMainWindow):
22260
22487
  """
22261
22488
  if not self.current_project or len(self.current_project.segments) == 0:
22262
22489
  return
22263
-
22490
+
22491
+ # Build in-memory termbase index FIRST (v1.9.182)
22492
+ # This is the key optimization: load all terms once, then do fast in-memory lookups
22493
+ self._build_termbase_index()
22494
+
22264
22495
  # 🧪 EXPERIMENTAL: Skip batch worker if cache kill switch is enabled
22265
22496
  if getattr(self, 'disable_all_caches', False):
22266
22497
  self.log("🧪 Termbase batch worker SKIPPED (caches disabled)")
22267
22498
  return
22268
-
22499
+
22269
22500
  # Stop any existing worker thread
22270
22501
  self.termbase_batch_stop_event.set()
22271
22502
  if self.termbase_batch_worker_thread and self.termbase_batch_worker_thread.is_alive():
22272
22503
  self.log("⏹️ Stopping existing termbase batch worker...")
22273
22504
  self.termbase_batch_worker_thread.join(timeout=2)
22274
-
22505
+
22275
22506
  # Reset stop event for new worker
22276
22507
  self.termbase_batch_stop_event.clear()
22277
-
22508
+
22278
22509
  # Start new background worker thread
22279
22510
  segment_count = len(self.current_project.segments)
22280
22511
  self.log(f"🔄 Starting background termbase batch processor for {segment_count} segments...")
@@ -22290,96 +22521,60 @@ class SupervertalerQt(QMainWindow):
22290
22521
  """
22291
22522
  Background worker thread: process all segments and populate termbase cache.
22292
22523
  Runs in separate thread to not block UI.
22293
-
22294
- IMPORTANT: Creates its own database connection to avoid SQLite threading errors.
22524
+
22525
+ v1.9.182: Now uses in-memory termbase index for 1000x faster lookups.
22526
+ Old approach: 365 seconds for 349 segments (1 second/segment)
22527
+ New approach: <1 second for 349 segments (<3ms/segment)
22295
22528
  """
22296
22529
  if not segments:
22297
22530
  return
22298
-
22299
- # Create a separate database connection for this thread
22300
- # SQLite connections are thread-local and cannot be shared across threads
22301
- import sqlite3
22302
- try:
22303
- thread_db_connection = sqlite3.connect(self.db_manager.db_path)
22304
- thread_db_connection.row_factory = sqlite3.Row
22305
- thread_db_cursor = thread_db_connection.cursor()
22306
- except Exception as e:
22307
- self.log(f"❌ Failed to create database connection in batch worker: {e}")
22308
- return
22309
-
22531
+
22310
22532
  try:
22311
22533
  processed = 0
22312
22534
  cached = 0
22535
+ with_matches = 0
22313
22536
  start_time = time.time()
22314
-
22537
+
22315
22538
  for segment in segments:
22316
22539
  # Check if stop event was signaled (user closed project or started new one)
22317
22540
  if self.termbase_batch_stop_event.is_set():
22318
22541
  self.log(f"⏹️ Termbase batch worker stopped by user (processed {processed} segments)")
22319
22542
  break
22320
-
22543
+
22321
22544
  segment_id = segment.id
22322
-
22545
+
22323
22546
  # Skip if already in cache (thread-safe check)
22324
22547
  with self.termbase_cache_lock:
22325
22548
  if segment_id in self.termbase_cache:
22326
22549
  cached += 1
22327
22550
  continue
22328
-
22329
- # Search termbase for this segment using thread-local database connection
22551
+
22552
+ # v1.9.182: Use in-memory index for instant lookup (no database queries!)
22330
22553
  try:
22331
- # Manually query the database using thread-local connection
22332
- # Pass project_id to filter by activated termbases only
22333
- current_project_id = self.current_project.id if (self.current_project and hasattr(self.current_project, 'id')) else None
22334
- matches = self._search_termbases_thread_safe(
22335
- segment.source,
22336
- thread_db_cursor,
22337
- source_lang=self.current_project.source_lang if self.current_project else None,
22338
- target_lang=self.current_project.target_lang if self.current_project else None,
22339
- project_id=current_project_id
22340
- )
22341
-
22554
+ matches = self._search_termbase_in_memory(segment.source)
22555
+
22556
+ # Store in cache (thread-safe) - even empty results to avoid re-lookup
22557
+ with self.termbase_cache_lock:
22558
+ self.termbase_cache[segment_id] = matches
22559
+
22560
+ processed += 1
22342
22561
  if matches:
22343
- # Store in cache (thread-safe)
22344
- with self.termbase_cache_lock:
22345
- self.termbase_cache[segment_id] = matches
22346
-
22347
- processed += 1
22348
-
22349
- # Log progress every 100 segments
22350
- if processed % 100 == 0:
22351
- elapsed = time.time() - start_time
22352
- rate = processed / elapsed if elapsed > 0 else 0
22353
- remaining = len(segments) - processed
22354
- eta_seconds = remaining / rate if rate > 0 else 0
22355
- self.log(f"📊 Batch progress: {processed}/{len(segments)} cached " +
22356
- f"({rate:.1f} seg/sec, ETA: {int(eta_seconds)}s)")
22357
-
22562
+ with_matches += 1
22563
+
22358
22564
  except Exception as e:
22359
22565
  self.log(f"❌ Error processing segment {segment_id} in batch worker: {e}")
22360
22566
  continue
22361
-
22362
- # Small delay to prevent CPU saturation (let UI thread work)
22363
- time.sleep(0.001) # 1ms delay between segments
22364
-
22567
+
22365
22568
  elapsed = time.time() - start_time
22366
22569
  total_cached = len(self.termbase_cache)
22367
- self.log(f"✅ Termbase batch worker complete: {processed} new + {cached} existing = " +
22368
- f"{total_cached} total cached in {elapsed:.1f}s")
22369
-
22570
+ rate = processed / elapsed if elapsed > 0 else 0
22571
+ self.log(f"✅ Termbase batch worker complete: {processed} segments in {elapsed:.2f}s " +
22572
+ f"({rate:.0f} seg/sec, {with_matches} with matches)")
22573
+
22370
22574
  except Exception as e:
22371
22575
  self.log(f"❌ Termbase batch worker error: {e}")
22372
22576
  import traceback
22373
22577
  self.log(traceback.format_exc())
22374
-
22375
- finally:
22376
- # Close thread-local database connection
22377
- try:
22378
- thread_db_cursor.close()
22379
- thread_db_connection.close()
22380
- self.log("✓ Closed thread-local database connection in batch worker")
22381
- except:
22382
- pass
22383
22578
 
22384
22579
  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]:
22385
22580
  """
@@ -22579,11 +22774,8 @@ class SupervertalerQt(QMainWindow):
22579
22774
  Also triggers PROACTIVE HIGHLIGHTING for upcoming segments with glossary matches.
22580
22775
  """
22581
22776
  import json
22582
-
22583
- print(f"[PROACTIVE DEBUG] _trigger_idle_prefetch called for row {current_row}")
22584
-
22777
+
22585
22778
  if not self.current_project or current_row < 0:
22586
- print(f"[PROACTIVE DEBUG] Early exit: no project or invalid row")
22587
22779
  return
22588
22780
 
22589
22781
  try:
@@ -22592,9 +22784,7 @@ class SupervertalerQt(QMainWindow):
22592
22784
  already_cached_ids = []
22593
22785
  start_idx = current_row + 1
22594
22786
  end_idx = min(start_idx + 5, len(self.current_project.segments))
22595
-
22596
- print(f"[PROACTIVE DEBUG] Checking segments {start_idx} to {end_idx}")
22597
-
22787
+
22598
22788
  for seg in self.current_project.segments[start_idx:end_idx]:
22599
22789
  # Check if already cached
22600
22790
  with self.translation_matches_cache_lock:
@@ -22602,23 +22792,19 @@ class SupervertalerQt(QMainWindow):
22602
22792
  next_segment_ids.append(seg.id)
22603
22793
  else:
22604
22794
  already_cached_ids.append(seg.id)
22605
-
22606
- print(f"[PROACTIVE DEBUG] Already cached IDs: {already_cached_ids}, Need prefetch: {next_segment_ids}")
22607
-
22795
+
22608
22796
  # For already-cached segments, trigger proactive highlighting immediately
22609
22797
  # This handles the case where segments were cached earlier but not highlighted
22610
22798
  for seg_id in already_cached_ids:
22611
22799
  try:
22612
22800
  with self.termbase_cache_lock:
22613
22801
  termbase_raw = self.termbase_cache.get(seg_id, {})
22614
- print(f"[PROACTIVE DEBUG] Segment {seg_id} termbase cache: {len(termbase_raw) if termbase_raw else 0} matches")
22615
22802
  if termbase_raw:
22616
22803
  termbase_json = json.dumps(termbase_raw)
22617
22804
  # Apply highlighting on main thread (we're already on main thread here)
22618
- print(f"[PROACTIVE DEBUG] Calling _apply_proactive_highlighting for seg {seg_id}")
22619
22805
  self._apply_proactive_highlighting(seg_id, termbase_json)
22620
- except Exception as e:
22621
- print(f"[PROACTIVE DEBUG] Error for seg {seg_id}: {e}")
22806
+ except Exception:
22807
+ pass # Silent failure for proactive highlighting
22622
22808
 
22623
22809
  if next_segment_ids:
22624
22810
  # Start prefetch in background (silent, no logging)
@@ -22700,43 +22886,35 @@ class SupervertalerQt(QMainWindow):
22700
22886
 
22701
22887
  # Fetch TM/termbase matches (pass cursor for thread-safe termbase lookups)
22702
22888
  matches = self._fetch_all_matches_for_segment(segment, thread_db_cursor)
22703
-
22704
- # Only cache if we got at least one match (don't cache empty results)
22705
- # This prevents "empty cache hits" when TM database is still empty
22889
+
22890
+ # Count matches for logging and proactive highlighting
22706
22891
  tm_count = len(matches.get("TM", []))
22707
22892
  tb_count = len(matches.get("Termbases", []))
22708
22893
  mt_count = len(matches.get("MT", []))
22709
22894
  llm_count = len(matches.get("LLM", []))
22710
22895
  total_matches = tm_count + tb_count + mt_count + llm_count
22711
22896
 
22712
- print(f"[PREFETCH DEBUG] Segment {segment_id}: TM={tm_count}, TB={tb_count}, MT={mt_count}, LLM={llm_count}")
22713
-
22897
+ # Only cache results if we found something
22898
+ # Don't cache empty results - let main thread do fresh lookup
22714
22899
  if total_matches > 0:
22715
- # Store in cache only if we have results
22716
22900
  with self.translation_matches_cache_lock:
22717
22901
  self.translation_matches_cache[segment_id] = matches
22718
-
22719
- # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
22720
- # This makes upcoming segments show their glossary matches immediately
22721
- if tb_count > 0:
22722
- try:
22723
- # Extract raw termbase matches from cache for highlighting
22724
- with self.termbase_cache_lock:
22725
- termbase_raw = self.termbase_cache.get(segment_id, {})
22726
-
22727
- print(f"[PREFETCH DEBUG] Segment {segment_id}: termbase_raw has {len(termbase_raw) if termbase_raw else 0} entries")
22728
-
22729
- if termbase_raw:
22730
- # Convert to JSON for thread-safe signal transfer
22731
- termbase_json = json.dumps(termbase_raw)
22732
- # Emit signal - will be handled on main thread
22733
- print(f"[PREFETCH DEBUG] Emitting proactive highlight signal for segment {segment_id}")
22734
- self._proactive_highlight_signal.emit(segment_id, termbase_json)
22735
- else:
22736
- print(f"[PREFETCH DEBUG] WARNING: tb_count={tb_count} but termbase_raw is empty!")
22737
- except Exception as e:
22738
- print(f"[PREFETCH DEBUG] ERROR emitting signal: {e}")
22739
- # else: Don't cache empty results - let it fall through to slow lookup next time
22902
+
22903
+ # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
22904
+ # This makes upcoming segments show their glossary matches immediately
22905
+ if tb_count > 0:
22906
+ try:
22907
+ # Extract raw termbase matches from cache for highlighting
22908
+ with self.termbase_cache_lock:
22909
+ termbase_raw = self.termbase_cache.get(segment_id, {})
22910
+
22911
+ if termbase_raw:
22912
+ # Convert to JSON for thread-safe signal transfer
22913
+ termbase_json = json.dumps(termbase_raw)
22914
+ # Emit signal - will be handled on main thread
22915
+ self._proactive_highlight_signal.emit(segment_id, termbase_json)
22916
+ except Exception:
22917
+ pass # Silent fail for proactive highlighting
22740
22918
 
22741
22919
  except Exception as e:
22742
22920
  self.log(f"Error in prefetch worker: {e}")
@@ -22786,31 +22964,9 @@ class SupervertalerQt(QMainWindow):
22786
22964
  source_lang_code = self._convert_language_to_code(source_lang)
22787
22965
  target_lang_code = self._convert_language_to_code(target_lang)
22788
22966
 
22789
- # 1. TM matches (if enabled) - thread-safe check
22790
- enable_tm = getattr(self, 'enable_tm_matching', True) # Default to True if not set
22791
- if enable_tm and hasattr(self, 'db_manager') and self.db_manager:
22792
- try:
22793
- tm_results = self.db_manager.search_translation_memory(
22794
- segment.source,
22795
- source_lang,
22796
- target_lang,
22797
- limit=5
22798
- )
22799
-
22800
- if tm_results: # Only add if we got results
22801
- for tm_match in tm_results:
22802
- match_obj = TranslationMatch(
22803
- source=tm_match.get('source', ''),
22804
- target=tm_match.get('target', ''),
22805
- relevance=tm_match.get('similarity', 0),
22806
- metadata={'tm_name': tm_match.get('tm_id', 'project')},
22807
- match_type='TM',
22808
- compare_source=tm_match.get('source', ''),
22809
- provider_code='TM'
22810
- )
22811
- matches_dict["TM"].append(match_obj)
22812
- except Exception as e:
22813
- pass # Silently continue
22967
+ # 1. TM matches - SKIP in prefetch worker (TM search not thread-safe)
22968
+ # TM will be fetched on-demand when user navigates to segment
22969
+ pass
22814
22970
 
22815
22971
  # 2. MT matches (if enabled)
22816
22972
  if self.enable_mt_matching:
@@ -22985,8 +23141,9 @@ class SupervertalerQt(QMainWindow):
22985
23141
  mode_note = " (overwrite)" if overwrite_mode else ""
22986
23142
  msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
22987
23143
  self._queue_tm_save_log(msg)
22988
- # Invalidate cache so prefetched segments get fresh TM matches
22989
- self.invalidate_translation_cache()
23144
+ # NOTE: Removed cache invalidation here - it was destroying batch worker's cache
23145
+ # on every Ctrl+Enter, making navigation extremely slow. The small chance of
23146
+ # seeing stale TM matches is far less important than responsive navigation.
22990
23147
 
22991
23148
  def invalidate_translation_cache(self, smart_invalidation=True):
22992
23149
  """
@@ -28959,6 +29116,9 @@ class SupervertalerQt(QMainWindow):
28959
29116
  segment_num_color = "black" if theme.name in ["Light (Default)", "Soft Gray", "Warm Cream", "Sepia", "High Contrast"] else theme.text
28960
29117
  id_item.setForeground(QColor(segment_num_color))
28961
29118
  id_item.setBackground(QColor()) # Default background from theme
29119
+ # Smaller font for segment numbers
29120
+ seg_num_font = QFont(self.default_font_family, max(8, self.default_font_size - 2))
29121
+ id_item.setFont(seg_num_font)
28962
29122
  self.table.setItem(row, 0, id_item)
28963
29123
 
28964
29124
  # Type - show segment type based on style and content
@@ -29041,13 +29201,17 @@ class SupervertalerQt(QMainWindow):
29041
29201
  type_item = QTableWidgetItem(type_display)
29042
29202
  type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable) # Read-only
29043
29203
  type_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
29044
-
29204
+
29045
29205
  # Color-code by type for better visibility
29046
29206
  if type_display in ("H1", "H2", "H3", "H4", "Title"):
29047
29207
  type_item.setForeground(QColor("#1976D2")) # Blue for headings (works in both themes)
29048
29208
  elif type_display.startswith("#") or type_display in ("•", "li"):
29049
29209
  type_item.setForeground(QColor("#388E3C")) # Green for list items (works in both themes)
29050
-
29210
+
29211
+ # Smaller font for type symbols
29212
+ type_font = QFont(self.default_font_family, max(8, self.default_font_size - 2))
29213
+ type_item.setFont(type_font)
29214
+
29051
29215
  self.table.setItem(row, 1, type_item)
29052
29216
 
29053
29217
  # Source - Use read-only QTextEdit widget for easy text selection
@@ -29324,9 +29488,13 @@ class SupervertalerQt(QMainWindow):
29324
29488
  termview_layout.setContentsMargins(4, 4, 4, 0)
29325
29489
  termview_layout.setSpacing(2)
29326
29490
 
29327
- # Termview header label
29491
+ # Termview header label (theme-aware)
29328
29492
  termview_header = QLabel("📖 Termview")
29329
- termview_header.setStyleSheet("font-weight: bold; font-size: 9px; color: #666;")
29493
+ if hasattr(self, 'theme_manager') and self.theme_manager:
29494
+ header_color = self.theme_manager.current_theme.text_disabled
29495
+ else:
29496
+ header_color = "#666"
29497
+ termview_header.setStyleSheet(f"font-weight: bold; font-size: 9px; color: {header_color};")
29330
29498
  termview_layout.addWidget(termview_header)
29331
29499
 
29332
29500
  # Third Termview instance for Match Panel
@@ -29350,11 +29518,19 @@ class SupervertalerQt(QMainWindow):
29350
29518
  tm_layout = QHBoxLayout(tm_container)
29351
29519
  tm_layout.setContentsMargins(0, 0, 0, 0)
29352
29520
  tm_layout.setSpacing(0)
29353
-
29354
- # Hardcode the green color for TM boxes (same as TM Target in Compare Panel)
29355
- tm_box_bg = "#d4edda" # Green (same as TM Target in Compare Panel)
29356
- text_color = "#333"
29357
- border_color = "#ddd"
29521
+
29522
+ # Get theme-aware colors for TM boxes (same as Compare Panel)
29523
+ if hasattr(self, 'theme_manager') and self.theme_manager:
29524
+ theme = self.theme_manager.current_theme
29525
+ is_dark = getattr(theme, 'is_dark', 'dark' in theme.name.lower())
29526
+ border_color = theme.border
29527
+ text_color = theme.text
29528
+ # Green background - theme-appropriate shade
29529
+ tm_box_bg = theme.panel_success if hasattr(theme, 'panel_success') else ("#1e3a2f" if is_dark else "#d4edda")
29530
+ else:
29531
+ tm_box_bg = "#d4edda" # Green (light mode)
29532
+ text_color = "#333"
29533
+ border_color = "#ddd"
29358
29534
 
29359
29535
  # TM Source box (GREEN, with navigation)
29360
29536
  self.match_panel_tm_matches = [] # Separate match list
@@ -29496,42 +29672,58 @@ class SupervertalerQt(QMainWindow):
29496
29672
  header_layout.addWidget(nav_label)
29497
29673
 
29498
29674
  if has_navigation:
29499
-
29500
- # Prev button
29501
- prev_btn = QPushButton("")
29502
- prev_btn.setFixedSize(18, 16)
29503
- prev_btn.setStyleSheet(f"""
29504
- QPushButton {{
29505
- font-size: 9px;
29506
- padding: 0px;
29507
- background: transparent;
29508
- border: 1px solid {border_color};
29509
- border-radius: 2px;
29510
- color: {text_color};
29511
- }}
29512
- QPushButton:hover {{
29513
- background: rgba(128,128,128,0.2);
29514
- }}
29515
- """)
29675
+ # Detect theme for arrow color
29676
+ is_dark_theme = hasattr(self, 'theme_manager') and self.theme_manager and 'dark' in self.theme_manager.current_theme.name.lower()
29677
+ arrow_color = "#FFFFFF" if is_dark_theme else "#333333"
29678
+
29679
+ # Create clickable label class with theme update capability
29680
+ from PyQt6.QtCore import pyqtSignal
29681
+
29682
+ class ClickableArrow(QLabel):
29683
+ clicked = pyqtSignal()
29684
+
29685
+ def __init__(self, arrow_symbol, parent=None):
29686
+ """arrow_symbol: '◀' or '▶'"""
29687
+ self.arrow_symbol = arrow_symbol
29688
+ super().__init__("", parent)
29689
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
29690
+
29691
+ def set_color(self, color):
29692
+ """Update arrow color for current theme"""
29693
+ self.setStyleSheet(f"""
29694
+ QLabel {{
29695
+ color: {color};
29696
+ background: transparent;
29697
+ border: none;
29698
+ font-size: 11px;
29699
+ font-weight: bold;
29700
+ }}
29701
+ """)
29702
+ self.setText(self.arrow_symbol)
29703
+
29704
+ def mousePressEvent(self, event):
29705
+ self.clicked.emit()
29706
+ super().mousePressEvent(event)
29707
+
29708
+ # Prev arrow - using ◀
29709
+ prev_btn = ClickableArrow("◀")
29710
+ prev_btn.set_color(arrow_color)
29711
+ prev_btn.setFixedSize(16, 16)
29712
+ prev_btn.setAlignment(Qt.AlignmentFlag.AlignCenter)
29516
29713
  header_layout.addWidget(prev_btn)
29517
-
29518
- # Next button
29519
- next_btn = QPushButton("")
29520
- next_btn.setFixedSize(18, 16)
29521
- next_btn.setStyleSheet(f"""
29522
- QPushButton {{
29523
- font-size: 9px;
29524
- padding: 0px;
29525
- background: transparent;
29526
- border: 1px solid {border_color};
29527
- border-radius: 2px;
29528
- color: {text_color};
29529
- }}
29530
- QPushButton:hover {{
29531
- background: rgba(128,128,128,0.2);
29532
- }}
29533
- """)
29714
+
29715
+ # Next arrow - using ▶
29716
+ next_btn = ClickableArrow("")
29717
+ next_btn.set_color(arrow_color)
29718
+ next_btn.setFixedSize(16, 16)
29719
+ next_btn.setAlignment(Qt.AlignmentFlag.AlignCenter)
29534
29720
  header_layout.addWidget(next_btn)
29721
+
29722
+ # Store reference for theme updates
29723
+ if not hasattr(self, 'theme_aware_arrows'):
29724
+ self.theme_aware_arrows = []
29725
+ self.theme_aware_arrows.extend([prev_btn, next_btn])
29726
+
29535
29727
  nav_buttons = [prev_btn, next_btn]
29536
29728
 
29537
29729
  header_layout.addStretch()
@@ -30460,8 +30652,8 @@ class SupervertalerQt(QMainWindow):
30460
30652
  status_label = QLabel(status_def.icon)
30461
30653
  status_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
30462
30654
  status_label.setToolTip(status_def.label)
30463
- # Slightly smaller X for "not_started" to match other icons better
30464
- font_size = "10px" if segment.status == "not_started" else "14px"
30655
+ # Smaller red X for "not_started" to match green checkmark visual size
30656
+ font_size = "8px" if segment.status == "not_started" else "13px"
30465
30657
  # Make confirmed checkmark green
30466
30658
  color = "color: #2e7d32;" if segment.status == "confirmed" else ""
30467
30659
 
@@ -30638,7 +30830,7 @@ class SupervertalerQt(QMainWindow):
30638
30830
 
30639
30831
  self.table.setFont(font)
30640
30832
 
30641
- # Also update header font
30833
+ # Also update header font - same size as grid content, just bold
30642
30834
  header_font = QFont(self.default_font_family, self.default_font_size, QFont.Weight.Bold)
30643
30835
  self.table.horizontalHeader().setFont(header_font)
30644
30836
 
@@ -30970,8 +31162,8 @@ class SupervertalerQt(QMainWindow):
30970
31162
  self.show_translation_results_pane = settings.get('show_translation_results_pane', False)
30971
31163
  self.show_compare_panel = settings.get('show_compare_panel', True)
30972
31164
 
30973
- # 🧪 EXPERIMENTAL: Load cache kill switch setting (default: True = caches disabled for stability)
30974
- self.disable_all_caches = settings.get('disable_all_caches', True)
31165
+ # Load cache kill switch setting (default: False = caches ENABLED for performance)
31166
+ self.disable_all_caches = settings.get('disable_all_caches', False)
30975
31167
 
30976
31168
  # Load LLM provider settings for AI Assistant
30977
31169
  llm_settings = self.load_llm_settings()
@@ -31384,7 +31576,7 @@ class SupervertalerQt(QMainWindow):
31384
31576
  """Handle cell selection change"""
31385
31577
  if self.debug_mode_enabled:
31386
31578
  self.log(f"🎯 on_cell_selected called: row {current_row}, col {current_col}")
31387
-
31579
+
31388
31580
  # 🚫 GUARD: Don't re-run lookups if we're staying on the same row
31389
31581
  # This prevents lookups when user edits text (focus changes within same row)
31390
31582
  if hasattr(self, '_last_selected_row') and self._last_selected_row == current_row:
@@ -31392,34 +31584,35 @@ class SupervertalerQt(QMainWindow):
31392
31584
  self.log(f"⏭️ Skipping lookup - already on row {current_row}")
31393
31585
  return
31394
31586
  self._last_selected_row = current_row
31395
-
31396
- # ⚡ FAST PATH: For arrow key OR Ctrl+Enter navigation, defer heavy lookups
31397
- # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
31398
- is_arrow_nav = getattr(self, '_arrow_key_navigation', False)
31399
- is_ctrl_enter_nav = getattr(self, '_ctrl_enter_navigation', False)
31400
-
31401
- if is_arrow_nav or is_ctrl_enter_nav:
31402
- self._arrow_key_navigation = False # Reset flags
31403
- self._ctrl_enter_navigation = False
31404
-
31405
- # Schedule deferred lookup with short delay (150ms) for rapid navigation
31406
- if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
31407
- self._deferred_lookup_timer.stop()
31408
- from PyQt6.QtCore import QTimer
31409
- self._deferred_lookup_timer = QTimer()
31410
- self._deferred_lookup_timer.setSingleShot(True)
31411
- self._deferred_lookup_timer.timeout.connect(
31412
- lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
31413
- self._on_cell_selected_full(r, c, pr, pc)
31414
- )
31415
- self._deferred_lookup_timer.start(150) # 150ms debounce
31416
-
31417
- # Do minimal UI update immediately (orange highlight, scroll)
31587
+
31588
+ # ⚡ FILTER MODE: Skip ALL heavy lookups when text filters are active
31589
+ # User is quickly navigating through filtered results - don't slow them down
31590
+ is_filtering = getattr(self, 'filtering_active', False)
31591
+ if is_filtering:
31592
+ # Only do minimal UI update (orange highlight) - no TM/termbase lookups
31418
31593
  self._on_cell_selected_minimal(current_row, previous_row)
31419
31594
  return
31420
-
31421
- # Full processing for non-arrow-key navigation (click, etc.)
31422
- self._on_cell_selected_full(current_row, current_col, previous_row, previous_col)
31595
+
31596
+ # FAST PATH: Defer heavy lookups for ALL navigation (arrow keys, Ctrl+Enter, AND mouse clicks)
31597
+ # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
31598
+ # Reset any navigation flags
31599
+ self._arrow_key_navigation = False
31600
+ self._ctrl_enter_navigation = False
31601
+
31602
+ # Schedule deferred lookup with short delay - debounce prevents hammering during rapid navigation
31603
+ if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
31604
+ self._deferred_lookup_timer.stop()
31605
+ from PyQt6.QtCore import QTimer
31606
+ self._deferred_lookup_timer = QTimer()
31607
+ self._deferred_lookup_timer.setSingleShot(True)
31608
+ self._deferred_lookup_timer.timeout.connect(
31609
+ lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
31610
+ self._on_cell_selected_full(r, c, pr, pc)
31611
+ )
31612
+ self._deferred_lookup_timer.start(10) # 10ms - just enough to batch rapid arrow key holding
31613
+
31614
+ # Do minimal UI update immediately (orange highlight, scroll)
31615
+ self._on_cell_selected_minimal(current_row, previous_row)
31423
31616
 
31424
31617
  def _center_row_in_viewport(self, row: int):
31425
31618
  """Center the given row vertically in the visible table viewport.
@@ -31679,9 +31872,25 @@ class SupervertalerQt(QMainWindow):
31679
31872
  if has_fuzzy_match and not has_100_match:
31680
31873
  self._play_sound_effect('tm_fuzzy_match')
31681
31874
 
31682
- # Skip the slow lookup below, we already have everything
31683
- # Continue to prefetch trigger at the end
31875
+ # Skip the slow TERMBASE lookup below, we already have termbase matches cached
31876
+ # But TM lookup was skipped in prefetch (not thread-safe), so schedule it now
31684
31877
  matches_dict = cached_matches # Set for later use
31878
+
31879
+ # v1.9.182: Schedule TM lookup even on cache hit (prefetch skips TM - not thread-safe)
31880
+ tm_count = len(cached_matches.get("TM", []))
31881
+ if tm_count == 0 and self.enable_tm_matching:
31882
+ find_replace_active = getattr(self, 'find_replace_active', False)
31883
+ if not find_replace_active:
31884
+ # Get termbase matches for the lookup
31885
+ termbase_matches_for_tm = [
31886
+ {
31887
+ 'source_term': match.source,
31888
+ 'target_term': match.target,
31889
+ 'termbase_name': match.metadata.get('termbase_name', '') if match.metadata else '',
31890
+ }
31891
+ for match in cached_matches.get("Termbases", [])
31892
+ ]
31893
+ self._schedule_mt_and_llm_matches(segment, termbase_matches_for_tm)
31685
31894
 
31686
31895
  # Check if TM/Termbase matching is enabled
31687
31896
  if not matches_dict and (not self.enable_tm_matching and not self.enable_termbase_matching):
@@ -31902,15 +32111,19 @@ class SupervertalerQt(QMainWindow):
31902
32111
 
31903
32112
  # Schedule expensive searches (TM, MT, LLM) with debouncing to prevent UI blocking
31904
32113
  # ONLY schedule if:
31905
- # 1. Cache miss (no prefetched matches)
32114
+ # 1. Cache miss OR cache hit with no TM matches (prefetch doesn't include TM - not thread-safe)
31906
32115
  # 2. TM matching is enabled
31907
32116
  # 3. Find/Replace is not active (to avoid slowdowns during navigation)
32117
+ needs_tm_lookup = True
31908
32118
  with self.translation_matches_cache_lock:
31909
- cache_hit = segment_id in self.translation_matches_cache
31910
-
32119
+ if segment_id in self.translation_matches_cache:
32120
+ cached = self.translation_matches_cache[segment_id]
32121
+ # v1.9.182: Check if TM matches exist - prefetch worker skips TM lookups
32122
+ needs_tm_lookup = len(cached.get("TM", [])) == 0
32123
+
31911
32124
  find_replace_active = getattr(self, 'find_replace_active', False)
31912
-
31913
- if not cache_hit and self.enable_tm_matching and not find_replace_active:
32125
+
32126
+ if needs_tm_lookup and self.enable_tm_matching and not find_replace_active:
31914
32127
  # Get termbase matches if they exist (could be None or empty)
31915
32128
  termbase_matches = matches_dict.get('Termbases', []) if matches_dict else []
31916
32129
  self._schedule_mt_and_llm_matches(segment, termbase_matches)
@@ -31922,9 +32135,7 @@ class SupervertalerQt(QMainWindow):
31922
32135
  next_segment_ids = []
31923
32136
  start_idx = current_row + 1
31924
32137
  end_idx = min(start_idx + 20, len(self.current_project.segments))
31925
-
31926
- print(f"[PROACTIVE NAV DEBUG] Navigation to row {current_row}, checking segments {start_idx} to {end_idx}")
31927
-
32138
+
31928
32139
  for seg in self.current_project.segments[start_idx:end_idx]:
31929
32140
  # Check if already cached
31930
32141
  with self.translation_matches_cache_lock:
@@ -31938,15 +32149,12 @@ class SupervertalerQt(QMainWindow):
31938
32149
  try:
31939
32150
  with self.termbase_cache_lock:
31940
32151
  termbase_raw = self.termbase_cache.get(seg.id, {})
31941
- print(f"[PROACTIVE NAV DEBUG] Seg {seg.id}: cached, termbase_raw has {len(termbase_raw) if termbase_raw else 0} matches")
31942
32152
  if termbase_raw:
31943
32153
  termbase_json = json.dumps(termbase_raw)
31944
- print(f"[PROACTIVE NAV DEBUG] Calling _apply_proactive_highlighting for seg {seg.id}")
31945
32154
  self._apply_proactive_highlighting(seg.id, termbase_json)
31946
- except Exception as e:
31947
- print(f"[PROACTIVE NAV DEBUG] Error for seg {seg.id}: {e}")
31948
-
31949
- print(f"[PROACTIVE NAV DEBUG] Need to prefetch: {len(next_segment_ids)} segments")
32155
+ except Exception:
32156
+ pass # Silent failure for proactive highlighting
32157
+
31950
32158
  if next_segment_ids:
31951
32159
  self._start_prefetch_worker(next_segment_ids)
31952
32160
 
@@ -34349,18 +34557,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34349
34557
  """
34350
34558
  Find all termbase matches in source text
34351
34559
  Returns dict of {term: translation} for all matches found
34560
+
34561
+ v1.9.182: Uses in-memory index for instant lookup when available.
34562
+ Falls back to per-word database queries if index not built.
34352
34563
  """
34353
34564
  if not source_text or not hasattr(self, 'db_manager') or not self.db_manager:
34354
34565
  return {}
34355
34566
 
34356
34567
  try:
34568
+ # v1.9.182: Use in-memory index for instant lookup (1000x faster)
34569
+ # The index is built on project load by _build_termbase_index()
34570
+ with self.termbase_index_lock:
34571
+ has_index = bool(self.termbase_index)
34572
+
34573
+ if has_index:
34574
+ # Fast path: use pre-built in-memory index
34575
+ return self._search_termbase_in_memory(source_text)
34576
+
34577
+ # Fallback: original per-word database query approach
34578
+ # (only used if index not yet built, e.g., during startup)
34357
34579
  source_lang = self.current_project.source_lang if self.current_project else None
34358
34580
  target_lang = self.current_project.target_lang if self.current_project else None
34359
-
34581
+
34360
34582
  # Convert language names to codes for termbase search
34361
34583
  source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
34362
34584
  target_lang_code = self._convert_language_to_code(target_lang) if target_lang else None
34363
-
34585
+
34364
34586
  # Strip HTML/XML/CAT tool tags from source text before word splitting
34365
34587
  # This handles <b>, </b>, <i>, memoQ {1}, [2}, Trados <1>, Déjà Vu {00001}, etc.
34366
34588
  import re
@@ -34370,7 +34592,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34370
34592
  # memoQ content tags: [uicontrol id="..."} or {uicontrol] or [tagname ...} or {tagname]
34371
34593
  clean_source_text = re.sub(r'\[[^\[\]]*\}', '', clean_source_text) # Opening: [anything}
34372
34594
  clean_source_text = re.sub(r'\{[^\{\}]*\]', '', clean_source_text) # Closing: {anything]
34373
-
34595
+
34374
34596
  # Search termbases for all terms that appear in the source text
34375
34597
  # Split source text into words and search for each one
34376
34598
  words = clean_source_text.split()
@@ -34592,23 +34814,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34592
34814
  termbase_matches_json: JSON-encoded termbase matches dict (thread-safe transfer)
34593
34815
  """
34594
34816
  import json
34595
-
34596
- print(f"[PROACTIVE DEBUG] _apply_proactive_highlighting called for segment {segment_id}")
34597
-
34817
+
34598
34818
  if not self.current_project or not self.table:
34599
- print(f"[PROACTIVE DEBUG] Early exit: no project or table")
34600
34819
  return
34601
-
34820
+
34602
34821
  try:
34603
34822
  # Decode the matches from JSON
34604
34823
  termbase_matches = json.loads(termbase_matches_json) if termbase_matches_json else {}
34605
-
34606
- print(f"[PROACTIVE DEBUG] Decoded {len(termbase_matches)} termbase matches")
34607
-
34824
+
34608
34825
  if not termbase_matches:
34609
- print(f"[PROACTIVE DEBUG] No matches to highlight, returning")
34610
34826
  return # Nothing to highlight
34611
-
34827
+
34612
34828
  # Find the row for this segment ID
34613
34829
  row = -1
34614
34830
  for r in range(self.table.rowCount()):
@@ -34621,44 +34837,25 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
34621
34837
  break
34622
34838
  except ValueError:
34623
34839
  continue
34624
-
34625
- print(f"[PROACTIVE DEBUG] Found row {row} for segment {segment_id}")
34626
-
34840
+
34627
34841
  if row < 0:
34628
- print(f"[PROACTIVE DEBUG] Segment not visible in current page")
34629
34842
  return # Segment not visible in current page
34630
-
34843
+
34631
34844
  # Get segment source text
34632
34845
  segment = None
34633
34846
  for seg in self.current_project.segments:
34634
34847
  if seg.id == segment_id:
34635
34848
  segment = seg
34636
34849
  break
34637
-
34850
+
34638
34851
  if not segment:
34639
- print(f"[PROACTIVE DEBUG] Segment object not found")
34640
34852
  return
34641
-
34642
- print(f"[PROACTIVE DEBUG] Applying highlight_source_with_termbase to row {row}")
34643
- print(f"[PROACTIVE DEBUG] Source text: {segment.source[:80]}...")
34644
- print(f"[PROACTIVE DEBUG] Matches keys: {list(termbase_matches.keys())[:5]}")
34645
- if termbase_matches:
34646
- first_key = list(termbase_matches.keys())[0]
34647
- print(f"[PROACTIVE DEBUG] Sample match: {first_key} => {termbase_matches[first_key]}")
34648
-
34649
- # Check if the source widget exists and is the right type
34650
- source_widget = self.table.cellWidget(row, 2)
34651
- print(f"[PROACTIVE DEBUG] Source widget type: {type(source_widget).__name__ if source_widget else 'None'}")
34652
- print(f"[PROACTIVE DEBUG] Has highlight method: {hasattr(source_widget, 'highlight_termbase_matches') if source_widget else 'N/A'}")
34653
-
34853
+
34654
34854
  # Apply highlighting (this updates the source cell widget)
34655
34855
  self.highlight_source_with_termbase(row, segment.source, termbase_matches)
34656
- print(f"[PROACTIVE DEBUG] ✅ Highlighting applied successfully")
34657
-
34658
- except Exception as e:
34659
- print(f"[PROACTIVE DEBUG] ERROR: {e}")
34660
- import traceback
34661
- print(f"[PROACTIVE DEBUG] Traceback: {traceback.format_exc()}")
34856
+
34857
+ except Exception:
34858
+ pass # Silent failure for proactive highlighting
34662
34859
 
34663
34860
  def insert_term_translation(self, row: int, translation: str):
34664
34861
  """
@@ -38828,95 +39025,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38828
39025
 
38829
39026
  self.table.clearSelection()
38830
39027
  self.table.setCurrentCell(row, 3) # Column 3 = Target (widget column)
39028
+ self.table.selectRow(row) # v1.9.182: Ensure row is visually selected
39029
+ # Ensure the row is visible by scrolling to it
39030
+ self.table.scrollToItem(self.table.item(row, 0), QTableWidget.ScrollHint.PositionAtCenter)
38831
39031
  self.log(f"⏭️ Moved to next unconfirmed segment {seg.id}")
38832
-
38833
- # Auto-confirm 100% TM matches if setting is enabled
38834
- if self.auto_confirm_100_percent_matches:
38835
- # Get TM matches for this segment
38836
- exact_match = None
38837
- if self.enable_tm_matching and hasattr(self, 'db_manager') and self.db_manager:
38838
- # Get activated TM IDs from project settings
38839
- activated_tm_ids = []
38840
- if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
38841
- activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
38842
-
38843
- if activated_tm_ids:
38844
- # Use get_exact_match for 100% matches instead of fuzzy search
38845
- source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
38846
- target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
38847
- exact_match = self.db_manager.get_exact_match(
38848
- seg.source,
38849
- tm_ids=activated_tm_ids,
38850
- source_lang=source_lang,
38851
- target_lang=target_lang
38852
- )
38853
-
38854
- # Check if there's a 100% match and (target is empty OR overwrite is enabled)
38855
- target_is_empty = not seg.target.strip()
38856
- can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
38857
-
38858
- if exact_match and can_auto_confirm:
38859
- match_target = exact_match.get('target_text', '')
38860
- overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
38861
- self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
38862
-
38863
- # Insert the match into the target cell
38864
- target_widget = self.table.cellWidget(row, 3)
38865
- if target_widget and match_target:
38866
- target_widget.setPlainText(match_target)
38867
- seg.target = match_target
38868
- seg.status = 'confirmed'
38869
- self.update_status_icon(row, 'confirmed')
38870
- self.project_modified = True
38871
-
38872
- # Save to TM
38873
- try:
38874
- self.save_segment_to_activated_tms(seg.source, seg.target)
38875
- self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
38876
- except Exception as e:
38877
- self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
38878
-
38879
- # Continue to the NEXT unconfirmed segment (skip this one)
38880
- for next_row in range(row + 1, self.table.rowCount()):
38881
- if next_row < len(self.current_project.segments):
38882
- next_seg = self.current_project.segments[next_row]
38883
- if next_seg.status not in ['confirmed', 'approved']:
38884
- # Check pagination
38885
- if self.table.isRowHidden(next_row):
38886
- if hasattr(self, 'grid_page_size') and hasattr(self, 'grid_current_page'):
38887
- target_page = next_row // self.grid_page_size
38888
- if target_page != self.grid_current_page:
38889
- self.grid_current_page = target_page
38890
- self._update_pagination_ui()
38891
- self._apply_pagination_to_grid()
38892
-
38893
- # ⚡ INSTANT NAVIGATION
38894
- self._ctrl_enter_navigation = True
38895
-
38896
- self.table.clearSelection()
38897
- self.table.setCurrentCell(next_row, 3)
38898
- self.log(f"⏭️ Auto-skipped to next unconfirmed segment {next_seg.id}")
38899
- next_target_widget = self.table.cellWidget(next_row, 3)
38900
- if next_target_widget:
38901
- next_target_widget.setFocus()
38902
- next_target_widget.moveCursor(QTextCursor.MoveOperation.End)
38903
-
38904
- # Recursively check if this next segment also has a 100% match
38905
- self.confirm_and_next_unconfirmed()
38906
- return
38907
-
38908
- # No more unconfirmed segments after this one
38909
- self.log("✅ No more unconfirmed segments after auto-confirm")
38910
- # Update status bar after auto-confirming
38911
- self.update_progress_stats()
38912
- return
38913
-
38914
- # Get the target cell widget and set focus to it (normal behavior without auto-confirm)
39032
+
39033
+ # v1.9.182: Explicitly update termview (don't rely on deferred signal)
39034
+ self._update_termview_for_segment(seg)
39035
+
39036
+ # v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
39037
+ if self.enable_tm_matching:
39038
+ find_replace_active = getattr(self, 'find_replace_active', False)
39039
+ if not find_replace_active:
39040
+ self._schedule_mt_and_llm_matches(seg, [])
39041
+
39042
+ # Get the target cell widget and set focus to it IMMEDIATELY
39043
+ # (moved BEFORE auto-confirm check for instant responsiveness)
38915
39044
  target_widget = self.table.cellWidget(row, 3)
38916
39045
  if target_widget:
38917
39046
  target_widget.setFocus()
38918
39047
  # Move cursor to end of text
38919
39048
  target_widget.moveCursor(QTextCursor.MoveOperation.End)
39049
+
39050
+ # v1.9.182: Defer auto-confirm check to not block navigation
39051
+ # The TM lookup is slow - do it asynchronously after navigation completes
39052
+ if self.auto_confirm_100_percent_matches:
39053
+ QTimer.singleShot(50, lambda r=row, s=seg: self._check_auto_confirm_100_percent(r, s))
38920
39054
  return
38921
39055
 
38922
39056
  # No more unconfirmed segments, just go to next
@@ -38938,14 +39072,106 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
38938
39072
 
38939
39073
  self.table.clearSelection()
38940
39074
  self.table.setCurrentCell(next_row, 3) # Column 3 = Target (widget column)
39075
+ self.table.selectRow(next_row) # v1.9.182: Ensure row is visually selected
39076
+ # Ensure the row is visible by scrolling to it
39077
+ self.table.scrollToItem(self.table.item(next_row, 0), QTableWidget.ScrollHint.PositionAtCenter)
38941
39078
  self.log(f"⏭️ Moved to next segment (all remaining confirmed)")
39079
+
39080
+ # v1.9.182: Explicitly update termview (don't rely on deferred signal)
39081
+ if next_row < len(self.current_project.segments):
39082
+ next_seg = self.current_project.segments[next_row]
39083
+ self._update_termview_for_segment(next_seg)
39084
+
39085
+ # v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
39086
+ if self.enable_tm_matching:
39087
+ find_replace_active = getattr(self, 'find_replace_active', False)
39088
+ if not find_replace_active:
39089
+ self._schedule_mt_and_llm_matches(next_seg, [])
39090
+
38942
39091
  # Get the target cell widget and set focus to it
38943
39092
  target_widget = self.table.cellWidget(next_row, 3)
38944
39093
  if target_widget:
38945
39094
  target_widget.setFocus()
38946
39095
  # Move cursor to end of text
38947
39096
  target_widget.moveCursor(QTextCursor.MoveOperation.End)
38948
-
39097
+
39098
+ def _check_auto_confirm_100_percent(self, row: int, seg):
39099
+ """
39100
+ v1.9.182: Deferred auto-confirm check for 100% TM matches.
39101
+
39102
+ This is called asynchronously after Ctrl+Enter navigation to avoid blocking
39103
+ the UI thread with slow TM database queries.
39104
+ """
39105
+ try:
39106
+ # Verify we're still on the same segment (user may have navigated away)
39107
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
39108
+ if current_row != row:
39109
+ return # User has moved - don't auto-confirm wrong segment
39110
+
39111
+ if not self.enable_tm_matching or not hasattr(self, 'db_manager') or not self.db_manager:
39112
+ return
39113
+
39114
+ # Get activated TM IDs from project settings
39115
+ activated_tm_ids = []
39116
+ if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
39117
+ activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
39118
+
39119
+ if not activated_tm_ids:
39120
+ return
39121
+
39122
+ # Use get_exact_match for 100% matches
39123
+ source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
39124
+ target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
39125
+ exact_match = self.db_manager.get_exact_match(
39126
+ seg.source,
39127
+ tm_ids=activated_tm_ids,
39128
+ source_lang=source_lang,
39129
+ target_lang=target_lang
39130
+ )
39131
+
39132
+ if not exact_match:
39133
+ return
39134
+
39135
+ # Check if there's a 100% match and (target is empty OR overwrite is enabled)
39136
+ target_is_empty = not seg.target.strip()
39137
+ can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
39138
+
39139
+ if not can_auto_confirm:
39140
+ return
39141
+
39142
+ # Verify AGAIN that we're still on the same segment (TM query may have taken time)
39143
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
39144
+ if current_row != row:
39145
+ return # User has moved during TM lookup
39146
+
39147
+ match_target = exact_match.get('target_text', '')
39148
+ if not match_target:
39149
+ return
39150
+
39151
+ overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
39152
+ self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
39153
+
39154
+ # Insert the match into the target cell
39155
+ target_widget = self.table.cellWidget(row, 3)
39156
+ if target_widget:
39157
+ target_widget.setPlainText(match_target)
39158
+ seg.target = match_target
39159
+ seg.status = 'confirmed'
39160
+ self.update_status_icon(row, 'confirmed')
39161
+ self.project_modified = True
39162
+
39163
+ # Save to TM
39164
+ try:
39165
+ self.save_segment_to_activated_tms(seg.source, seg.target)
39166
+ self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
39167
+ except Exception as e:
39168
+ self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
39169
+
39170
+ # Continue to the NEXT unconfirmed segment (skip this one)
39171
+ self.confirm_and_next_unconfirmed()
39172
+ except Exception as e:
39173
+ self.log(f"⚠️ Error in auto-confirm check: {e}")
39174
+
38949
39175
  def confirm_selected_or_next(self):
38950
39176
  """Smart confirm: if multiple segments selected, confirm all; otherwise confirm and go to next.
38951
39177
 
@@ -43763,7 +43989,15 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43763
43989
  # Reapply alternating row colors with new theme
43764
43990
  if hasattr(self, 'apply_alternating_row_colors'):
43765
43991
  self.apply_alternating_row_colors()
43766
-
43992
+
43993
+ # Update navigation arrow colors based on theme
43994
+ if hasattr(self, 'theme_aware_arrows'):
43995
+ is_dark = theme.name == "Dark"
43996
+ arrow_color = "#FFFFFF" if is_dark else "#333333"
43997
+ for arrow in self.theme_aware_arrows:
43998
+ if hasattr(arrow, 'set_color'):
43999
+ arrow.set_color(arrow_color)
44000
+
43767
44001
  # Refresh segment numbers color
43768
44002
  if hasattr(self, 'table') and self.table:
43769
44003
  # Determine segment number color based on theme
@@ -43800,6 +44034,11 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
43800
44034
  if hasattr(self.termview_widget, 'apply_theme'):
43801
44035
  self.termview_widget.apply_theme()
43802
44036
 
44037
+ # Also refresh Match Panel TermView (right panel)
44038
+ if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
44039
+ if hasattr(self.termview_widget_match, 'apply_theme'):
44040
+ self.termview_widget_match.apply_theme()
44041
+
43803
44042
  def show_file_progress_dialog(self):
43804
44043
  """Show File Progress dialog for multi-file projects.
43805
44044
 
@@ -44096,12 +44335,12 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44096
44335
  self._pending_mt_llm_segment = segment
44097
44336
  self._pending_termbase_matches = termbase_matches or []
44098
44337
 
44099
- # Start debounced timer - only call APIs after user stops clicking for 0.3 seconds
44338
+ # Start debounced timer - only call APIs after user stops navigating
44100
44339
  from PyQt6.QtCore import QTimer
44101
44340
  self._mt_llm_timer = QTimer()
44102
44341
  self._mt_llm_timer.setSingleShot(True)
44103
44342
  self._mt_llm_timer.timeout.connect(lambda: self._execute_mt_llm_lookup())
44104
- self._mt_llm_timer.start(300) # Wait 0.3 seconds of inactivity
44343
+ self._mt_llm_timer.start(150) # Wait 150ms of inactivity before external API calls
44105
44344
 
44106
44345
  except Exception as e:
44107
44346
  self.log(f"Error scheduling MT/LLM search: {e}")
@@ -44126,7 +44365,21 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44126
44365
  """Search for TM, MT and LLM matches - called only after debounce delay"""
44127
44366
  try:
44128
44367
  from modules.translation_results_panel import TranslationMatch
44129
-
44368
+
44369
+ # v1.9.182: Validate we're still on the same segment before displaying results
44370
+ # This prevents stale results from showing when user navigates quickly
44371
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
44372
+ if current_row >= 0:
44373
+ id_item = self.table.item(current_row, 0)
44374
+ if id_item:
44375
+ try:
44376
+ current_segment_id = int(id_item.text())
44377
+ if current_segment_id != segment.id:
44378
+ # User has moved to a different segment - abort this lookup
44379
+ return
44380
+ except (ValueError, AttributeError):
44381
+ pass
44382
+
44130
44383
  # Get current project languages for all translation services
44131
44384
  source_lang = getattr(self.current_project, 'source_lang', None) if self.current_project else None
44132
44385
  target_lang = getattr(self.current_project, 'target_lang', None) if self.current_project else None
@@ -44199,6 +44452,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44199
44452
 
44200
44453
  # Show TM matches immediately (progressive loading)
44201
44454
  if matches_dict["TM"]:
44455
+ # v1.9.182: Re-validate we're still on same segment before displaying
44456
+ current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
44457
+ if current_row >= 0:
44458
+ id_item = self.table.item(current_row, 0)
44459
+ if id_item:
44460
+ try:
44461
+ current_segment_id = int(id_item.text())
44462
+ if current_segment_id != segment.id:
44463
+ # User moved - still cache results but don't display
44464
+ with self.translation_matches_cache_lock:
44465
+ if segment.id in self.translation_matches_cache:
44466
+ self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
44467
+ return # Don't display stale results
44468
+ except (ValueError, AttributeError):
44469
+ pass
44470
+
44202
44471
  tm_only = {"TM": matches_dict["TM"]}
44203
44472
  if hasattr(self, 'results_panels') and self.results_panels:
44204
44473
  for panel in self.results_panels:
@@ -44252,6 +44521,16 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
44252
44521
  has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in matches_dict["TM"])
44253
44522
  if has_fuzzy_match and not has_100_match:
44254
44523
  self._play_sound_effect('tm_fuzzy_match')
44524
+
44525
+ # v1.9.182: Update cache with TM results so subsequent visits are instant
44526
+ if matches_dict["TM"]:
44527
+ with self.translation_matches_cache_lock:
44528
+ if segment.id in self.translation_matches_cache:
44529
+ # Merge TM results into existing cache entry
44530
+ self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
44531
+ else:
44532
+ # Create new cache entry with TM results
44533
+ self.translation_matches_cache[segment.id] = matches_dict
44255
44534
  except Exception as e:
44256
44535
  self.log(f"Error in delayed TM search: {e}")
44257
44536
 
@@ -46830,10 +47109,8 @@ class SuperlookupTab(QWidget):
46830
47109
  for row in db_manager.cursor.fetchall():
46831
47110
  if row[0]:
46832
47111
  all_languages.add(row[0])
46833
- except Exception as e:
46834
- print(f"[DEBUG] Error getting languages from TMs: {e}")
46835
- else:
46836
- print(f"[DEBUG] No db_manager available for language population")
47112
+ except Exception:
47113
+ pass # Silent failure for language population
46837
47114
 
46838
47115
  # Get languages from termbases
46839
47116
  if termbase_mgr:
@@ -46844,8 +47121,8 @@ class SuperlookupTab(QWidget):
46844
47121
  all_languages.add(tb['source_lang'])
46845
47122
  if tb.get('target_lang'):
46846
47123
  all_languages.add(tb['target_lang'])
46847
- except Exception as e:
46848
- print(f"[DEBUG] Error getting languages from termbases: {e}")
47124
+ except Exception:
47125
+ pass # Silent failure for language population
46849
47126
 
46850
47127
  # Group languages by their base language name
46851
47128
  # E.g., "en", "en-US", "en-GB", "English" all map to "English"
@@ -46875,8 +47152,6 @@ class SuperlookupTab(QWidget):
46875
47152
  # Store variants list as the data for this item
46876
47153
  self.lang_from_combo.addItem(base_name, variants)
46877
47154
  self.lang_to_combo.addItem(base_name, variants)
46878
-
46879
- print(f"[DEBUG] Populated language dropdowns with {len(sorted_base_langs)} base languages (from {len(all_languages)} variants)")
46880
47155
 
46881
47156
  def _get_base_language_name(self, lang_code):
46882
47157
  """Extract the base language name from any language code or name.
@@ -47063,37 +47338,20 @@ class SuperlookupTab(QWidget):
47063
47338
  selected_tm_ids = self.get_selected_tm_ids()
47064
47339
  search_direction = self.get_search_direction()
47065
47340
  from_lang, to_lang = self.get_language_filters()
47066
-
47067
- # Write language info to debug file
47068
- with open('superlookup_debug.txt', 'a') as f:
47069
- f.write(f"Language filters: from_lang='{from_lang}', to_lang='{to_lang}'\\n")
47070
- f.write(f"Search direction: {search_direction}\\n")
47071
-
47072
- print(f"[DEBUG] Superlookup: Selected TM IDs: {selected_tm_ids}, direction: {search_direction}", flush=True)
47073
- print(f"[DEBUG] Superlookup: Language filters: from={from_lang}, to={to_lang}", flush=True)
47074
- print(f"[DEBUG] Superlookup: tm_database = {self.tm_database}", flush=True)
47341
+
47075
47342
  if self.engine:
47076
47343
  self.engine.set_enabled_tm_ids(selected_tm_ids if selected_tm_ids else None)
47077
47344
 
47078
47345
  # Perform TM lookup with direction and language filters
47079
47346
  tm_results = []
47080
47347
  if self.tm_database:
47081
- print(f"[DEBUG] Superlookup: Searching TM for '{text[:50]}...'", flush=True)
47082
- tm_results = self.engine.search_tm(text, direction=search_direction,
47348
+ tm_results = self.engine.search_tm(text, direction=search_direction,
47083
47349
  source_lang=from_lang, target_lang=to_lang)
47084
- print(f"[DEBUG] Superlookup: Got {len(tm_results)} TM results", flush=True)
47085
- else:
47086
- print(f"[DEBUG] Superlookup: tm_database is None, skipping TM search!", flush=True)
47087
-
47350
+
47088
47351
  # Perform termbase lookup (search Supervertaler termbases directly)
47089
- print(f"[DEBUG] About to call search_termbases with from_lang='{from_lang}', to_lang='{to_lang}'", flush=True)
47090
47352
  try:
47091
47353
  termbase_results = self.search_termbases(text, source_lang=from_lang, target_lang=to_lang)
47092
- print(f"[DEBUG] search_termbases returned {len(termbase_results)} results", flush=True)
47093
- except Exception as e:
47094
- print(f"[DEBUG] ERROR in search_termbases: {e}", flush=True)
47095
- import traceback
47096
- traceback.print_exc()
47354
+ except Exception:
47097
47355
  termbase_results = []
47098
47356
 
47099
47357
  # Perform Supermemory semantic search