supervertaler 1.9.153__py3-none-any.whl → 1.9.164__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.
Supervertaler.py CHANGED
@@ -34,9 +34,9 @@ License: MIT
34
34
  """
35
35
 
36
36
  # Version Information.
37
- __version__ = "1.9.153"
37
+ __version__ = "1.9.164"
38
38
  __phase__ = "0.9"
39
- __release_date__ = "2026-01-23"
39
+ __release_date__ = "2026-01-26"
40
40
  __edition__ = "Qt"
41
41
 
42
42
  import sys
@@ -240,6 +240,7 @@ from modules.find_replace_qt import (
240
240
  HistoryComboBox,
241
241
  ) # F&R History and Sets
242
242
  from modules.shortcut_manager import ShortcutManager # Keyboard shortcut management
243
+ from modules.termview_widget import TermviewWidget # Termview widget for glossary display
243
244
 
244
245
 
245
246
  STATUS_ORDER = [
@@ -1566,11 +1567,15 @@ class ReadOnlyGridTextEditor(QTextEdit):
1566
1567
  """
1567
1568
  from PyQt6.QtGui import QTextCursor, QTextCharFormat, QColor, QFont
1568
1569
 
1570
+ print(f"[HIGHLIGHT DEBUG] highlight_termbase_matches called with {len(matches_dict) if matches_dict else 0} matches")
1571
+
1569
1572
  # Get the document and create a cursor
1570
1573
  doc = self.document()
1571
1574
  text = self.toPlainText()
1572
1575
  text_lower = text.lower()
1573
1576
 
1577
+ print(f"[HIGHLIGHT DEBUG] Widget text length: {len(text)}, text preview: {text[:60]}...")
1578
+
1574
1579
  # IMPORTANT: Always clear all previous formatting first to prevent inconsistent highlighting
1575
1580
  cursor = QTextCursor(doc)
1576
1581
  cursor.select(QTextCursor.SelectionType.Document)
@@ -1579,6 +1584,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
1579
1584
 
1580
1585
  # If no matches, we're done (highlighting has been cleared)
1581
1586
  if not matches_dict:
1587
+ print(f"[HIGHLIGHT DEBUG] No matches, returning after clear")
1582
1588
  return
1583
1589
 
1584
1590
  # Get highlight style from main window settings
@@ -1597,6 +1603,8 @@ class ReadOnlyGridTextEditor(QTextEdit):
1597
1603
  break
1598
1604
  parent = parent.parent() if hasattr(parent, 'parent') else None
1599
1605
 
1606
+ print(f"[HIGHLIGHT DEBUG] Using style: {highlight_style}")
1607
+
1600
1608
  # Sort matches by source term length (longest first) to avoid partial matches
1601
1609
  # Since dict keys are now term_ids, we need to extract source terms first
1602
1610
  term_entries = []
@@ -1606,11 +1614,16 @@ class ReadOnlyGridTextEditor(QTextEdit):
1606
1614
  if source_term:
1607
1615
  term_entries.append((source_term, term_id, match_info))
1608
1616
 
1617
+ print(f"[HIGHLIGHT DEBUG] Built {len(term_entries)} term entries from matches")
1618
+ if term_entries:
1619
+ print(f"[HIGHLIGHT DEBUG] First few terms to search: {[t[0] for t in term_entries[:3]]}")
1620
+
1609
1621
  # Sort by source term length (longest first)
1610
1622
  term_entries.sort(key=lambda x: len(x[0]), reverse=True)
1611
1623
 
1612
1624
  # Track positions we've already highlighted to avoid overlaps
1613
1625
  highlighted_ranges = []
1626
+ found_count = 0
1614
1627
 
1615
1628
  for term, term_id, match_info in term_entries:
1616
1629
  # Get ranking, forbidden status, and termbase type
@@ -1723,11 +1736,14 @@ class ReadOnlyGridTextEditor(QTextEdit):
1723
1736
 
1724
1737
  # Apply format
1725
1738
  cursor.setCharFormat(fmt)
1739
+ found_count += 1
1726
1740
 
1727
1741
  # Track this range as highlighted
1728
1742
  highlighted_ranges.append((idx, end_idx))
1729
1743
 
1730
1744
  start = end_idx
1745
+
1746
+ print(f"[HIGHLIGHT DEBUG] Applied formatting to {found_count} term occurrences in text")
1731
1747
 
1732
1748
  def highlight_non_translatables(self, nt_matches: list, highlighted_ranges: list = None):
1733
1749
  """
@@ -5751,12 +5767,19 @@ class SupervertalerQt(QMainWindow):
5751
5767
  # Signal for thread-safe logging (background threads emit, main thread handles)
5752
5768
  _log_signal = pyqtSignal(str)
5753
5769
 
5770
+ # Signal for proactive highlighting - prefetch worker emits, main thread applies highlighting
5771
+ # Args: segment_id (int), termbase_matches (dict as JSON string for thread safety)
5772
+ _proactive_highlight_signal = pyqtSignal(int, str)
5773
+
5754
5774
  def __init__(self):
5755
5775
  super().__init__()
5756
5776
 
5757
5777
  # Connect thread-safe log signal (must be done first for logging to work from threads)
5758
5778
  self._log_signal.connect(self._log_to_ui)
5759
5779
 
5780
+ # Connect proactive highlighting signal (prefetch worker emits, main thread highlights)
5781
+ self._proactive_highlight_signal.connect(self._apply_proactive_highlighting)
5782
+
5760
5783
  # Application state
5761
5784
  self.current_project: Optional[Project] = None
5762
5785
  self.project_file_path: Optional[str] = None
@@ -5782,7 +5805,7 @@ class SupervertalerQt(QMainWindow):
5782
5805
 
5783
5806
  # Right panel visibility settings
5784
5807
  self.show_translation_results_pane = False # Show Translation Results tab (hidden by default for new users)
5785
- self.show_compare_panel = True # Show Compare Panel tab
5808
+ # Note: Match Panel is always visible (no toggle needed)
5786
5809
 
5787
5810
  # TM and Termbase matching toggle (default: enabled)
5788
5811
  self.enable_tm_matching = True
@@ -5817,6 +5840,9 @@ class SupervertalerQt(QMainWindow):
5817
5840
  # Focus border settings for target cells
5818
5841
  self.focus_border_color = '#f1b79a' # Peach/salmon
5819
5842
  self.focus_border_thickness = 2 # 2px
5843
+
5844
+ # Sound effects settings
5845
+ self.enable_sound_effects = False # Sound effects disabled by default
5820
5846
 
5821
5847
  # Debug mode settings (for troubleshooting performance issues)
5822
5848
  self.debug_mode_enabled = False # Enables verbose debug logging
@@ -5852,6 +5878,14 @@ class SupervertalerQt(QMainWindow):
5852
5878
  self.prefetch_stop_event = threading.Event()
5853
5879
  self.prefetch_queue = [] # List of segment IDs to prefetch
5854
5880
 
5881
+ # Idle prefetch: prefetch next segments while user is thinking/typing
5882
+ self.idle_prefetch_timer = None # QTimer for triggering prefetch after typing pause
5883
+ self.idle_prefetch_delay_ms = 1500 # Start prefetch 1.5s after user stops typing
5884
+
5885
+ # 🧪 EXPERIMENTAL: Cache kill switch for performance testing
5886
+ # When True, all caches are bypassed - direct lookups every time
5887
+ self.disable_all_caches = False
5888
+
5855
5889
  # Undo/Redo stack for grid edits
5856
5890
  self.undo_stack = [] # List of (segment_id, old_target, new_target, old_status, new_status)
5857
5891
  self.redo_stack = [] # List of undone actions that can be redone
@@ -7388,22 +7422,22 @@ class SupervertalerQt(QMainWindow):
7388
7422
  results_note.setEnabled(False)
7389
7423
  results_zoom_menu.addAction(results_note)
7390
7424
 
7391
- # Compare Panel section
7392
- compare_zoom_menu = view_menu.addMenu("🔍 &Compare Panel")
7425
+ # Match Panel zoom section
7426
+ match_panel_zoom_menu = view_menu.addMenu("🔍 &Match Panel")
7393
7427
 
7394
- compare_zoom_in_action = QAction("Compare Panel Zoom &In", self)
7395
- compare_zoom_in_action.setShortcut("Ctrl+Alt+=")
7396
- compare_zoom_in_action.triggered.connect(self.compare_panel_zoom_in)
7397
- compare_zoom_menu.addAction(compare_zoom_in_action)
7428
+ match_panel_zoom_in_action = QAction("Match Panel Zoom &In", self)
7429
+ match_panel_zoom_in_action.setShortcut("Ctrl+Alt+=")
7430
+ match_panel_zoom_in_action.triggered.connect(self.match_panel_zoom_in)
7431
+ match_panel_zoom_menu.addAction(match_panel_zoom_in_action)
7398
7432
 
7399
- compare_zoom_out_action = QAction("Compare Panel Zoom &Out", self)
7400
- compare_zoom_out_action.setShortcut("Ctrl+Alt+-")
7401
- compare_zoom_out_action.triggered.connect(self.compare_panel_zoom_out)
7402
- compare_zoom_menu.addAction(compare_zoom_out_action)
7433
+ match_panel_zoom_out_action = QAction("Match Panel Zoom &Out", self)
7434
+ match_panel_zoom_out_action.setShortcut("Ctrl+Alt+-")
7435
+ match_panel_zoom_out_action.triggered.connect(self.match_panel_zoom_out)
7436
+ match_panel_zoom_menu.addAction(match_panel_zoom_out_action)
7403
7437
 
7404
- compare_zoom_reset_action = QAction("Compare Panel Zoom &Reset", self)
7405
- compare_zoom_reset_action.triggered.connect(self.compare_panel_zoom_reset)
7406
- compare_zoom_menu.addAction(compare_zoom_reset_action)
7438
+ match_panel_zoom_reset_action = QAction("Match Panel Zoom &Reset", self)
7439
+ match_panel_zoom_reset_action.triggered.connect(self.match_panel_zoom_reset)
7440
+ match_panel_zoom_menu.addAction(match_panel_zoom_reset_action)
7407
7441
 
7408
7442
  view_menu.addSeparator()
7409
7443
 
@@ -11509,7 +11543,12 @@ class SupervertalerQt(QMainWindow):
11509
11543
  )
11510
11544
 
11511
11545
  def _update_both_termviews(self, source_text, termbase_list, nt_matches):
11512
- """Update both Termview instances (one under grid, one in right panel) with the same data."""
11546
+ """Update all three Termview instances with the same data.
11547
+
11548
+ Termview locations:
11549
+ 1. Under grid (collapsible via View menu)
11550
+ 2. Match Panel tab (top section)
11551
+ """
11513
11552
  # Update left Termview (under grid)
11514
11553
  if hasattr(self, 'termview_widget') and self.termview_widget:
11515
11554
  try:
@@ -11517,12 +11556,12 @@ class SupervertalerQt(QMainWindow):
11517
11556
  except Exception as e:
11518
11557
  self.log(f"Error updating left termview: {e}")
11519
11558
 
11520
- # Update right Termview (in right panel)
11521
- if hasattr(self, 'termview_widget_right') and self.termview_widget_right:
11559
+ # Update Match Panel Termview
11560
+ if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
11522
11561
  try:
11523
- self.termview_widget_right.update_with_matches(source_text, termbase_list, nt_matches)
11562
+ self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches)
11524
11563
  except Exception as e:
11525
- self.log(f"Error updating right termview: {e}")
11564
+ self.log(f"Error updating Match Panel termview: {e}")
11526
11565
 
11527
11566
  def _refresh_termbase_display_for_current_segment(self):
11528
11567
  """Refresh only termbase/glossary display for the current segment.
@@ -16214,6 +16253,36 @@ class SupervertalerQt(QMainWindow):
16214
16253
  self.precision_spin = precision_spin
16215
16254
  self.auto_center_cb = auto_center_cb
16216
16255
 
16256
+ # 🧪 Experimental Performance group
16257
+ experimental_group = QGroupBox("🧪 Experimental Performance")
16258
+ experimental_layout = QVBoxLayout()
16259
+
16260
+ exp_info = QLabel(
16261
+ "⚠️ These options are for testing and debugging performance.\n"
16262
+ "Use with caution - they may affect application behavior."
16263
+ )
16264
+ exp_info.setWordWrap(True)
16265
+ exp_info.setStyleSheet("color: #d97706; font-size: 9pt; padding: 5px;")
16266
+ experimental_layout.addWidget(exp_info)
16267
+
16268
+ # Cache kill switch
16269
+ disable_cache_cb = CheckmarkCheckBox("Disable ALL caches (direct lookups every time)")
16270
+ disable_cache_cb.setChecked(general_settings.get('disable_all_caches', False))
16271
+ disable_cache_cb.setToolTip(
16272
+ "When enabled, ALL caching is bypassed:\n"
16273
+ "• Termbase cache\n"
16274
+ "• Translation matches cache\n"
16275
+ "• Prefetch system\n\n"
16276
+ "Every segment navigation will perform fresh database lookups.\n"
16277
+ "Use this to test if caching is causing issues or to measure\n"
16278
+ "baseline performance without any caching."
16279
+ )
16280
+ experimental_layout.addWidget(disable_cache_cb)
16281
+ self.disable_cache_checkbox = disable_cache_cb
16282
+
16283
+ experimental_group.setLayout(experimental_layout)
16284
+ layout.addWidget(experimental_group)
16285
+
16217
16286
  # Translation Results Match Limits group
16218
16287
  match_limits_group = QGroupBox("📊 Translation Results - Match Limits")
16219
16288
  match_limits_layout = QVBoxLayout()
@@ -16299,7 +16368,8 @@ class SupervertalerQt(QMainWindow):
16299
16368
  auto_confirm_100_cb=auto_confirm_100_cb,
16300
16369
  auto_confirm_overwrite_cb=auto_confirm_overwrite_cb,
16301
16370
  sound_effects_cb=sound_effects_cb,
16302
- sound_event_combos=sound_event_combos
16371
+ sound_event_combos=sound_event_combos,
16372
+ disable_cache_cb=disable_cache_cb
16303
16373
  ))
16304
16374
  layout.addWidget(save_btn)
16305
16375
 
@@ -16917,6 +16987,22 @@ class SupervertalerQt(QMainWindow):
16917
16987
  termview_font_spin.setValue(font_settings.get('termview_font_size', 10))
16918
16988
  termview_font_spin.setSuffix(" pt")
16919
16989
  termview_font_spin.setToolTip("Termview font size (6-16 pt)")
16990
+ termview_font_spin.setMinimumHeight(28)
16991
+ termview_font_spin.setMinimumWidth(80)
16992
+ # Fix spinbox arrow buttons - ensure both up and down work correctly
16993
+ termview_font_spin.setStyleSheet("""
16994
+ QSpinBox {
16995
+ padding-right: 20px;
16996
+ }
16997
+ QSpinBox::up-button {
16998
+ width: 20px;
16999
+ height: 14px;
17000
+ }
17001
+ QSpinBox::down-button {
17002
+ width: 20px;
17003
+ height: 14px;
17004
+ }
17005
+ """)
16920
17006
  termview_size_layout.addWidget(termview_font_spin)
16921
17007
  termview_size_layout.addStretch()
16922
17008
  termview_layout.addLayout(termview_size_layout)
@@ -16933,49 +17019,6 @@ class SupervertalerQt(QMainWindow):
16933
17019
  termview_group.setLayout(termview_layout)
16934
17020
  layout.addWidget(termview_group)
16935
17021
 
16936
- # Panel Visibility section (NEW)
16937
- panel_visibility_group = QGroupBox("👁️ Right Panel Visibility")
16938
- panel_visibility_layout = QVBoxLayout()
16939
-
16940
- panel_visibility_info = QLabel(
16941
- "Choose which panels to show on the right side of the editor. "
16942
- "The first visible panel will be selected by default."
16943
- )
16944
- panel_visibility_info.setStyleSheet("font-size: 8pt; padding: 8px; border-radius: 2px;")
16945
- panel_visibility_info.setWordWrap(True)
16946
- panel_visibility_layout.addWidget(panel_visibility_info)
16947
-
16948
- # Translation Results pane checkbox
16949
- show_results_check = CheckmarkCheckBox("Show Translation Results pane")
16950
- show_results_check.setChecked(font_settings.get('show_translation_results_pane', False))
16951
- show_results_check.setToolTip("Show TM matches, MT results, and segment notes in a tabbed panel")
16952
- panel_visibility_layout.addWidget(show_results_check)
16953
-
16954
- # Compare Panel checkbox
16955
- show_compare_check = CheckmarkCheckBox("Show Compare Panel")
16956
- show_compare_check.setChecked(font_settings.get('show_compare_panel', True))
16957
- show_compare_check.setToolTip("Show side-by-side comparison of source, TM match, and MT result")
16958
- panel_visibility_layout.addWidget(show_compare_check)
16959
-
16960
- # Warning label (shown when both are unchecked)
16961
- panel_warning_label = QLabel("⚠️ At least one panel must remain visible. Preview is always available.")
16962
- panel_warning_label.setStyleSheet("font-size: 8pt; color: #cc6600; padding: 4px;")
16963
- panel_warning_label.setVisible(False)
16964
- panel_visibility_layout.addWidget(panel_warning_label)
16965
-
16966
- # Connect checkbox signals to show warning if both unchecked
16967
- def update_panel_warning():
16968
- if not show_results_check.isChecked() and not show_compare_check.isChecked():
16969
- panel_warning_label.setVisible(True)
16970
- else:
16971
- panel_warning_label.setVisible(False)
16972
-
16973
- show_results_check.stateChanged.connect(lambda: update_panel_warning())
16974
- show_compare_check.stateChanged.connect(lambda: update_panel_warning())
16975
-
16976
- panel_visibility_group.setLayout(panel_visibility_layout)
16977
- layout.addWidget(panel_visibility_group)
16978
-
16979
17022
  # Quick Reference section
16980
17023
  reference_group = QGroupBox("⌨️ Font Size Quick Reference")
16981
17024
  reference_layout = QVBoxLayout()
@@ -17004,8 +17047,7 @@ class SupervertalerQt(QMainWindow):
17004
17047
  grid_font_spin, match_font_spin, compare_font_spin, show_tags_check, tag_color_btn,
17005
17048
  alt_colors_check, even_color_btn, odd_color_btn, invisible_char_color_btn, grid_font_family_combo,
17006
17049
  termview_font_family_combo, termview_font_spin, termview_bold_check,
17007
- border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check,
17008
- show_results_check, show_compare_check
17050
+ border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check
17009
17051
  ))
17010
17052
  layout.addWidget(save_btn)
17011
17053
 
@@ -18745,7 +18787,8 @@ class SupervertalerQt(QMainWindow):
18745
18787
  enable_backup_cb=None, backup_interval_spin=None,
18746
18788
  tb_order_combo=None, tb_hide_shorter_cb=None, smart_selection_cb=None,
18747
18789
  ahk_path_edit=None, auto_center_cb=None, auto_confirm_100_cb=None,
18748
- auto_confirm_overwrite_cb=None, sound_effects_cb=None, sound_event_combos=None):
18790
+ auto_confirm_overwrite_cb=None, sound_effects_cb=None, sound_event_combos=None,
18791
+ disable_cache_cb=None):
18749
18792
  """Save general settings from UI (non-AI settings only)"""
18750
18793
  self.allow_replace_in_source = allow_replace_cb.isChecked()
18751
18794
  self.update_warning_banner()
@@ -18806,11 +18849,25 @@ class SupervertalerQt(QMainWindow):
18806
18849
  'results_match_font_size': 9,
18807
18850
  'results_compare_font_size': 9,
18808
18851
  'autohotkey_path': ahk_path_edit.text().strip() if ahk_path_edit is not None else existing_settings.get('autohotkey_path', ''),
18809
- 'enable_sound_effects': sound_effects_cb.isChecked() if sound_effects_cb is not None else existing_settings.get('enable_sound_effects', False)
18852
+ 'enable_sound_effects': sound_effects_cb.isChecked() if sound_effects_cb is not None else existing_settings.get('enable_sound_effects', False),
18853
+ 'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', False)
18810
18854
  }
18811
18855
 
18812
18856
  # Keep a fast-access instance value
18813
18857
  self.enable_sound_effects = general_settings.get('enable_sound_effects', False)
18858
+
18859
+ # Update cache kill switch
18860
+ if disable_cache_cb is not None:
18861
+ self.disable_all_caches = disable_cache_cb.isChecked()
18862
+ if self.disable_all_caches:
18863
+ self.log("🧪 EXPERIMENTAL: All caches DISABLED - direct lookups enabled")
18864
+ # Stop any running background workers that use the database
18865
+ if hasattr(self, 'termbase_batch_stop_event'):
18866
+ self.termbase_batch_stop_event.set()
18867
+ if hasattr(self, 'prefetch_stop_event'):
18868
+ self.prefetch_stop_event.set()
18869
+ else:
18870
+ self.log("✓ Caches enabled (normal mode)")
18814
18871
 
18815
18872
  # Persist per-event sound mapping
18816
18873
  existing_map = existing_settings.get('sound_effects_map', {}) if isinstance(existing_settings, dict) else {}
@@ -18875,6 +18932,8 @@ class SupervertalerQt(QMainWindow):
18875
18932
  'termbase_display_order': self.termbase_display_order,
18876
18933
  'termbase_hide_shorter_matches': self.termbase_hide_shorter_matches,
18877
18934
  'enable_smart_word_selection': self.enable_smart_word_selection,
18935
+ 'enable_sound_effects': self.enable_sound_effects,
18936
+ 'sound_effects_map': getattr(self, 'sound_effects_map', {}),
18878
18937
  }
18879
18938
  self.log("💾 Settings also saved to active project")
18880
18939
  self.project_modified = True # Mark project as modified
@@ -18889,8 +18948,7 @@ class SupervertalerQt(QMainWindow):
18889
18948
  def _save_view_settings_from_ui(self, grid_spin, match_spin, compare_spin, show_tags_check=None, tag_color_btn=None,
18890
18949
  alt_colors_check=None, even_color_btn=None, odd_color_btn=None, invisible_char_color_btn=None,
18891
18950
  grid_font_family_combo=None, termview_font_family_combo=None, termview_font_spin=None, termview_bold_check=None,
18892
- border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None,
18893
- show_results_check=None, show_compare_check=None):
18951
+ border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None):
18894
18952
  """Save view settings from UI"""
18895
18953
  general_settings = {
18896
18954
  'restore_last_project': self.load_general_settings().get('restore_last_project', False),
@@ -18906,14 +18964,6 @@ class SupervertalerQt(QMainWindow):
18906
18964
  general_settings['tabs_above_grid'] = tabs_above_check.isChecked()
18907
18965
  self.tabs_above_grid = tabs_above_check.isChecked()
18908
18966
 
18909
- # Add panel visibility settings if provided
18910
- if show_results_check is not None:
18911
- general_settings['show_translation_results_pane'] = show_results_check.isChecked()
18912
- self.show_translation_results_pane = show_results_check.isChecked()
18913
- if show_compare_check is not None:
18914
- general_settings['show_compare_panel'] = show_compare_check.isChecked()
18915
- self.show_compare_panel = show_compare_check.isChecked()
18916
-
18917
18967
  # Add font family if provided
18918
18968
  if grid_font_family_combo is not None:
18919
18969
  general_settings['grid_font_family'] = grid_font_family_combo.currentText()
@@ -19001,13 +19051,20 @@ class SupervertalerQt(QMainWindow):
19001
19051
 
19002
19052
  self.save_general_settings(general_settings)
19003
19053
 
19004
- # Apply termview font settings immediately
19054
+ # Apply termview font settings immediately to BOTH termview widgets
19005
19055
  if hasattr(self, 'termview_widget') and self.termview_widget is not None:
19006
19056
  termview_family = general_settings.get('termview_font_family', 'Segoe UI')
19007
19057
  termview_size = general_settings.get('termview_font_size', 10)
19008
19058
  termview_bold = general_settings.get('termview_font_bold', False)
19009
19059
  self.termview_widget.set_font_settings(termview_family, termview_size, termview_bold)
19010
19060
 
19061
+ # Also apply to the Match Panel's Termview widget
19062
+ if hasattr(self, 'termview_widget_match') and self.termview_widget_match is not None:
19063
+ termview_family = general_settings.get('termview_font_family', 'Segoe UI')
19064
+ termview_size = general_settings.get('termview_font_size', 10)
19065
+ termview_bold = general_settings.get('termview_font_bold', False)
19066
+ self.termview_widget_match.set_font_settings(termview_family, termview_size, termview_bold)
19067
+
19011
19068
  # Apply font family and size immediately
19012
19069
  font_changed = False
19013
19070
  if grid_font_family_combo is not None and self.default_font_family != grid_font_family_combo.currentText():
@@ -19101,27 +19158,14 @@ class SupervertalerQt(QMainWindow):
19101
19158
  # Also refresh row colors
19102
19159
  self.apply_alternating_row_colors()
19103
19160
 
19104
- # Check if panel visibility changed (requires restart)
19105
- panel_visibility_changed = False
19106
- if show_results_check is not None or show_compare_check is not None:
19107
- old_results = hasattr(self, 'right_tabs') and self.right_tabs.count() > 0
19108
- old_compare = hasattr(self, 'right_tabs') and self.right_tabs.count() > 1
19109
- new_results = show_results_check.isChecked() if show_results_check else old_results
19110
- new_compare = show_compare_check.isChecked() if show_compare_check else old_compare
19111
- if (new_results != old_results or new_compare != old_compare):
19112
- panel_visibility_changed = True
19113
-
19114
19161
  self.log("✓ View settings saved and applied")
19115
-
19116
- if panel_visibility_changed:
19117
- QMessageBox.information(
19118
- self, "Settings Saved",
19119
- "View settings have been saved.\n\n"
19120
- "Note: Panel visibility changes will take effect when you restart Supervertaler "
19121
- "or open a new project."
19122
- )
19123
- else:
19124
- QMessageBox.information(self, "Settings Saved", "View settings have been saved and applied successfully.")
19162
+ # Use explicit QMessageBox instance to ensure proper dialog closing
19163
+ msg = QMessageBox(self)
19164
+ msg.setIcon(QMessageBox.Icon.Information)
19165
+ msg.setWindowTitle("Settings Saved")
19166
+ msg.setText("View settings have been saved and applied successfully.")
19167
+ msg.setStandardButtons(QMessageBox.StandardButton.Ok)
19168
+ msg.exec()
19125
19169
 
19126
19170
  def create_grid_view_widget(self):
19127
19171
  """Create the Grid View widget (existing grid functionality)"""
@@ -19555,29 +19599,6 @@ class SupervertalerQt(QMainWindow):
19555
19599
  tab_seg_info.setStyleSheet("font-weight: bold;")
19556
19600
  toolbar_layout.addWidget(tab_seg_info)
19557
19601
 
19558
- # TM/Termbase toggle button
19559
- tm_toggle_btn = QPushButton("🔍 TM ON")
19560
- tm_toggle_btn.setCheckable(True)
19561
- tm_toggle_btn.setChecked(True)
19562
- tm_toggle_btn.setStyleSheet("""
19563
- QPushButton {
19564
- background-color: #4CAF50;
19565
- color: white;
19566
- font-weight: bold;
19567
- padding: 4px 8px;
19568
- border-radius: 3px;
19569
- }
19570
- QPushButton:checked {
19571
- background-color: #4CAF50;
19572
- }
19573
- QPushButton:!checked {
19574
- background-color: #757575;
19575
- }
19576
- """)
19577
- tm_toggle_btn.setToolTip("Toggle TM and Glossary lookups")
19578
- tm_toggle_btn.clicked.connect(lambda checked: self.toggle_tm_from_editor(checked, tm_toggle_btn))
19579
- toolbar_layout.addWidget(tm_toggle_btn)
19580
-
19581
19602
  # Tag View toggle button
19582
19603
  tag_view_btn = QPushButton("🏷️ Tags OFF")
19583
19604
  tag_view_btn.setCheckable(True)
@@ -19616,17 +19637,6 @@ class SupervertalerQt(QMainWindow):
19616
19637
 
19617
19638
  toolbar_layout.addWidget(QLabel("|")) # Separator
19618
19639
 
19619
- # Action buttons
19620
- copy_btn = QPushButton("📋 Copy")
19621
- copy_btn.setToolTip("Copy Source → Target")
19622
- copy_btn.clicked.connect(self.copy_source_to_grid_target)
19623
- toolbar_layout.addWidget(copy_btn)
19624
-
19625
- clear_btn = QPushButton("🗑️ Clear")
19626
- clear_btn.setToolTip("Clear Target")
19627
- clear_btn.clicked.connect(self.clear_grid_target)
19628
- toolbar_layout.addWidget(clear_btn)
19629
-
19630
19640
  preview_prompt_btn = QPushButton("🧪 Preview Prompts")
19631
19641
  preview_prompt_btn.setToolTip("Preview the complete assembled prompt\n(System Prompt + Custom Prompts + current segment)")
19632
19642
  preview_prompt_btn.setStyleSheet("background-color: #9C27B0; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
@@ -19662,11 +19672,6 @@ class SupervertalerQt(QMainWindow):
19662
19672
 
19663
19673
  toolbar_layout.addStretch()
19664
19674
 
19665
- save_btn = QPushButton("💾 Save")
19666
- save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
19667
- save_btn.clicked.connect(self.save_grid_segment)
19668
- toolbar_layout.addWidget(save_btn)
19669
-
19670
19675
  save_next_btn = QPushButton("✓ Confirm && Next")
19671
19676
  save_next_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; padding: 4px 8px; border: none; outline: none;")
19672
19677
  save_next_btn.clicked.connect(self.confirm_selected_or_next)
@@ -19808,53 +19813,29 @@ class SupervertalerQt(QMainWindow):
19808
19813
  # Hide the panel if not added as tab (it still exists as child widget)
19809
19814
  self.translation_results_panel.hide()
19810
19815
 
19811
- # Tab 2: Compare Panel (conditionally added)
19812
- self.compare_panel = self._create_compare_panel()
19813
- if self.show_compare_panel:
19814
- right_tabs.addTab(self.compare_panel, "⚖️ Compare Panel")
19815
- compare_tab_index = tab_index
19816
- tab_index += 1
19817
- else:
19818
- # Hide the panel if not added as tab
19819
- self.compare_panel.hide()
19816
+ # Tab 2: Match Panel (Termview + TM Source/Target) - shown first by default
19817
+ match_panel_widget = self._create_match_panel()
19818
+ right_tabs.addTab(match_panel_widget, "🎯 Match Panel")
19819
+ match_panel_tab_index = tab_index
19820
+ tab_index += 1
19820
19821
 
19821
- # Tab 3: Document Preview (always added)
19822
+ # Tab 4: Document Preview (always added)
19822
19823
  preview_widget = self._create_preview_tab()
19823
19824
  right_tabs.addTab(preview_widget, "📄 Preview")
19824
19825
  preview_tab_index = tab_index
19826
+ self._preview_tab_index = preview_tab_index # Store for visibility checks
19825
19827
  tab_index += 1
19826
19828
 
19827
- # Tab 4: Segment Note (moved from bottom panel)
19829
+ # Tab 5: Segment Note (moved from bottom panel)
19828
19830
  right_tabs.addTab(self._notes_widget_for_right_panel, "📝 Segment note")
19829
19831
  tab_index += 1
19830
19832
 
19831
- # Tab 5: Session Log (moved from bottom panel)
19833
+ # Tab 6: Session Log (moved from bottom panel)
19832
19834
  right_tabs.addTab(self._session_log_widget_for_right_panel, "📋 Session Log")
19833
19835
  tab_index += 1
19834
19836
 
19835
- # Tab 6: Second Termview instance (duplicate of the one under grid)
19836
- self.termview_widget_right = TermviewWidget(self, db_manager=self.db_manager, log_callback=self.log, theme_manager=self.theme_manager)
19837
- self.termview_widget_right.term_insert_requested.connect(self.insert_termview_text)
19838
- self.termview_widget_right.edit_entry_requested.connect(self._on_termview_edit_entry)
19839
- self.termview_widget_right.delete_entry_requested.connect(self._on_termview_delete_entry)
19840
-
19841
- # Apply same font settings to right Termview
19842
- font_settings = self.load_general_settings()
19843
- termview_family = font_settings.get('termview_font_family', 'Segoe UI')
19844
- termview_size = font_settings.get('termview_font_size', 10)
19845
- termview_bold = font_settings.get('termview_font_bold', False)
19846
- self.termview_widget_right.set_font_settings(termview_family, termview_size, termview_bold)
19847
-
19848
- right_tabs.addTab(self.termview_widget_right, "🔍 Termview")
19849
-
19850
- # Set default selected tab based on visibility settings
19851
- # Priority: Compare Panel > Translation Results > Preview
19852
- if compare_tab_index >= 0:
19853
- right_tabs.setCurrentIndex(compare_tab_index)
19854
- elif results_tab_index >= 0:
19855
- right_tabs.setCurrentIndex(results_tab_index)
19856
- else:
19857
- right_tabs.setCurrentIndex(preview_tab_index)
19837
+ # Set default selected tab to Match Panel (always show Match Panel first)
19838
+ right_tabs.setCurrentIndex(match_panel_tab_index)
19858
19839
 
19859
19840
  # Store reference for later use
19860
19841
  self.right_tabs = right_tabs
@@ -20507,35 +20488,6 @@ class SupervertalerQt(QMainWindow):
20507
20488
  tab_seg_info.setStyleSheet("font-weight: bold; font-size: 11pt;")
20508
20489
  info_layout.addWidget(tab_seg_info, stretch=1)
20509
20490
 
20510
- # TM/Glossary toggle button
20511
- tm_toggle_btn = QPushButton("🔍 TM/Glossary ON")
20512
- tm_toggle_btn.setCheckable(True)
20513
- tm_toggle_btn.setChecked(True) # Start enabled
20514
- tm_toggle_btn.setStyleSheet("""
20515
- QPushButton {
20516
- background-color: #4CAF50;
20517
- color: white;
20518
- font-weight: bold;
20519
- padding: 5px 10px;
20520
- border-radius: 3px;
20521
- }
20522
- QPushButton:checked {
20523
- background-color: #4CAF50;
20524
- }
20525
- QPushButton:!checked {
20526
- background-color: #757575;
20527
- }
20528
- QPushButton:hover {
20529
- opacity: 0.9;
20530
- }
20531
- """)
20532
- tm_toggle_btn.setToolTip("Toggle TM and Glossary lookups when clicking segments (speeds up editing)")
20533
- tm_toggle_btn.clicked.connect(lambda checked: self.toggle_tm_from_editor(checked, tm_toggle_btn))
20534
- info_layout.addWidget(tm_toggle_btn)
20535
-
20536
- # Store reference to button for updates from Settings
20537
- editor_widget.tm_toggle_btn = tm_toggle_btn
20538
-
20539
20491
  # Status selector
20540
20492
  from modules.statuses import STATUSES
20541
20493
  status_label = QLabel("Status:")
@@ -21328,6 +21280,14 @@ class SupervertalerQt(QMainWindow):
21328
21280
 
21329
21281
  if 'enable_smart_word_selection' in project_settings:
21330
21282
  self.enable_smart_word_selection = project_settings['enable_smart_word_selection']
21283
+
21284
+ if 'enable_sound_effects' in project_settings:
21285
+ self.enable_sound_effects = project_settings['enable_sound_effects']
21286
+ self.log(f"✓ Project override: sound effects = {self.enable_sound_effects}")
21287
+
21288
+ if 'sound_effects_map' in project_settings:
21289
+ self.sound_effects_map = project_settings['sound_effects_map']
21290
+ self.log(f"✓ Project override: sound effects map loaded")
21331
21291
 
21332
21292
  self.log(f"✓ Loaded project: {self.current_project.name} ({len(self.current_project.segments)} segments)")
21333
21293
 
@@ -21363,6 +21323,11 @@ class SupervertalerQt(QMainWindow):
21363
21323
  if not self.current_project or len(self.current_project.segments) == 0:
21364
21324
  return
21365
21325
 
21326
+ # 🧪 EXPERIMENTAL: Skip batch worker if cache kill switch is enabled
21327
+ if getattr(self, 'disable_all_caches', False):
21328
+ self.log("🧪 Termbase batch worker SKIPPED (caches disabled)")
21329
+ return
21330
+
21366
21331
  # Stop any existing worker thread
21367
21332
  self.termbase_batch_stop_event.set()
21368
21333
  if self.termbase_batch_worker_thread and self.termbase_batch_worker_thread.is_alive():
@@ -21426,11 +21391,14 @@ class SupervertalerQt(QMainWindow):
21426
21391
  # Search termbase for this segment using thread-local database connection
21427
21392
  try:
21428
21393
  # Manually query the database using thread-local connection
21394
+ # Pass project_id to filter by activated termbases only
21395
+ current_project_id = self.current_project.id if (self.current_project and hasattr(self.current_project, 'id')) else None
21429
21396
  matches = self._search_termbases_thread_safe(
21430
21397
  segment.source,
21431
21398
  thread_db_cursor,
21432
21399
  source_lang=self.current_project.source_lang if self.current_project else None,
21433
- target_lang=self.current_project.target_lang if self.current_project else None
21400
+ target_lang=self.current_project.target_lang if self.current_project else None,
21401
+ project_id=current_project_id
21434
21402
  )
21435
21403
 
21436
21404
  if matches:
@@ -21475,7 +21443,7 @@ class SupervertalerQt(QMainWindow):
21475
21443
  except:
21476
21444
  pass
21477
21445
 
21478
- def _search_termbases_thread_safe(self, source_text: str, cursor, source_lang: str = None, target_lang: str = None) -> Dict[str, str]:
21446
+ 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]:
21479
21447
  """
21480
21448
  Search termbases using a provided cursor (thread-safe for background threads).
21481
21449
  This method allows background workers to query the database without SQLite threading errors.
@@ -21485,6 +21453,7 @@ class SupervertalerQt(QMainWindow):
21485
21453
  cursor: A database cursor from a thread-local connection
21486
21454
  source_lang: Source language code
21487
21455
  target_lang: Target language code
21456
+ project_id: Current project ID (required to filter by activated termbases)
21488
21457
 
21489
21458
  Returns:
21490
21459
  Dictionary of {term: translation} matches
@@ -21509,17 +21478,21 @@ class SupervertalerQt(QMainWindow):
21509
21478
  continue
21510
21479
 
21511
21480
  try:
21512
- # JOIN termbases to get is_project_termbase, name, and ranking
21481
+ # JOIN termbases AND termbase_activation to filter by activated termbases
21482
+ # This matches the logic in database_manager.py search_termbases()
21513
21483
  query = """
21514
21484
  SELECT
21515
21485
  t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
21516
21486
  t.domain, t.notes, t.project, t.client, t.forbidden,
21517
- tb.is_project_termbase, tb.name as termbase_name, tb.ranking
21487
+ tb.is_project_termbase, tb.name as termbase_name,
21488
+ COALESCE(ta.priority, tb.ranking) as ranking
21518
21489
  FROM termbase_terms t
21519
21490
  LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
21491
+ LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
21520
21492
  WHERE LOWER(t.source_term) LIKE ?
21493
+ AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
21521
21494
  """
21522
- params = [f"%{clean_word.lower()}%"]
21495
+ params = [project_id if project_id else 0, f"%{clean_word.lower()}%"]
21523
21496
 
21524
21497
  if source_lang_code:
21525
21498
  query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
@@ -21610,6 +21583,62 @@ class SupervertalerQt(QMainWindow):
21610
21583
  except Exception:
21611
21584
  return {}
21612
21585
 
21586
+ def _trigger_idle_prefetch(self, current_row: int):
21587
+ """
21588
+ Trigger prefetch for the next few segments while user is idle.
21589
+ Called after user stops typing (debounced) - uses their thinking time productively.
21590
+
21591
+ This makes Ctrl+Enter feel INSTANT because matches are already cached.
21592
+ Also triggers PROACTIVE HIGHLIGHTING for upcoming segments with glossary matches.
21593
+ """
21594
+ import json
21595
+
21596
+ print(f"[PROACTIVE DEBUG] _trigger_idle_prefetch called for row {current_row}")
21597
+
21598
+ if not self.current_project or current_row < 0:
21599
+ print(f"[PROACTIVE DEBUG] Early exit: no project or invalid row")
21600
+ return
21601
+
21602
+ try:
21603
+ # Prefetch next 5 segments (enough for fast workflow, not too many to waste resources)
21604
+ next_segment_ids = []
21605
+ already_cached_ids = []
21606
+ start_idx = current_row + 1
21607
+ end_idx = min(start_idx + 5, len(self.current_project.segments))
21608
+
21609
+ print(f"[PROACTIVE DEBUG] Checking segments {start_idx} to {end_idx}")
21610
+
21611
+ for seg in self.current_project.segments[start_idx:end_idx]:
21612
+ # Check if already cached
21613
+ with self.translation_matches_cache_lock:
21614
+ if seg.id not in self.translation_matches_cache:
21615
+ next_segment_ids.append(seg.id)
21616
+ else:
21617
+ already_cached_ids.append(seg.id)
21618
+
21619
+ print(f"[PROACTIVE DEBUG] Already cached IDs: {already_cached_ids}, Need prefetch: {next_segment_ids}")
21620
+
21621
+ # For already-cached segments, trigger proactive highlighting immediately
21622
+ # This handles the case where segments were cached earlier but not highlighted
21623
+ for seg_id in already_cached_ids:
21624
+ try:
21625
+ with self.termbase_cache_lock:
21626
+ termbase_raw = self.termbase_cache.get(seg_id, {})
21627
+ print(f"[PROACTIVE DEBUG] Segment {seg_id} termbase cache: {len(termbase_raw) if termbase_raw else 0} matches")
21628
+ if termbase_raw:
21629
+ termbase_json = json.dumps(termbase_raw)
21630
+ # Apply highlighting on main thread (we're already on main thread here)
21631
+ print(f"[PROACTIVE DEBUG] Calling _apply_proactive_highlighting for seg {seg_id}")
21632
+ self._apply_proactive_highlighting(seg_id, termbase_json)
21633
+ except Exception as e:
21634
+ print(f"[PROACTIVE DEBUG] Error for seg {seg_id}: {e}")
21635
+
21636
+ if next_segment_ids:
21637
+ # Start prefetch in background (silent, no logging)
21638
+ self._start_prefetch_worker(next_segment_ids)
21639
+ except Exception:
21640
+ pass # Silent failure - prefetch is optimization, not critical
21641
+
21613
21642
  def _start_prefetch_worker(self, segment_ids):
21614
21643
  """
21615
21644
  Start background thread to prefetch TM/MT/LLM matches for given segments.
@@ -21618,6 +21647,10 @@ class SupervertalerQt(QMainWindow):
21618
21647
  if not segment_ids:
21619
21648
  return
21620
21649
 
21650
+ # 🧪 EXPERIMENTAL: Skip prefetch if cache kill switch is enabled
21651
+ if getattr(self, 'disable_all_caches', False):
21652
+ return
21653
+
21621
21654
  # Stop any existing worker thread
21622
21655
  self.prefetch_stop_event.set()
21623
21656
  if self.prefetch_worker_thread and self.prefetch_worker_thread.is_alive():
@@ -21638,7 +21671,24 @@ class SupervertalerQt(QMainWindow):
21638
21671
  """
21639
21672
  Background worker: prefetch TM/MT/LLM matches for given segments.
21640
21673
  Runs in separate thread to avoid blocking UI.
21674
+
21675
+ Creates its own database connection for thread-safe termbase lookups.
21676
+ Also emits signal to apply proactive highlighting on the main thread.
21641
21677
  """
21678
+ import sqlite3
21679
+ import json
21680
+
21681
+ # Create thread-local database connection for termbase searches
21682
+ thread_db_cursor = None
21683
+ thread_db_connection = None
21684
+ try:
21685
+ if hasattr(self, 'db_manager') and self.db_manager and hasattr(self.db_manager, 'db_path'):
21686
+ thread_db_connection = sqlite3.connect(self.db_manager.db_path)
21687
+ thread_db_connection.row_factory = sqlite3.Row
21688
+ thread_db_cursor = thread_db_connection.cursor()
21689
+ except Exception:
21690
+ pass # Continue without thread-local connection - will use cache only
21691
+
21642
21692
  try:
21643
21693
  for idx, segment_id in enumerate(segment_ids):
21644
21694
  # Check stop signal
@@ -21661,8 +21711,8 @@ class SupervertalerQt(QMainWindow):
21661
21711
  if not segment:
21662
21712
  continue
21663
21713
 
21664
- # Fetch TM/MT/LLM matches (this is the expensive part)
21665
- matches = self._fetch_all_matches_for_segment(segment)
21714
+ # Fetch TM/termbase matches (pass cursor for thread-safe termbase lookups)
21715
+ matches = self._fetch_all_matches_for_segment(segment, thread_db_cursor)
21666
21716
 
21667
21717
  # Only cache if we got at least one match (don't cache empty results)
21668
21718
  # This prevents "empty cache hits" when TM database is still empty
@@ -21672,19 +21722,60 @@ class SupervertalerQt(QMainWindow):
21672
21722
  llm_count = len(matches.get("LLM", []))
21673
21723
  total_matches = tm_count + tb_count + mt_count + llm_count
21674
21724
 
21725
+ print(f"[PREFETCH DEBUG] Segment {segment_id}: TM={tm_count}, TB={tb_count}, MT={mt_count}, LLM={llm_count}")
21726
+
21675
21727
  if total_matches > 0:
21676
21728
  # Store in cache only if we have results
21677
21729
  with self.translation_matches_cache_lock:
21678
21730
  self.translation_matches_cache[segment_id] = matches
21731
+
21732
+ # PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
21733
+ # This makes upcoming segments show their glossary matches immediately
21734
+ if tb_count > 0:
21735
+ try:
21736
+ # Extract raw termbase matches from cache for highlighting
21737
+ with self.termbase_cache_lock:
21738
+ termbase_raw = self.termbase_cache.get(segment_id, {})
21739
+
21740
+ print(f"[PREFETCH DEBUG] Segment {segment_id}: termbase_raw has {len(termbase_raw) if termbase_raw else 0} entries")
21741
+
21742
+ if termbase_raw:
21743
+ # Convert to JSON for thread-safe signal transfer
21744
+ termbase_json = json.dumps(termbase_raw)
21745
+ # Emit signal - will be handled on main thread
21746
+ print(f"[PREFETCH DEBUG] Emitting proactive highlight signal for segment {segment_id}")
21747
+ self._proactive_highlight_signal.emit(segment_id, termbase_json)
21748
+ else:
21749
+ print(f"[PREFETCH DEBUG] WARNING: tb_count={tb_count} but termbase_raw is empty!")
21750
+ except Exception as e:
21751
+ print(f"[PREFETCH DEBUG] ERROR emitting signal: {e}")
21679
21752
  # else: Don't cache empty results - let it fall through to slow lookup next time
21680
21753
 
21681
21754
  except Exception as e:
21682
21755
  self.log(f"Error in prefetch worker: {e}")
21756
+
21757
+ finally:
21758
+ # Close thread-local database connection
21759
+ if thread_db_cursor:
21760
+ try:
21761
+ thread_db_cursor.close()
21762
+ except:
21763
+ pass
21764
+ if thread_db_connection:
21765
+ try:
21766
+ thread_db_connection.close()
21767
+ except:
21768
+ pass
21683
21769
 
21684
- def _fetch_all_matches_for_segment(self, segment):
21770
+ def _fetch_all_matches_for_segment(self, segment, thread_db_cursor=None):
21685
21771
  """
21686
21772
  Fetch TM, MT, and LLM matches for a single segment.
21687
21773
  Used by prefetch worker. Returns matches_dict with all match types.
21774
+
21775
+ Args:
21776
+ segment: The segment to fetch matches for
21777
+ thread_db_cursor: Optional thread-local database cursor for termbase searches.
21778
+ If provided and segment not in termbase_cache, will do direct lookup.
21688
21779
  """
21689
21780
  from modules.translation_results_panel import TranslationMatch
21690
21781
 
@@ -21747,53 +21838,80 @@ class SupervertalerQt(QMainWindow):
21747
21838
  # LLM will still be fetched on-demand when user clicks
21748
21839
  pass
21749
21840
 
21750
- # 4. Termbase matches (from cache)
21841
+ # 4. Termbase matches - try cache first, then direct lookup if cursor provided
21842
+ termbase_matches_raw = None
21843
+
21751
21844
  with self.termbase_cache_lock:
21752
21845
  if segment.id in self.termbase_cache:
21753
- stored_matches = self.termbase_cache[segment.id]
21754
- for term_id, match_info in stored_matches.items():
21755
- # Extract source term, translation, ranking, and other metadata from match_info
21756
- if isinstance(match_info, dict):
21757
- source_term = match_info.get('source', '')
21758
- target_term = match_info.get('translation', '')
21759
- priority = match_info.get('priority', 50) # Keep for backward compatibility
21760
- ranking = match_info.get('ranking', None) # NEW: termbase ranking
21761
- forbidden = match_info.get('forbidden', False)
21762
- is_project_termbase = match_info.get('is_project_termbase', False)
21763
- termbase_name = match_info.get('termbase_name', 'Default')
21764
- else:
21765
- # Backward compatibility: if just string (shouldn't happen with new code)
21766
- source_term = str(term_id)
21767
- target_term = match_info
21768
- priority = 50
21769
- ranking = None
21770
- forbidden = False
21771
- is_project_termbase = False
21772
- termbase_name = 'Default'
21773
-
21774
- match_obj = TranslationMatch(
21775
- source=source_term,
21776
- target=target_term,
21777
- relevance=95,
21778
- metadata={
21779
- 'termbase_name': termbase_name,
21780
- 'priority': priority, # Keep for backward compatibility
21781
- 'ranking': ranking, # NEW: termbase-level ranking
21782
- 'forbidden': forbidden,
21783
- 'is_project_termbase': is_project_termbase,
21784
- 'term_id': match_info.get('term_id') if isinstance(match_info, dict) else None,
21785
- 'termbase_id': match_info.get('termbase_id') if isinstance(match_info, dict) else None,
21786
- 'domain': match_info.get('domain', '') if isinstance(match_info, dict) else '',
21787
- 'notes': match_info.get('notes', '') if isinstance(match_info, dict) else '',
21788
- 'project': match_info.get('project', '') if isinstance(match_info, dict) else '',
21789
- 'client': match_info.get('client', '') if isinstance(match_info, dict) else '',
21790
- 'target_synonyms': match_info.get('target_synonyms', []) if isinstance(match_info, dict) else []
21791
- },
21792
- match_type='Termbase',
21793
- compare_source=source_term,
21794
- provider_code='TB'
21795
- )
21796
- matches_dict["Termbases"].append(match_obj)
21846
+ termbase_matches_raw = self.termbase_cache[segment.id]
21847
+
21848
+ # If not in cache and we have a thread-local cursor, do direct lookup
21849
+ if termbase_matches_raw is None and thread_db_cursor is not None:
21850
+ try:
21851
+ # Get project_id for activation filtering
21852
+ current_project_id = None
21853
+ if hasattr(self, 'current_project') and self.current_project and hasattr(self.current_project, 'id'):
21854
+ current_project_id = self.current_project.id
21855
+
21856
+ termbase_matches_raw = self._search_termbases_thread_safe(
21857
+ segment.source,
21858
+ thread_db_cursor,
21859
+ source_lang=source_lang,
21860
+ target_lang=target_lang,
21861
+ project_id=current_project_id
21862
+ )
21863
+ # Also populate the termbase cache for future use
21864
+ if termbase_matches_raw:
21865
+ with self.termbase_cache_lock:
21866
+ self.termbase_cache[segment.id] = termbase_matches_raw
21867
+ except Exception:
21868
+ pass # Silently continue
21869
+
21870
+ # Convert raw termbase matches to TranslationMatch objects
21871
+ if termbase_matches_raw:
21872
+ for term_id, match_info in termbase_matches_raw.items():
21873
+ # Extract source term, translation, ranking, and other metadata from match_info
21874
+ if isinstance(match_info, dict):
21875
+ source_term = match_info.get('source', '')
21876
+ target_term = match_info.get('translation', '')
21877
+ priority = match_info.get('priority', 50) # Keep for backward compatibility
21878
+ ranking = match_info.get('ranking', None) # NEW: termbase ranking
21879
+ forbidden = match_info.get('forbidden', False)
21880
+ is_project_termbase = match_info.get('is_project_termbase', False)
21881
+ termbase_name = match_info.get('termbase_name', 'Default')
21882
+ else:
21883
+ # Backward compatibility: if just string (shouldn't happen with new code)
21884
+ source_term = str(term_id)
21885
+ target_term = match_info
21886
+ priority = 50
21887
+ ranking = None
21888
+ forbidden = False
21889
+ is_project_termbase = False
21890
+ termbase_name = 'Default'
21891
+
21892
+ match_obj = TranslationMatch(
21893
+ source=source_term,
21894
+ target=target_term,
21895
+ relevance=95,
21896
+ metadata={
21897
+ 'termbase_name': termbase_name,
21898
+ 'priority': priority, # Keep for backward compatibility
21899
+ 'ranking': ranking, # NEW: termbase-level ranking
21900
+ 'forbidden': forbidden,
21901
+ 'is_project_termbase': is_project_termbase,
21902
+ 'term_id': match_info.get('term_id') if isinstance(match_info, dict) else None,
21903
+ 'termbase_id': match_info.get('termbase_id') if isinstance(match_info, dict) else None,
21904
+ 'domain': match_info.get('domain', '') if isinstance(match_info, dict) else '',
21905
+ 'notes': match_info.get('notes', '') if isinstance(match_info, dict) else '',
21906
+ 'project': match_info.get('project', '') if isinstance(match_info, dict) else '',
21907
+ 'client': match_info.get('client', '') if isinstance(match_info, dict) else '',
21908
+ 'target_synonyms': match_info.get('target_synonyms', []) if isinstance(match_info, dict) else []
21909
+ },
21910
+ match_type='Termbase',
21911
+ compare_source=source_term,
21912
+ provider_code='TB'
21913
+ )
21914
+ matches_dict["Termbases"].append(match_obj)
21797
21915
 
21798
21916
  return matches_dict
21799
21917
 
@@ -22051,6 +22169,8 @@ class SupervertalerQt(QMainWindow):
22051
22169
  'termbase_display_order': self.termbase_display_order,
22052
22170
  'termbase_hide_shorter_matches': self.termbase_hide_shorter_matches,
22053
22171
  'enable_smart_word_selection': self.enable_smart_word_selection,
22172
+ 'enable_sound_effects': self.enable_sound_effects,
22173
+ 'sound_effects_map': getattr(self, 'sound_effects_map', {}),
22054
22174
  }
22055
22175
 
22056
22176
  # Save original DOCX path for structure-preserving export
@@ -28105,6 +28225,135 @@ class SupervertalerQt(QMainWindow):
28105
28225
 
28106
28226
  return widget
28107
28227
 
28228
+ def _create_match_panel(self) -> QWidget:
28229
+ """Create Match Panel with Termview + TM Source/Target boxes."""
28230
+ widget = QWidget()
28231
+ main_layout = QVBoxLayout(widget)
28232
+ main_layout.setContentsMargins(0, 0, 0, 0)
28233
+ main_layout.setSpacing(0)
28234
+
28235
+ # Initialize compare_panel_text_edits if not exists (needed for _create_compare_panel_box)
28236
+ if not hasattr(self, 'compare_panel_text_edits'):
28237
+ self.compare_panel_text_edits = []
28238
+
28239
+ # Vertical splitter: Termview (top) | TM boxes (bottom)
28240
+ splitter = QSplitter(Qt.Orientation.Vertical)
28241
+
28242
+ # TOP: Container with header + Termview
28243
+ termview_container = QWidget()
28244
+ termview_layout = QVBoxLayout(termview_container)
28245
+ termview_layout.setContentsMargins(4, 4, 4, 0)
28246
+ termview_layout.setSpacing(2)
28247
+
28248
+ # Termview header label
28249
+ termview_header = QLabel("📖 Termview")
28250
+ termview_header.setStyleSheet("font-weight: bold; font-size: 9px; color: #666;")
28251
+ termview_layout.addWidget(termview_header)
28252
+
28253
+ # Third Termview instance for Match Panel
28254
+ self.termview_widget_match = TermviewWidget(self, db_manager=self.db_manager, log_callback=self.log, theme_manager=self.theme_manager)
28255
+ # Connect Termview signals
28256
+ self.termview_widget_match.term_insert_requested.connect(self.insert_termview_text)
28257
+ self.termview_widget_match.edit_entry_requested.connect(self._on_termview_edit_entry)
28258
+ self.termview_widget_match.delete_entry_requested.connect(self._on_termview_delete_entry)
28259
+ # Apply font settings
28260
+ font_settings = self.load_general_settings()
28261
+ termview_family = font_settings.get('termview_font_family', 'Segoe UI')
28262
+ termview_size = font_settings.get('termview_font_size', 10)
28263
+ termview_bold = font_settings.get('termview_font_bold', False)
28264
+ self.termview_widget_match.set_font_settings(termview_family, termview_size, termview_bold)
28265
+ termview_layout.addWidget(self.termview_widget_match)
28266
+
28267
+ splitter.addWidget(termview_container)
28268
+
28269
+ # BOTTOM: Container for TM Source + TM Target boxes
28270
+ tm_container = QWidget()
28271
+ tm_layout = QHBoxLayout(tm_container)
28272
+ tm_layout.setContentsMargins(0, 0, 0, 0)
28273
+ tm_layout.setSpacing(0)
28274
+
28275
+ # Hardcode the green color for TM boxes (same as TM Target in Compare Panel)
28276
+ tm_box_bg = "#d4edda" # Green (same as TM Target in Compare Panel)
28277
+ text_color = "#333"
28278
+ border_color = "#ddd"
28279
+
28280
+ # TM Source box (GREEN, with navigation)
28281
+ self.match_panel_tm_matches = [] # Separate match list
28282
+ self.match_panel_tm_index = 0 # Separate navigation index
28283
+
28284
+ tm_source_container, self.match_panel_tm_source, self.match_panel_tm_nav_label, tm_nav_btns = self._create_compare_panel_box(
28285
+ "📚 TM Source", tm_box_bg, text_color, border_color, has_navigation=True)
28286
+ if tm_nav_btns:
28287
+ tm_nav_btns[0].clicked.connect(lambda: self._match_panel_nav_tm(-1))
28288
+ tm_nav_btns[1].clicked.connect(lambda: self._match_panel_nav_tm(1))
28289
+ tm_layout.addWidget(tm_source_container, 1)
28290
+
28291
+ # TM Target box (GREEN, with metadata)
28292
+ tm_target_container, self.match_panel_tm_target, self.match_panel_tm_target_label, _ = self._create_compare_panel_box(
28293
+ "✅ TM Target", tm_box_bg, text_color, border_color, has_navigation=False, show_metadata_label=True)
28294
+ tm_layout.addWidget(tm_target_container, 1)
28295
+
28296
+ # Force stylesheet on the tm_container itself (the parent widget holding both boxes)
28297
+ tm_container.setStyleSheet("background-color: transparent;")
28298
+
28299
+ splitter.addWidget(tm_container)
28300
+
28301
+ # Set initial splitter sizes (60% Termview, 40% TM boxes)
28302
+ splitter.setSizes([600, 400])
28303
+
28304
+ main_layout.addWidget(splitter)
28305
+
28306
+ return widget
28307
+
28308
+ def _match_panel_nav_tm(self, direction: int):
28309
+ """Navigate TM matches in Match Panel (-1 = prev, 1 = next)"""
28310
+ if not self.match_panel_tm_matches:
28311
+ return
28312
+
28313
+ new_index = self.match_panel_tm_index + direction
28314
+ if 0 <= new_index < len(self.match_panel_tm_matches):
28315
+ self.match_panel_tm_index = new_index
28316
+ self._update_match_panel_tm_display()
28317
+
28318
+ def _update_match_panel_tm_display(self):
28319
+ """Update Match Panel TM Source and TM Target display with current match"""
28320
+ if not self.match_panel_tm_matches:
28321
+ self.match_panel_tm_source.setPlainText("(No TM match)")
28322
+ self.match_panel_tm_target.setPlainText("(No TM match)")
28323
+ if hasattr(self, 'match_panel_tm_nav_label') and self.match_panel_tm_nav_label:
28324
+ self.match_panel_tm_nav_label.setText("(0/0)")
28325
+ if hasattr(self, 'match_panel_tm_target_label') and self.match_panel_tm_target_label:
28326
+ self.match_panel_tm_target_label.setText("")
28327
+ return
28328
+
28329
+ match = self.match_panel_tm_matches[self.match_panel_tm_index]
28330
+ total = len(self.match_panel_tm_matches)
28331
+ idx = self.match_panel_tm_index + 1
28332
+
28333
+ # Update navigation label
28334
+ if hasattr(self, 'match_panel_tm_nav_label') and self.match_panel_tm_nav_label:
28335
+ nav_html = f"(<span style='font-size:8px'>{idx}/{total}</span>)"
28336
+ self.match_panel_tm_nav_label.setText(nav_html)
28337
+
28338
+ # Update TM Source text with diff highlighting
28339
+ tm_source_text = match.get('source', '')
28340
+ current_source = getattr(self, 'match_panel_current_source', '')
28341
+ if tm_source_text and current_source:
28342
+ self._set_compare_panel_text_with_diff(self.match_panel_tm_source, current_source, tm_source_text)
28343
+ else:
28344
+ self.match_panel_tm_source.setPlainText(tm_source_text or "(No TM match)")
28345
+
28346
+ # Update TM Target text (no diff highlighting needed for target)
28347
+ target_text = match.get('target', '')
28348
+ self.match_panel_tm_target.setPlainText(target_text)
28349
+
28350
+ # Update metadata label (TM name, percentage)
28351
+ if hasattr(self, 'match_panel_tm_target_label') and self.match_panel_tm_target_label:
28352
+ tm_name = match.get('tm_name', 'Unknown TM')
28353
+ match_pct = match.get('match_pct', 0)
28354
+ metadata_html = f"<span style='font-size:10px'>{tm_name}</span> (<span style='font-size:8px'>{match_pct}%</span>)"
28355
+ self.match_panel_tm_target_label.setText(metadata_html)
28356
+
28108
28357
  def _create_compare_panel_box(self, label: str, bg_color: str, text_color: str, border_color: str,
28109
28358
  has_navigation: bool = False, show_metadata_label: bool = False,
28110
28359
  shortcut_badge_text: str = None, shortcut_badge_tooltip: str = None) -> tuple:
@@ -28356,7 +28605,7 @@ class SupervertalerQt(QMainWindow):
28356
28605
 
28357
28606
  def set_compare_panel_matches(self, segment_id: int, current_source: str,
28358
28607
  tm_matches: list = None, mt_matches: list = None):
28359
- """Set all matches for the Compare Panel with navigation support
28608
+ """Set all matches for Match Panel (and Compare Panel if it exists)
28360
28609
 
28361
28610
  Args:
28362
28611
  segment_id: Current segment ID
@@ -28364,38 +28613,40 @@ class SupervertalerQt(QMainWindow):
28364
28613
  tm_matches: List of dicts with keys: source, target, tm_name, match_pct
28365
28614
  mt_matches: List of dicts with keys: translation, provider
28366
28615
  """
28367
- if not hasattr(self, 'compare_panel_current_source'):
28368
- return
28369
-
28370
- # Update segment label
28371
- if hasattr(self, 'compare_panel_segment_label'):
28372
- self.compare_panel_segment_label.setText(f"Segment {segment_id + 1}")
28373
-
28374
- # Update Current Source
28375
- self.compare_panel_current_source.setPlainText(current_source)
28376
-
28377
- # Store matches and reset indices
28378
- self.compare_panel_tm_matches = tm_matches or []
28379
- self.compare_panel_mt_matches = mt_matches or []
28380
- self.compare_panel_tm_index = 0
28381
- self.compare_panel_mt_index = 0
28382
-
28383
- # Update displays
28384
- self._update_compare_panel_mt_display()
28385
- self._update_compare_panel_tm_display()
28616
+ # Always update Match Panel with TM matches
28617
+ self.match_panel_tm_matches = tm_matches or []
28618
+ self.match_panel_tm_index = 0
28619
+ self.match_panel_current_source = current_source # Store for diff highlighting
28620
+ self._update_match_panel_tm_display()
28621
+
28622
+ # Update Compare Panel if it exists (legacy support)
28623
+ if hasattr(self, 'compare_panel_current_source') and self.compare_panel_current_source:
28624
+ # Update segment label
28625
+ if hasattr(self, 'compare_panel_segment_label'):
28626
+ self.compare_panel_segment_label.setText(f"Segment {segment_id + 1}")
28627
+
28628
+ # Update Current Source
28629
+ self.compare_panel_current_source.setPlainText(current_source)
28630
+
28631
+ # Store matches and reset indices
28632
+ self.compare_panel_tm_matches = tm_matches or []
28633
+ self.compare_panel_mt_matches = mt_matches or []
28634
+ self.compare_panel_tm_index = 0
28635
+ self.compare_panel_mt_index = 0
28636
+
28637
+ # Update displays
28638
+ self._update_compare_panel_mt_display()
28639
+ self._update_compare_panel_tm_display()
28386
28640
 
28387
28641
  def update_compare_panel(self, segment_id: int, current_source: str,
28388
28642
  tm_source: str = "", tm_target: str = "",
28389
28643
  mt_translation: str = "", tm_match_percent: int = 0,
28390
28644
  tm_name: str = "TM", mt_provider: str = "MT"):
28391
- """Update the Compare Panel with new data for the current segment (legacy single-match method)
28645
+ """Update the Match Panel with new data for the current segment (legacy single-match method)
28392
28646
 
28393
28647
  This is the legacy method that accepts single TM/MT matches. For multiple matches,
28394
28648
  use set_compare_panel_matches() instead.
28395
28649
  """
28396
- if not hasattr(self, 'compare_panel_current_source'):
28397
- return
28398
-
28399
28650
  # Convert to the new format and use set_compare_panel_matches
28400
28651
  tm_matches = []
28401
28652
  mt_matches = []
@@ -28480,7 +28731,7 @@ class SupervertalerQt(QMainWindow):
28480
28731
  text_edit.setTextCursor(cursor)
28481
28732
 
28482
28733
  def _refresh_compare_panel_theme(self):
28483
- """Refresh Compare Panel colors based on current theme"""
28734
+ """Refresh Match Panel TM box colors based on current theme"""
28484
28735
  if not hasattr(self, 'compare_panel_text_edits'):
28485
28736
  return
28486
28737
 
@@ -28491,13 +28742,11 @@ class SupervertalerQt(QMainWindow):
28491
28742
  is_dark = getattr(theme, 'is_dark', 'dark' in theme.name.lower())
28492
28743
  border_color = theme.border
28493
28744
  text_color = theme.text
28494
- title_color = theme.text_disabled
28495
- # Use theme-appropriate colors for boxes
28745
+ # Use theme-appropriate colors for Match Panel boxes (both green)
28746
+ panel_success = theme.panel_success if hasattr(theme, 'panel_success') else ("#1e3a2f" if is_dark else "#d4edda")
28496
28747
  box_colors = [
28497
- theme.panel_info if hasattr(theme, 'panel_info') else ("#2d3748" if is_dark else "#e3f2fd"),
28498
- theme.panel_warning if hasattr(theme, 'panel_warning') else ("#3d3520" if is_dark else "#fff3cd"),
28499
- theme.panel_success if hasattr(theme, 'panel_success') else ("#1e3a2f" if is_dark else "#d4edda"),
28500
- theme.panel_neutral if hasattr(theme, 'panel_neutral') else ("#2d2d3d" if is_dark else "#e8e8f0"),
28748
+ panel_success, # 0: Match Panel - TM Source (green)
28749
+ panel_success, # 1: Match Panel - TM Target (green)
28501
28750
  ]
28502
28751
  else:
28503
28752
  return
@@ -28525,10 +28774,6 @@ class SupervertalerQt(QMainWindow):
28525
28774
  color: {text_color};
28526
28775
  }}
28527
28776
  """)
28528
-
28529
- # Update segment label
28530
- if hasattr(self, 'compare_panel_segment_label'):
28531
- self.compare_panel_segment_label.setStyleSheet(f"font-weight: bold; font-size: 11px; color: {title_color};")
28532
28777
 
28533
28778
  # =========================================================================
28534
28779
  # DOCUMENT PREVIEW TAB
@@ -28627,15 +28872,33 @@ class SupervertalerQt(QMainWindow):
28627
28872
  try:
28628
28873
  row_segment_id = int(id_item.text())
28629
28874
  if row_segment_id == segment_id:
28630
- # Switch to Grid tab if we're in Document view
28631
- if hasattr(self, 'document_views_widget'):
28632
- self.document_views_widget.setCurrentIndex(0) # Grid tab
28875
+ # Switch to Grid tab first
28876
+ if hasattr(self, 'main_tabs'):
28877
+ self.main_tabs.setCurrentIndex(0) # Grid tab
28878
+
28879
+ # Handle pagination - switch to correct page if needed
28880
+ if hasattr(self, 'page_size_combo') and self.page_size_combo.currentText() != "All":
28881
+ try:
28882
+ page_size = int(self.page_size_combo.currentText())
28883
+ target_page = (row // page_size) + 1
28884
+ if hasattr(self, 'page_number_input'):
28885
+ self.page_number_input.setText(str(target_page))
28886
+ self.go_to_page()
28887
+ except ValueError:
28888
+ pass
28633
28889
 
28634
28890
  # Select this row and focus the target cell
28635
28891
  self.table.setCurrentCell(row, 3) # Column 3 = Target
28892
+ self.table.scrollToItem(self.table.item(row, 0), QTableWidget.ScrollHint.PositionAtCenter)
28893
+
28636
28894
  target_widget = self.table.cellWidget(row, 3)
28637
28895
  if target_widget:
28638
28896
  target_widget.setFocus()
28897
+ # Place cursor at end of text
28898
+ if isinstance(target_widget, QTextEdit):
28899
+ cursor = target_widget.textCursor()
28900
+ cursor.movePosition(cursor.MoveOperation.End)
28901
+ target_widget.setTextCursor(cursor)
28639
28902
 
28640
28903
  self.log(f"📄 Preview: Navigated to segment {segment_id}")
28641
28904
  return
@@ -28660,6 +28923,111 @@ class SupervertalerQt(QMainWindow):
28660
28923
 
28661
28924
  return None
28662
28925
 
28926
+ def _is_preview_tab_active(self) -> bool:
28927
+ """Check if the Preview tab is currently selected in the right panel.
28928
+
28929
+ Used to optimize performance by skipping heavy lookups when user is
28930
+ navigating in Preview mode (they're likely just reading, not translating).
28931
+ """
28932
+ if not hasattr(self, 'right_tabs') or not self.right_tabs:
28933
+ return False
28934
+ if not hasattr(self, '_preview_tab_index'):
28935
+ return False
28936
+ return self.right_tabs.currentIndex() == self._preview_tab_index
28937
+
28938
+ def _scroll_preview_to_segment(self, segment_id: int):
28939
+ """Scroll the preview to show the specified segment and highlight it.
28940
+
28941
+ ⚡ PERFORMANCE: Only does work if Preview tab is actually visible.
28942
+ This prevents expensive operations (looping through all segments,
28943
+ updating char formatting) when user is working in the grid.
28944
+ """
28945
+ # ⚡ Skip entirely if Preview tab is not visible - major performance optimization
28946
+ if not self._is_preview_tab_active():
28947
+ return
28948
+
28949
+ if not hasattr(self, 'preview_widgets') or not self.preview_widgets:
28950
+ return
28951
+
28952
+ for widget in self.preview_widgets:
28953
+ if not hasattr(widget, 'segment_positions') or not hasattr(widget, 'preview_text'):
28954
+ continue
28955
+
28956
+ preview_text = widget.preview_text
28957
+
28958
+ # Find the position of the target segment
28959
+ target_start = None
28960
+ target_end = None
28961
+ for (start_pos, end_pos), seg_id in widget.segment_positions.items():
28962
+ if seg_id == segment_id:
28963
+ target_start = start_pos
28964
+ target_end = end_pos
28965
+ break
28966
+
28967
+ if target_start is None:
28968
+ continue
28969
+
28970
+ # Update highlighting - need to re-render to show new selection
28971
+ # Store the new current segment ID and re-render
28972
+ widget.current_highlighted_segment_id = segment_id
28973
+
28974
+ # Clear existing formatting and re-apply with new highlight
28975
+ cursor = preview_text.textCursor()
28976
+
28977
+ # First, remove any existing yellow highlight from ALL segments
28978
+ for (start_pos, end_pos), seg_id in widget.segment_positions.items():
28979
+ cursor.setPosition(start_pos)
28980
+ cursor.setPosition(end_pos, QTextCursor.MoveMode.KeepAnchor)
28981
+ fmt = cursor.charFormat()
28982
+ # Reset background - use white or status-based color
28983
+ if seg_id == segment_id:
28984
+ fmt.setBackground(QColor('#fff9c4')) # Yellow for current
28985
+ else:
28986
+ # Find the segment to get its status
28987
+ seg = next((s for s in self.current_project.segments if s.id == seg_id), None)
28988
+ if seg:
28989
+ if seg.status == 'not_started':
28990
+ fmt.setBackground(QColor('#ffe6e6')) # Light red
28991
+ elif seg.status in ('translated', 'pretranslated'):
28992
+ fmt.setBackground(QColor('#e6ffe6')) # Light green
28993
+ elif seg.status in ('confirmed', 'approved', 'proofread'):
28994
+ fmt.setBackground(QColor('#e6f3ff')) # Light blue
28995
+ else:
28996
+ fmt.setBackground(QColor('white'))
28997
+ else:
28998
+ fmt.setBackground(QColor('white'))
28999
+ cursor.mergeCharFormat(fmt)
29000
+
29001
+ # Now scroll to the target segment, centered in viewport
29002
+ scroll_cursor = preview_text.textCursor()
29003
+ scroll_cursor.setPosition(target_start)
29004
+ preview_text.setTextCursor(scroll_cursor)
29005
+ self._center_cursor_in_preview(preview_text)
29006
+
29007
+ def _center_cursor_in_preview(self, preview_text: QTextEdit):
29008
+ """Center the current cursor position in the preview viewport"""
29009
+ # Get the cursor rectangle (position in document coordinates)
29010
+ cursor_rect = preview_text.cursorRect()
29011
+
29012
+ # Get viewport height
29013
+ viewport_height = preview_text.viewport().height()
29014
+
29015
+ # Calculate scroll position to center the cursor
29016
+ # cursor_rect.top() is relative to the viewport, we need document position
29017
+ scrollbar = preview_text.verticalScrollBar()
29018
+ current_scroll = scrollbar.value()
29019
+
29020
+ # The cursor rect top is relative to viewport, so add current scroll to get document position
29021
+ cursor_doc_y = current_scroll + cursor_rect.top()
29022
+
29023
+ # Calculate target scroll to put cursor in center
29024
+ target_scroll = cursor_doc_y - (viewport_height // 2)
29025
+
29026
+ # Clamp to valid scroll range
29027
+ target_scroll = max(0, min(target_scroll, scrollbar.maximum()))
29028
+
29029
+ scrollbar.setValue(target_scroll)
29030
+
28663
29031
  def refresh_preview(self):
28664
29032
  """Refresh all preview tabs with current document content"""
28665
29033
  if not self.current_project or not self.current_project.segments:
@@ -28716,15 +29084,91 @@ class SupervertalerQt(QMainWindow):
28716
29084
 
28717
29085
  # Render all segments
28718
29086
  current_segment_id = self._get_current_segment_id()
29087
+ segments = self.current_project.segments
28719
29088
 
28720
- for seg in self.current_project.segments:
29089
+ for idx, seg in enumerate(segments):
28721
29090
  style = getattr(seg, 'style', 'Normal') or 'Normal'
29091
+ paragraph_id = getattr(seg, 'paragraph_id', 0)
29092
+ seg_type = getattr(seg, 'type', 'para') or 'para'
29093
+
29094
+ # Normalize type for comparison - grid shows symbols like "¶" for para, "Sub" for subtitle
29095
+ # Type field values: "para", "heading", "list_item", "table_cell", "Sub", "¶", etc.
29096
+ seg_type_lower = seg_type.lower() if seg_type else 'para'
29097
+
29098
+ # Check if this is a heading/subtitle
29099
+ is_heading = ('Heading' in style or 'Title' in style or 'Subtitle' in style
29100
+ or seg_type_lower in ('heading', 'sub', 'subtitle', 'title'))
29101
+
29102
+ # Check if this is a list item
29103
+ source_text = seg.source.strip() if seg.source else ""
29104
+ is_list_item = ('<li-o>' in source_text or '<li-b>' in source_text or
29105
+ '<li>' in source_text or seg_type_lower == 'list_item')
29106
+
29107
+ # Check if this is a new paragraph (¶ symbol or "para" type)
29108
+ is_new_paragraph = seg_type == '¶' or seg_type_lower == 'para'
29109
+
29110
+ # Determine spacing before this segment
29111
+ need_double_break = False # Empty line (for paragraph separation)
29112
+ need_single_break = False # Line break (for headings, list items)
29113
+ need_space = False # Just a space (running text in same paragraph)
29114
+
29115
+ if idx > 0:
29116
+ prev_seg = segments[idx - 1]
29117
+ prev_style = getattr(prev_seg, 'style', 'Normal') or 'Normal'
29118
+ prev_paragraph_id = getattr(prev_seg, 'paragraph_id', 0)
29119
+ prev_type = getattr(prev_seg, 'type', 'para') or 'para'
29120
+ prev_type_lower = prev_type.lower() if prev_type else 'para'
29121
+
29122
+ prev_is_heading = ('Heading' in prev_style or 'Title' in prev_style or
29123
+ 'Subtitle' in prev_style or
29124
+ prev_type_lower in ('heading', 'sub', 'subtitle', 'title'))
29125
+ prev_is_paragraph = prev_type == '¶' or prev_type_lower == 'para'
29126
+ prev_is_list = prev_type_lower == 'list_item' or '<li' in (prev_seg.source or '')
29127
+
29128
+ # Rules for spacing:
29129
+ # 1. After a heading: double break (empty line)
29130
+ if prev_is_heading:
29131
+ need_double_break = True
29132
+ # 2. Before a heading: double break
29133
+ elif is_heading:
29134
+ need_double_break = True
29135
+ # 3. New paragraph (different paragraph_id or new ¶ type): double break
29136
+ elif is_new_paragraph and prev_is_paragraph:
29137
+ # If paragraph_id is being used and changed, it's a new paragraph
29138
+ if paragraph_id != prev_paragraph_id and (paragraph_id != 0 or prev_paragraph_id != 0):
29139
+ need_double_break = True
29140
+ # If both are ¶ type, they are separate paragraphs
29141
+ elif prev_type == '¶' and seg_type == '¶':
29142
+ need_double_break = True
29143
+ else:
29144
+ # Same paragraph, running text
29145
+ need_space = True
29146
+ # 4. List items: single break (each on own line)
29147
+ elif is_list_item or prev_is_list:
29148
+ need_single_break = True
29149
+ # 5. Default: space (running text)
29150
+ else:
29151
+ need_space = True
29152
+
29153
+ # Insert spacing/breaks
29154
+ if need_double_break:
29155
+ cursor.insertText("\n\n") # Empty line between paragraphs
29156
+ elif need_single_break:
29157
+ cursor.insertText("\n") # Line break
29158
+ elif need_space:
29159
+ cursor.insertText(" ") # Space between sentences
28722
29160
 
28723
29161
  # Set formatting based on style
28724
29162
  char_format = QTextCharFormat()
28725
29163
  char_format.setFontFamily("Georgia")
28726
-
28727
- if 'Heading 1' in style or 'Heading1' in style or 'Title' in style:
29164
+
29165
+ # Check type field for heading detection as well
29166
+ if is_heading and seg_type_lower in ('sub', 'subtitle'):
29167
+ # Subtitle style (like "TECHNISCH DOMEIN")
29168
+ char_format.setFontPointSize(13)
29169
+ char_format.setFontWeight(QFont.Weight.Bold)
29170
+ char_format.setForeground(QColor('#1f4068'))
29171
+ elif 'Heading 1' in style or 'Heading1' in style or 'Title' in style or seg_type_lower == 'title':
28728
29172
  char_format.setFontPointSize(18)
28729
29173
  char_format.setFontWeight(QFont.Weight.Bold)
28730
29174
  char_format.setForeground(QColor('#1a1a2e'))
@@ -28732,7 +29176,7 @@ class SupervertalerQt(QMainWindow):
28732
29176
  char_format.setFontPointSize(15)
28733
29177
  char_format.setFontWeight(QFont.Weight.Bold)
28734
29178
  char_format.setForeground(QColor('#16213e'))
28735
- elif 'Heading 3' in style or 'Heading3' in style:
29179
+ elif 'Heading 3' in style or 'Heading3' in style or seg_type_lower == 'heading':
28736
29180
  char_format.setFontPointSize(13)
28737
29181
  char_format.setFontWeight(QFont.Weight.Bold)
28738
29182
  char_format.setForeground(QColor('#1f4068'))
@@ -28776,13 +29220,31 @@ class SupervertalerQt(QMainWindow):
28776
29220
 
28777
29221
  end_pos = cursor.position()
28778
29222
  widget.segment_positions[(start_pos, end_pos)] = seg.id
28779
-
28780
- # Add line break after each segment
28781
- cursor.insertText("\n")
28782
-
28783
- # Set cursor to beginning
28784
- cursor.movePosition(QTextCursor.MoveOperation.Start)
28785
- preview_text.setTextCursor(cursor)
29223
+
29224
+ # Track position of current segment for scrolling
29225
+ if seg.id == current_segment_id:
29226
+ widget.current_segment_start_pos = start_pos
29227
+
29228
+ # Note: Line breaks are now handled at the START of each segment based on context
29229
+ # This ensures running text flows naturally while paragraphs are separated
29230
+
29231
+ # Add final newline at end of document
29232
+ cursor.insertText("\n")
29233
+
29234
+ # Scroll to current segment if we have one
29235
+ if hasattr(widget, 'current_segment_start_pos') and widget.current_segment_start_pos is not None:
29236
+ # Create cursor at the current segment position
29237
+ scroll_cursor = preview_text.textCursor()
29238
+ scroll_cursor.setPosition(widget.current_segment_start_pos)
29239
+ preview_text.setTextCursor(scroll_cursor)
29240
+ # Center the segment in the viewport
29241
+ # Use QTimer to delay centering until after layout is complete
29242
+ from PyQt6.QtCore import QTimer
29243
+ QTimer.singleShot(0, lambda pt=preview_text: self._center_cursor_in_preview(pt))
29244
+ else:
29245
+ # Set cursor to beginning if no current segment
29246
+ cursor.movePosition(QTextCursor.MoveOperation.Start)
29247
+ preview_text.setTextCursor(cursor)
28786
29248
 
28787
29249
  def _render_formatted_text(self, cursor, text: str, base_format: QTextCharFormat,
28788
29250
  list_number: Optional[int] = None):
@@ -29300,39 +29762,39 @@ class SupervertalerQt(QMainWindow):
29300
29762
  self.save_current_font_sizes()
29301
29763
 
29302
29764
  # =========================================================================
29303
- # COMPARE PANEL ZOOM METHODS
29765
+ # MATCH PANEL ZOOM METHODS
29304
29766
  # =========================================================================
29305
29767
 
29306
- # Class variable for Compare Panel font size
29307
- compare_panel_font_size = 10 # Default font size
29768
+ # Class variable for Match Panel font size (TM Source/Target boxes)
29769
+ match_panel_font_size = 10 # Default font size
29308
29770
 
29309
- def compare_panel_zoom_in(self):
29310
- """Increase font size in Compare Panel"""
29311
- SupervertalerQt.compare_panel_font_size = min(18, SupervertalerQt.compare_panel_font_size + 1)
29312
- self._apply_compare_panel_font_size()
29771
+ def match_panel_zoom_in(self):
29772
+ """Increase font size in Match Panel TM boxes"""
29773
+ SupervertalerQt.match_panel_font_size = min(18, SupervertalerQt.match_panel_font_size + 1)
29774
+ self._apply_match_panel_font_size()
29313
29775
  self.save_current_font_sizes()
29314
- self.log(f"Compare Panel font size: {SupervertalerQt.compare_panel_font_size}pt")
29776
+ self.log(f"Match Panel font size: {SupervertalerQt.match_panel_font_size}pt")
29315
29777
 
29316
- def compare_panel_zoom_out(self):
29317
- """Decrease font size in Compare Panel"""
29318
- SupervertalerQt.compare_panel_font_size = max(7, SupervertalerQt.compare_panel_font_size - 1)
29319
- self._apply_compare_panel_font_size()
29778
+ def match_panel_zoom_out(self):
29779
+ """Decrease font size in Match Panel TM boxes"""
29780
+ SupervertalerQt.match_panel_font_size = max(7, SupervertalerQt.match_panel_font_size - 1)
29781
+ self._apply_match_panel_font_size()
29320
29782
  self.save_current_font_sizes()
29321
- self.log(f"Compare Panel font size: {SupervertalerQt.compare_panel_font_size}pt")
29783
+ self.log(f"Match Panel font size: {SupervertalerQt.match_panel_font_size}pt")
29322
29784
 
29323
- def compare_panel_zoom_reset(self):
29324
- """Reset font size in Compare Panel to default"""
29325
- SupervertalerQt.compare_panel_font_size = 10
29326
- self._apply_compare_panel_font_size()
29785
+ def match_panel_zoom_reset(self):
29786
+ """Reset font size in Match Panel TM boxes to default"""
29787
+ SupervertalerQt.match_panel_font_size = 10
29788
+ self._apply_match_panel_font_size()
29327
29789
  self.save_current_font_sizes()
29328
- self.log("Compare Panel font size reset to 10pt")
29790
+ self.log("Match Panel font size reset to 10pt")
29329
29791
 
29330
- def _apply_compare_panel_font_size(self):
29331
- """Apply current font size to all Compare Panel text edits"""
29792
+ def _apply_match_panel_font_size(self):
29793
+ """Apply current font size to Match Panel TM Source/Target text edits"""
29332
29794
  if not hasattr(self, 'compare_panel_text_edits'):
29333
29795
  return
29334
29796
 
29335
- font_size = SupervertalerQt.compare_panel_font_size
29797
+ font_size = SupervertalerQt.match_panel_font_size
29336
29798
 
29337
29799
  for item in self.compare_panel_text_edits:
29338
29800
  text_edit = item[0] # First element is the QTextEdit
@@ -29361,8 +29823,8 @@ class SupervertalerQt(QMainWindow):
29361
29823
  general_settings['results_show_tags'] = CompactMatchItem.show_tags
29362
29824
  if hasattr(EditableGridTextEditor, 'tag_highlight_color'):
29363
29825
  general_settings['tag_highlight_color'] = EditableGridTextEditor.tag_highlight_color
29364
- # Save Compare Panel font size
29365
- general_settings['compare_panel_font_size'] = SupervertalerQt.compare_panel_font_size
29826
+ # Save Match Panel font size
29827
+ general_settings['match_panel_font_size'] = SupervertalerQt.match_panel_font_size
29366
29828
  # Preserve other settings
29367
29829
  if 'restore_last_project' not in general_settings:
29368
29830
  general_settings['restore_last_project'] = False
@@ -29403,7 +29865,6 @@ class SupervertalerQt(QMainWindow):
29403
29865
  self.precision_scroll_divisor = settings.get('precision_scroll_divisor', 3)
29404
29866
  # Load auto-center active segment setting (default True, like memoQ/Trados)
29405
29867
  self.auto_center_active_segment = settings.get('auto_center_active_segment', True)
29406
- self.log(f"🔄 Loaded auto-center setting: {self.auto_center_active_segment}")
29407
29868
  # Load termbase display settings
29408
29869
  self.termbase_display_order = settings.get('termbase_display_order', 'appearance')
29409
29870
  self.termbase_hide_shorter_matches = settings.get('termbase_hide_shorter_matches', False)
@@ -29428,6 +29889,9 @@ class SupervertalerQt(QMainWindow):
29428
29889
  self.show_translation_results_pane = settings.get('show_translation_results_pane', False)
29429
29890
  self.show_compare_panel = settings.get('show_compare_panel', True)
29430
29891
 
29892
+ # 🧪 EXPERIMENTAL: Load cache kill switch setting (default: False = caches enabled)
29893
+ self.disable_all_caches = settings.get('disable_all_caches', False)
29894
+
29431
29895
  # Load LLM provider settings for AI Assistant
29432
29896
  llm_settings = self.load_llm_settings()
29433
29897
  self.current_provider = llm_settings.get('provider', 'openai')
@@ -29706,11 +30170,11 @@ class SupervertalerQt(QMainWindow):
29706
30170
  EditableGridTextEditor.focus_border_color = focus_border_color
29707
30171
  EditableGridTextEditor.focus_border_thickness = focus_border_thickness
29708
30172
 
29709
- # Load and apply Compare Panel font size
29710
- compare_panel_size = general_settings.get('compare_panel_font_size', 10)
29711
- if 7 <= compare_panel_size <= 18:
29712
- SupervertalerQt.compare_panel_font_size = compare_panel_size
29713
- self._apply_compare_panel_font_size()
30173
+ # Load and apply Match Panel font size
30174
+ match_panel_size = general_settings.get('match_panel_font_size', 10)
30175
+ if 7 <= match_panel_size <= 18:
30176
+ SupervertalerQt.match_panel_font_size = match_panel_size
30177
+ self._apply_match_panel_font_size()
29714
30178
 
29715
30179
  if hasattr(self, 'results_panels'):
29716
30180
  # Load and apply match limits
@@ -29798,6 +30262,10 @@ class SupervertalerQt(QMainWindow):
29798
30262
 
29799
30263
  # Update status bar progress stats (debounced - only after user stops typing)
29800
30264
  self.update_progress_stats()
30265
+
30266
+ # 🚀 IDLE PREFETCH: User stopped typing, prefetch next segments for instant Ctrl+Enter
30267
+ self._trigger_idle_prefetch(row)
30268
+
29801
30269
  except Exception as e:
29802
30270
  self.log(f"Error in debounced target handler: {e}")
29803
30271
 
@@ -29844,11 +30312,15 @@ class SupervertalerQt(QMainWindow):
29844
30312
  return
29845
30313
  self._last_selected_row = current_row
29846
30314
 
29847
- # ⚡ FAST PATH: For arrow key navigation, defer heavy lookups
29848
- # This makes segment navigation feel instant
30315
+ # ⚡ FAST PATH: For arrow key OR Ctrl+Enter navigation, defer heavy lookups
30316
+ # This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
29849
30317
  is_arrow_nav = getattr(self, '_arrow_key_navigation', False)
29850
- if is_arrow_nav:
29851
- self._arrow_key_navigation = False # Reset flag
30318
+ is_ctrl_enter_nav = getattr(self, '_ctrl_enter_navigation', False)
30319
+
30320
+ if is_arrow_nav or is_ctrl_enter_nav:
30321
+ self._arrow_key_navigation = False # Reset flags
30322
+ self._ctrl_enter_navigation = False
30323
+
29852
30324
  # Schedule deferred lookup with short delay (150ms) for rapid navigation
29853
30325
  if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
29854
30326
  self._deferred_lookup_timer.stop()
@@ -29974,6 +30446,9 @@ class SupervertalerQt(QMainWindow):
29974
30446
  self.log(f"⚠️ Could not find segment with ID {segment_id} in project")
29975
30447
  return
29976
30448
 
30449
+ # Update Preview panel - scroll to and highlight this segment
30450
+ self._scroll_preview_to_segment(segment_id)
30451
+
29977
30452
  # Update Translation Results panel header with segment info
29978
30453
  if hasattr(self, 'results_panels'):
29979
30454
  for panel in self.results_panels:
@@ -30025,103 +30500,104 @@ class SupervertalerQt(QMainWindow):
30025
30500
  notes=segment.notes
30026
30501
  )
30027
30502
 
30503
+ # ⚡ PERFORMANCE: Skip heavy lookups if Preview tab is open
30504
+ # User is likely just reading/reviewing, not actively translating
30505
+ if self._is_preview_tab_active():
30506
+ if self.debug_mode_enabled:
30507
+ self.log(f"⏭️ Preview tab active - skipping TM/Termbase lookups for segment {segment.id}")
30508
+ return
30509
+
30028
30510
  # Get termbase matches (from cache or search on-demand) - ONLY if enabled
30029
30511
  matches_dict = None # Initialize at the top level
30512
+ cached_matches = None # Initialize for cache skip path
30030
30513
 
30031
- # 🚀 CHECK PREFETCH CACHE FIRST for instant display (like memoQ)
30032
- segment_id = segment.id
30033
- with self.translation_matches_cache_lock:
30034
- if segment_id in self.translation_matches_cache:
30035
- cached_matches = self.translation_matches_cache[segment_id]
30036
-
30037
- # Count matches in each category
30038
- tm_count = len(cached_matches.get("TM", []))
30039
- tb_count = len(cached_matches.get("Termbases", []))
30040
- mt_count = len(cached_matches.get("MT", []))
30041
- llm_count = len(cached_matches.get("LLM", []))
30042
-
30043
- self.log(f"⚡ CACHE HIT for segment {segment_id}: TM={tm_count}, TB={tb_count}, MT={mt_count}, LLM={llm_count}")
30044
-
30045
- # Use cached results even if empty (to avoid re-searching)
30046
- if True: # Always use cache if it exists
30047
- # Display cached matches immediately
30048
- if hasattr(self, 'results_panels'):
30049
- for panel in self.results_panels:
30050
- try:
30051
- panel.clear()
30052
- panel.set_matches(cached_matches)
30053
- except Exception as e:
30054
- self.log(f"Error displaying cached matches: {e}")
30055
-
30056
- # 🔄 Update TermView with cached termbase matches (always update, even if empty)
30057
- if hasattr(self, 'termview_widget') and self.current_project:
30058
- try:
30059
- # Convert TranslationMatch objects to dict format for termview
30060
- termbase_matches = [
30061
- {
30062
- 'source_term': match.source,
30063
- 'target_term': match.target,
30064
- 'termbase_name': match.metadata.get('termbase_name', '') if match.metadata else '',
30065
- 'ranking': match.metadata.get('ranking', 99) if match.metadata else 99,
30066
- 'is_project_termbase': match.metadata.get('is_project_termbase', False) if match.metadata else False,
30067
- 'term_id': match.metadata.get('term_id') if match.metadata else None,
30068
- 'termbase_id': match.metadata.get('termbase_id') if match.metadata else None,
30069
- 'notes': match.metadata.get('notes', '') if match.metadata else ''
30070
- }
30071
- for match in cached_matches.get("Termbases", [])
30072
- ]
30073
- # Also get NT matches (fresh, not cached - they may have changed)
30074
- nt_matches = self.find_nt_matches_in_source(segment.source)
30075
-
30076
- # Update both Termview widgets (left and right)
30077
- self._update_both_termviews(segment.source, termbase_matches, nt_matches)
30078
- except Exception as e:
30079
- self.log(f"Error updating termview from cache: {e}")
30514
+ # 🧪 EXPERIMENTAL: Skip ALL cache checks if cache kill switch is enabled
30515
+ if not getattr(self, 'disable_all_caches', False):
30516
+ # 🚀 CHECK PREFETCH CACHE FIRST for instant display (like memoQ)
30517
+ segment_id = segment.id
30518
+ with self.translation_matches_cache_lock:
30519
+ if segment_id in self.translation_matches_cache:
30520
+ cached_matches = self.translation_matches_cache[segment_id]
30080
30521
 
30081
- # 🎯 AUTO-INSERT 100% TM MATCH from cache (if enabled in settings)
30082
- self.log(f"🎯 CACHE: Auto-insert setting: {self.auto_insert_100_percent_matches}, TM count: {tm_count}")
30083
- self.log(f"🎯 CACHE: Segment target: '{segment.target}' (length={len(segment.target)}, stripped='{segment.target.strip()}')")
30522
+ # Count matches in each category
30523
+ tm_count = len(cached_matches.get("TM", []))
30524
+ tb_count = len(cached_matches.get("Termbases", []))
30525
+ mt_count = len(cached_matches.get("MT", []))
30526
+ llm_count = len(cached_matches.get("LLM", []))
30084
30527
 
30085
- if self.auto_insert_100_percent_matches and tm_count > 0:
30086
- # Check if segment target is empty (don't overwrite existing translations)
30087
- target_empty = not segment.target or len(segment.target.strip()) == 0
30088
- self.log(f"🎯 CACHE: Target empty check: {target_empty}")
30089
-
30090
- if target_empty:
30091
- # Find first 100% match in cached TM results
30092
- best_match = None
30093
- for tm_match in cached_matches.get("TM", []):
30094
- self.log(f"🔍 CACHE: Checking TM match: relevance={tm_match.relevance} (type={type(tm_match.relevance).__name__})")
30095
- # Use >= 99.5 to handle potential floating point issues
30096
- if float(tm_match.relevance) >= 99.5:
30097
- best_match = tm_match
30098
- self.log(f"✅ CACHE: Found 100% match with target: '{tm_match.target[:50]}...'")
30099
- break
30100
-
30101
- if best_match:
30102
- self.log(f"✨ CACHE: Auto-inserting 100% TM match into segment {segment.id} at row {current_row}")
30103
- self._auto_insert_tm_match(segment, best_match.target, current_row)
30104
- # Play 100% TM match alert sound
30105
- self._play_sound_effect('tm_100_percent_match')
30106
- else:
30107
- relevances = [(tm.relevance, type(tm.relevance).__name__) for tm in cached_matches.get("TM", [])]
30108
- self.log(f"⚠️ CACHE: No 100% match found. All relevances: {relevances}")
30109
- else:
30110
- self.log(f"⚠️ CACHE: Target not empty ('{segment.target}') - skipping auto-insert")
30528
+ self.log(f"⚡ CACHE HIT for segment {segment_id}: TM={tm_count}, TB={tb_count}, MT={mt_count}, LLM={llm_count}")
30529
+ else:
30530
+ if self.debug_mode_enabled:
30531
+ self.log(f"🧪 Cache DISABLED - forcing fresh lookup for segment {segment.id}")
30532
+
30533
+ # Process cached matches if we have them
30534
+ if cached_matches is not None:
30535
+ # Display cached matches immediately
30536
+ if hasattr(self, 'results_panels'):
30537
+ for panel in self.results_panels:
30538
+ try:
30539
+ panel.clear()
30540
+ panel.set_matches(cached_matches)
30541
+ except Exception as e:
30542
+ self.log(f"Error displaying cached matches: {e}")
30543
+
30544
+ # 🔄 Update TermView with cached termbase matches (always update, even if empty)
30545
+ if hasattr(self, 'termview_widget') and self.current_project:
30546
+ try:
30547
+ # Convert TranslationMatch objects to dict format for termview
30548
+ termbase_matches = [
30549
+ {
30550
+ 'source_term': match.source,
30551
+ 'target_term': match.target,
30552
+ 'termbase_name': match.metadata.get('termbase_name', '') if match.metadata else '',
30553
+ 'ranking': match.metadata.get('ranking', 99) if match.metadata else 99,
30554
+ 'is_project_termbase': match.metadata.get('is_project_termbase', False) if match.metadata else False,
30555
+ 'term_id': match.metadata.get('term_id') if match.metadata else None,
30556
+ 'termbase_id': match.metadata.get('termbase_id') if match.metadata else None,
30557
+ 'notes': match.metadata.get('notes', '') if match.metadata else ''
30558
+ }
30559
+ for match in cached_matches.get("Termbases", [])
30560
+ ]
30561
+ # Also get NT matches (fresh, not cached - they may have changed)
30562
+ nt_matches = self.find_nt_matches_in_source(segment.source)
30111
30563
 
30112
- # 🔊 Play fuzzy match sound if fuzzy matches found from cache (but not 100%)
30113
- if tm_count > 0:
30114
- tm_matches = cached_matches.get("TM", [])
30115
- has_100_match = any(float(tm.relevance) >= 99.5 for tm in tm_matches)
30116
- has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in tm_matches)
30117
- if has_fuzzy_match and not has_100_match:
30118
- self._play_sound_effect('tm_fuzzy_match')
30564
+ # Update both Termview widgets (left and right)
30565
+ self._update_both_termviews(segment.source, termbase_matches, nt_matches)
30566
+ except Exception as e:
30567
+ self.log(f"Error updating termview from cache: {e}")
30568
+
30569
+ # 🎯 AUTO-INSERT 100% TM MATCH from cache (if enabled in settings)
30570
+ tm_count = len(cached_matches.get("TM", []))
30571
+ if self.auto_insert_100_percent_matches and tm_count > 0:
30572
+ # Check if segment target is empty (don't overwrite existing translations)
30573
+ target_empty = not segment.target or len(segment.target.strip()) == 0
30574
+
30575
+ if target_empty:
30576
+ # Find first 100% match in cached TM results
30577
+ best_match = None
30578
+ for tm_match in cached_matches.get("TM", []):
30579
+ # Use >= 99.5 to handle potential floating point issues
30580
+ if float(tm_match.relevance) >= 99.5:
30581
+ best_match = tm_match
30582
+ break
30119
30583
 
30120
- # Skip the slow lookup below, we already have everything
30121
- # Continue to prefetch trigger at the end
30122
- matches_dict = cached_matches # Set for later use
30123
- else:
30124
- cached_matches = None
30584
+ if best_match:
30585
+ self.log(f"✨ Auto-inserting 100% TM match into segment {segment.id}")
30586
+ self._auto_insert_tm_match(segment, best_match.target, current_row)
30587
+ # Play 100% TM match alert sound
30588
+ self._play_sound_effect('tm_100_percent_match')
30589
+
30590
+ # 🔊 Play fuzzy match sound if fuzzy matches found from cache (but not 100%)
30591
+ if tm_count > 0:
30592
+ tm_matches = cached_matches.get("TM", [])
30593
+ has_100_match = any(float(tm.relevance) >= 99.5 for tm in tm_matches)
30594
+ has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in tm_matches)
30595
+ if has_fuzzy_match and not has_100_match:
30596
+ self._play_sound_effect('tm_fuzzy_match')
30597
+
30598
+ # Skip the slow lookup below, we already have everything
30599
+ # Continue to prefetch trigger at the end
30600
+ matches_dict = cached_matches # Set for later use
30125
30601
 
30126
30602
  # Check if TM/Termbase matching is enabled
30127
30603
  if not matches_dict and (not self.enable_tm_matching and not self.enable_termbase_matching):
@@ -30133,19 +30609,23 @@ class SupervertalerQt(QMainWindow):
30133
30609
  # Termbase lookup (if enabled)
30134
30610
  stored_matches = {}
30135
30611
  if self.enable_termbase_matching:
30136
- # Check cache first (thread-safe) - uses `in` check to properly handle empty caches
30612
+ # 🧪 EXPERIMENTAL: Skip termbase cache if cache kill switch is enabled
30137
30613
  cache_checked = False
30138
- with self.termbase_cache_lock:
30139
- if segment_id in self.termbase_cache:
30140
- stored_matches = self.termbase_cache[segment_id]
30141
- cache_checked = True
30614
+ if not getattr(self, 'disable_all_caches', False):
30615
+ # Check cache first (thread-safe) - uses `in` check to properly handle empty caches
30616
+ with self.termbase_cache_lock:
30617
+ if segment_id in self.termbase_cache:
30618
+ stored_matches = self.termbase_cache[segment_id]
30619
+ cache_checked = True
30142
30620
 
30143
30621
  if not cache_checked and source_widget:
30144
30622
  stored_matches = self.find_termbase_matches_in_source(segment.source)
30145
30623
 
30146
30624
  # Store in cache for future access (thread-safe) - EVEN IF EMPTY
30147
- with self.termbase_cache_lock:
30148
- self.termbase_cache[segment_id] = stored_matches
30625
+ # BUT skip cache storage if cache kill switch is enabled
30626
+ if not getattr(self, 'disable_all_caches', False):
30627
+ with self.termbase_cache_lock:
30628
+ self.termbase_cache[segment_id] = stored_matches
30149
30629
 
30150
30630
  # CRITICAL FIX: Always update Termview (even with empty results) - show "No matches" state
30151
30631
  if hasattr(self, 'termview_widget') and self.current_project:
@@ -30349,17 +30829,37 @@ class SupervertalerQt(QMainWindow):
30349
30829
  self._schedule_mt_and_llm_matches(segment, termbase_matches)
30350
30830
 
30351
30831
  # Trigger prefetch for next 20 segments (adaptive background caching)
30832
+ # Also trigger PROACTIVE HIGHLIGHTING for already-cached segments
30352
30833
  if self.current_project and current_row >= 0:
30834
+ import json
30353
30835
  next_segment_ids = []
30354
30836
  start_idx = current_row + 1
30355
30837
  end_idx = min(start_idx + 20, len(self.current_project.segments))
30356
30838
 
30839
+ print(f"[PROACTIVE NAV DEBUG] Navigation to row {current_row}, checking segments {start_idx} to {end_idx}")
30840
+
30357
30841
  for seg in self.current_project.segments[start_idx:end_idx]:
30358
- # Only prefetch if not already cached
30842
+ # Check if already cached
30359
30843
  with self.translation_matches_cache_lock:
30360
- if seg.id not in self.translation_matches_cache:
30361
- next_segment_ids.append(seg.id)
30844
+ is_cached = seg.id in self.translation_matches_cache
30845
+
30846
+ if not is_cached:
30847
+ next_segment_ids.append(seg.id)
30848
+ else:
30849
+ # PROACTIVE HIGHLIGHTING: Apply highlighting for cached segments
30850
+ # that haven't been highlighted yet
30851
+ try:
30852
+ with self.termbase_cache_lock:
30853
+ termbase_raw = self.termbase_cache.get(seg.id, {})
30854
+ print(f"[PROACTIVE NAV DEBUG] Seg {seg.id}: cached, termbase_raw has {len(termbase_raw) if termbase_raw else 0} matches")
30855
+ if termbase_raw:
30856
+ termbase_json = json.dumps(termbase_raw)
30857
+ print(f"[PROACTIVE NAV DEBUG] Calling _apply_proactive_highlighting for seg {seg.id}")
30858
+ self._apply_proactive_highlighting(seg.id, termbase_json)
30859
+ except Exception as e:
30860
+ print(f"[PROACTIVE NAV DEBUG] Error for seg {seg.id}: {e}")
30362
30861
 
30862
+ print(f"[PROACTIVE NAV DEBUG] Need to prefetch: {len(next_segment_ids)} segments")
30363
30863
  if next_segment_ids:
30364
30864
  self._start_prefetch_worker(next_segment_ids)
30365
30865
 
@@ -32879,6 +33379,87 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
32879
33379
  import traceback
32880
33380
  self.log(f"Traceback: {traceback.format_exc()}")
32881
33381
 
33382
+ def _apply_proactive_highlighting(self, segment_id: int, termbase_matches_json: str):
33383
+ """
33384
+ Apply termbase highlighting proactively to a segment that was prefetched.
33385
+ Called on main thread via signal from prefetch worker.
33386
+
33387
+ This enables "see-ahead" highlighting: while you're working on segment N,
33388
+ segments N+1, N+2, N+3 already show their glossary matches highlighted.
33389
+
33390
+ Args:
33391
+ segment_id: The segment ID to highlight
33392
+ termbase_matches_json: JSON-encoded termbase matches dict (thread-safe transfer)
33393
+ """
33394
+ import json
33395
+
33396
+ print(f"[PROACTIVE DEBUG] _apply_proactive_highlighting called for segment {segment_id}")
33397
+
33398
+ if not self.current_project or not self.table:
33399
+ print(f"[PROACTIVE DEBUG] Early exit: no project or table")
33400
+ return
33401
+
33402
+ try:
33403
+ # Decode the matches from JSON
33404
+ termbase_matches = json.loads(termbase_matches_json) if termbase_matches_json else {}
33405
+
33406
+ print(f"[PROACTIVE DEBUG] Decoded {len(termbase_matches)} termbase matches")
33407
+
33408
+ if not termbase_matches:
33409
+ print(f"[PROACTIVE DEBUG] No matches to highlight, returning")
33410
+ return # Nothing to highlight
33411
+
33412
+ # Find the row for this segment ID
33413
+ row = -1
33414
+ for r in range(self.table.rowCount()):
33415
+ id_item = self.table.item(r, 0)
33416
+ if id_item:
33417
+ try:
33418
+ row_seg_id = int(id_item.text())
33419
+ if row_seg_id == segment_id:
33420
+ row = r
33421
+ break
33422
+ except ValueError:
33423
+ continue
33424
+
33425
+ print(f"[PROACTIVE DEBUG] Found row {row} for segment {segment_id}")
33426
+
33427
+ if row < 0:
33428
+ print(f"[PROACTIVE DEBUG] Segment not visible in current page")
33429
+ return # Segment not visible in current page
33430
+
33431
+ # Get segment source text
33432
+ segment = None
33433
+ for seg in self.current_project.segments:
33434
+ if seg.id == segment_id:
33435
+ segment = seg
33436
+ break
33437
+
33438
+ if not segment:
33439
+ print(f"[PROACTIVE DEBUG] Segment object not found")
33440
+ return
33441
+
33442
+ print(f"[PROACTIVE DEBUG] Applying highlight_source_with_termbase to row {row}")
33443
+ print(f"[PROACTIVE DEBUG] Source text: {segment.source[:80]}...")
33444
+ print(f"[PROACTIVE DEBUG] Matches keys: {list(termbase_matches.keys())[:5]}")
33445
+ if termbase_matches:
33446
+ first_key = list(termbase_matches.keys())[0]
33447
+ print(f"[PROACTIVE DEBUG] Sample match: {first_key} => {termbase_matches[first_key]}")
33448
+
33449
+ # Check if the source widget exists and is the right type
33450
+ source_widget = self.table.cellWidget(row, 2)
33451
+ print(f"[PROACTIVE DEBUG] Source widget type: {type(source_widget).__name__ if source_widget else 'None'}")
33452
+ print(f"[PROACTIVE DEBUG] Has highlight method: {hasattr(source_widget, 'highlight_termbase_matches') if source_widget else 'N/A'}")
33453
+
33454
+ # Apply highlighting (this updates the source cell widget)
33455
+ self.highlight_source_with_termbase(row, segment.source, termbase_matches)
33456
+ print(f"[PROACTIVE DEBUG] ✅ Highlighting applied successfully")
33457
+
33458
+ except Exception as e:
33459
+ print(f"[PROACTIVE DEBUG] ERROR: {e}")
33460
+ import traceback
33461
+ print(f"[PROACTIVE DEBUG] Traceback: {traceback.format_exc()}")
33462
+
32882
33463
  def insert_term_translation(self, row: int, translation: str):
32883
33464
  """
32884
33465
  Double-click handler: insert termbase translation into target cell
@@ -33920,6 +34501,18 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
33920
34501
  self.log(f" ⚠️ Panel update error: {e}")
33921
34502
  self.log(" ✓ Translation Results panel updated")
33922
34503
 
34504
+ # 7b. Update Match Panel with TM matches
34505
+ tm_matches_for_panel = []
34506
+ for tm_match in tm_matches:
34507
+ tm_matches_for_panel.append({
34508
+ 'source': tm_match.get('source', ''),
34509
+ 'target': tm_match.get('target', ''),
34510
+ 'tm_name': tm_match.get('tm_name', 'TM'),
34511
+ 'match_pct': int(tm_match.get('similarity', 0) * 100)
34512
+ })
34513
+ self.set_compare_panel_matches(segment_id, segment.source, tm_matches_for_panel, [])
34514
+ self.log(" ✓ Match Panel updated")
34515
+
33923
34516
  # 8. Re-apply termbase highlighting in the grid source cell
33924
34517
  try:
33925
34518
  source_widget = self.table.cellWidget(current_row, 2)
@@ -36261,6 +36854,10 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36261
36854
  current_row = self.table.currentRow()
36262
36855
  if current_row < self.table.rowCount() - 1:
36263
36856
  next_row = current_row + 1
36857
+
36858
+ # ⚡ INSTANT NAVIGATION
36859
+ self._ctrl_enter_navigation = True
36860
+
36264
36861
  self.table.setCurrentCell(next_row, 3) # Column 3 = Target (widget column)
36265
36862
  # Get the target cell widget and set focus to it
36266
36863
  target_widget = self.table.cellWidget(next_row, 3)
@@ -36427,7 +37024,6 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36427
37024
  """Return active shortcut context for match shortcuts.
36428
37025
 
36429
37026
  Returns:
36430
- 'compare' if Compare Panel tab is active,
36431
37027
  'results' if Translation Results tab is active,
36432
37028
  '' otherwise.
36433
37029
  """
@@ -36437,8 +37033,6 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36437
37033
  except Exception:
36438
37034
  current = None
36439
37035
  if current is not None:
36440
- if hasattr(self, 'compare_panel') and self.compare_panel and current is self.compare_panel:
36441
- return 'compare'
36442
37036
  if hasattr(self, 'translation_results_panel') and self.translation_results_panel and current is self.translation_results_panel:
36443
37037
  return 'results'
36444
37038
  return ''
@@ -36979,6 +37573,10 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36979
37573
  self._update_pagination_ui()
36980
37574
  self._apply_pagination_to_grid()
36981
37575
 
37576
+ # ⚡ INSTANT NAVIGATION: Set flag so on_cell_selected uses fast path
37577
+ # This makes Ctrl+Enter feel instant - cursor moves first, lookups happen after
37578
+ self._ctrl_enter_navigation = True
37579
+
36982
37580
  self.table.clearSelection()
36983
37581
  self.table.setCurrentCell(row, 3) # Column 3 = Target (widget column)
36984
37582
  self.log(f"⏭️ Moved to next unconfirmed segment {seg.id}")
@@ -37043,6 +37641,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37043
37641
  self._update_pagination_ui()
37044
37642
  self._apply_pagination_to_grid()
37045
37643
 
37644
+ # ⚡ INSTANT NAVIGATION
37645
+ self._ctrl_enter_navigation = True
37646
+
37046
37647
  self.table.clearSelection()
37047
37648
  self.table.setCurrentCell(next_row, 3)
37048
37649
  self.log(f"⏭️ Auto-skipped to next unconfirmed segment {next_seg.id}")
@@ -37083,6 +37684,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37083
37684
  self._update_pagination_ui()
37084
37685
  self._apply_pagination_to_grid()
37085
37686
 
37687
+ # ⚡ INSTANT NAVIGATION
37688
+ self._ctrl_enter_navigation = True
37689
+
37086
37690
  self.table.clearSelection()
37087
37691
  self.table.setCurrentCell(next_row, 3) # Column 3 = Target (widget column)
37088
37692
  self.log(f"⏭️ Moved to next segment (all remaining confirmed)")
@@ -37151,9 +37755,6 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37151
37755
  return None
37152
37756
 
37153
37757
  self.log(f"🔍 Ctrl+Enter: Row {current_row}, Segment ID {segment.id}")
37154
- self.log(f"🔍 Source: '{segment.source[:50]}...'")
37155
- self.log(f"🔍 Target before: '{segment.target[:50] if segment.target else '<empty>'}...'")
37156
- self.log(f"🔍 Segment object ID: {id(segment)}")
37157
37758
 
37158
37759
  old_target = segment.target
37159
37760
  old_status = segment.status
@@ -37162,22 +37763,15 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37162
37763
  if target_widget:
37163
37764
  current_text = target_widget.toPlainText().strip()
37164
37765
  segment.target = current_text
37165
- self.log(f"🔍 Target from widget: '{current_text[:50]}...'")
37166
- self.log(f"🔍 After assignment: segment.target = '{segment.target[:50] if segment.target else '<empty>'}...'")
37167
37766
 
37168
37767
  segment.status = 'confirmed'
37169
37768
 
37170
37769
  # Record undo state for Ctrl+Enter confirmation
37171
37770
  self.record_undo_state(segment_id, old_target, segment.target, old_status, 'confirmed')
37172
- self.log(f"🔍 After status assignment: segment.status = '{segment.status}', segment.target = '{segment.target[:50] if segment.target else '<empty>'}...')")
37173
37771
  self.update_status_icon(current_row, 'confirmed')
37174
37772
  self.project_modified = True
37175
37773
  self.log(f"✅ Segment {segment.id} confirmed")
37176
37774
 
37177
- verification_seg = next((s for s in self.current_project.segments if s.id == segment.id), None)
37178
- if verification_seg:
37179
- self.log(f"✅ VERIFICATION: Segment {segment_id} - target still correct: '{verification_seg.target[:30] if verification_seg.target else 'EMPTY'}', object ID={id(verification_seg)}")
37180
-
37181
37775
  self.update_progress_stats()
37182
37776
 
37183
37777
  # Play sound effect for segment confirmation
@@ -42173,9 +42767,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42173
42767
  self._compare_panel_tm_matches = [] # Reset TM matches list for navigation
42174
42768
  self._compare_panel_mt_matches = [] # Reset MT matches list for navigation
42175
42769
 
42176
- # Update Compare Panel with current source immediately (before TM/MT lookup)
42177
- if hasattr(self, 'compare_panel_current_source'):
42178
- self.set_compare_panel_matches(segment.id, segment.source, [], [])
42770
+ # Update Match Panel with current source immediately (before TM/MT lookup)
42771
+ self.set_compare_panel_matches(segment.id, segment.source, [], [])
42179
42772
 
42180
42773
  # Prepare matches dict - use the stored termbase matches from immediate display
42181
42774
  matches_dict = {
@@ -42189,9 +42782,6 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42189
42782
  # 🔥 DELAYED TM SEARCH: Search TM database using new TMDatabase with activated TMs
42190
42783
  if self.enable_tm_matching and hasattr(self, 'tm_database') and self.tm_database:
42191
42784
  try:
42192
- self.log(f"🚀 DELAYED TM SEARCH: Searching for '{segment.source[:50]}...'")
42193
- self.log(f"🚀 DELAYED TM SEARCH: Project languages: {source_lang_code} → {target_lang_code}")
42194
-
42195
42785
  # Get activated TM IDs for current project
42196
42786
  tm_ids = None
42197
42787
  if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr and self.current_project:
@@ -42199,18 +42789,16 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42199
42789
  if project_id:
42200
42790
  tm_ids = self.tm_metadata_mgr.get_active_tm_ids(project_id)
42201
42791
 
42202
- self.log(f"🚀 DELAYED TM SEARCH: Using TM IDs: {tm_ids}")
42203
-
42204
42792
  # Skip TM search if no TMs are activated
42205
42793
  if tm_ids is not None and isinstance(tm_ids, list) and len(tm_ids) == 0:
42206
- self.log(f"🚀 DELAYED TM SEARCH: Skipping (no TMs activated)")
42207
42794
  all_tm_matches = []
42208
42795
  else:
42209
42796
  # Search using TMDatabase (includes bidirectional + base language matching)
42210
- # Pass enabled_only=False to bypass the hardcoded tm_metadata filter
42211
42797
  all_tm_matches = self.tm_database.search_all(segment.source, tm_ids=tm_ids, enabled_only=False, max_matches=10)
42212
42798
 
42213
- self.log(f"🚀 DELAYED TM SEARCH: Found {len(all_tm_matches)} matches")
42799
+ # Single consolidated log message for TM search results
42800
+ if all_tm_matches:
42801
+ self.log(f"🔍 TM: Found {len(all_tm_matches)} matches for segment {segment.id}")
42214
42802
 
42215
42803
  for match in all_tm_matches:
42216
42804
  match_obj = TranslationMatch(
@@ -42239,27 +42827,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
42239
42827
  except Exception as e:
42240
42828
  self.log(f"Error adding TM matches: {e}")
42241
42829
 
42242
- # 🔄 Update Compare Panel with all TM matches (for navigation)
42243
- if hasattr(self, 'compare_panel_current_source'):
42244
- # Convert TranslationMatch objects to dict format for Compare Panel
42245
- tm_matches_for_panel = []
42246
- for tm in matches_dict["TM"]:
42247
- tm_name = tm.metadata.get('tm_name', 'TM') if tm.metadata else 'TM'
42248
- tm_matches_for_panel.append({
42249
- 'source': tm.compare_source or tm.source,
42250
- 'target': tm.target,
42251
- 'tm_name': tm_name,
42252
- 'match_pct': int(tm.relevance)
42253
- })
42254
- # Store for later (MT will be added when available)
42255
- self._compare_panel_tm_matches = tm_matches_for_panel
42256
- # Update panel with TM data only (MT empty for now)
42257
- self.set_compare_panel_matches(
42258
- segment.id,
42259
- segment.source,
42260
- tm_matches=tm_matches_for_panel,
42261
- mt_matches=getattr(self, '_compare_panel_mt_matches', [])
42262
- )
42830
+ # 🔄 Update Match Panel with all TM matches (for navigation)
42831
+ # Convert TranslationMatch objects to dict format for Match Panel
42832
+ tm_matches_for_panel = []
42833
+ for tm in matches_dict["TM"]:
42834
+ tm_name = tm.metadata.get('tm_name', 'TM') if tm.metadata else 'TM'
42835
+ tm_matches_for_panel.append({
42836
+ 'source': tm.compare_source or tm.source,
42837
+ 'target': tm.target,
42838
+ 'tm_name': tm_name,
42839
+ 'match_pct': int(tm.relevance)
42840
+ })
42841
+ # Store for later (MT will be added when available)
42842
+ self._compare_panel_tm_matches = tm_matches_for_panel
42843
+ # Update panel with TM data only (MT empty for now)
42844
+ self.set_compare_panel_matches(
42845
+ segment.id,
42846
+ segment.source,
42847
+ tm_matches=tm_matches_for_panel,
42848
+ mt_matches=getattr(self, '_compare_panel_mt_matches', [])
42849
+ )
42263
42850
 
42264
42851
  # 🎯 AUTO-INSERT 100% TM MATCH (if enabled in settings)
42265
42852
  if self.auto_insert_100_percent_matches:
@@ -47086,7 +47673,12 @@ class AutoFingersWidget(QWidget):
47086
47673
 
47087
47674
  def setup_shortcuts(self):
47088
47675
  """Setup GLOBAL keyboard shortcuts for AutoFingers actions"""
47089
- import keyboard
47676
+ # keyboard module is Windows-only
47677
+ try:
47678
+ import keyboard
47679
+ except ImportError:
47680
+ self.log("ℹ️ Global hotkeys not available on this platform (Windows only)")
47681
+ return
47090
47682
 
47091
47683
  try:
47092
47684
  # Store hotkey references for later removal
@@ -47145,8 +47737,13 @@ class AutoFingersWidget(QWidget):
47145
47737
  def cleanup_hotkeys(self):
47146
47738
  """Cleanup AutoFingers hotkeys when widget is closed/hidden"""
47147
47739
  # Unregister ONLY AutoFingers hotkeys
47740
+ # keyboard module is Windows-only
47148
47741
  try:
47149
47742
  import keyboard
47743
+ except ImportError:
47744
+ return # Not available on this platform
47745
+
47746
+ try:
47150
47747
  if hasattr(self, 'hotkeys'):
47151
47748
  for hotkey in self.hotkeys:
47152
47749
  try: