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 +1081 -484
- modules/database_manager.py +70 -37
- modules/superlookup.py +12 -8
- modules/tag_manager.py +20 -2
- modules/termview_widget.py +14 -10
- modules/translation_memory.py +2 -2
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.164.dist-info}/METADATA +20 -10
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.164.dist-info}/RECORD +12 -12
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.164.dist-info}/WHEEL +1 -1
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.164.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.164.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.164.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -34,9 +34,9 @@ License: MIT
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
37
|
+
__version__ = "1.9.164"
|
|
38
38
|
__phase__ = "0.9"
|
|
39
|
-
__release_date__ = "2026-01-
|
|
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
|
-
|
|
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
|
-
#
|
|
7392
|
-
|
|
7425
|
+
# Match Panel zoom section
|
|
7426
|
+
match_panel_zoom_menu = view_menu.addMenu("🔍 &Match Panel")
|
|
7393
7427
|
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
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
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
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
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
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
|
|
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
|
|
11521
|
-
if hasattr(self, '
|
|
11559
|
+
# Update Match Panel Termview
|
|
11560
|
+
if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
|
|
11522
11561
|
try:
|
|
11523
|
-
self.
|
|
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
|
|
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
|
-
|
|
19117
|
-
|
|
19118
|
-
|
|
19119
|
-
|
|
19120
|
-
|
|
19121
|
-
|
|
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:
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
19836
|
-
|
|
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
|
|
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,
|
|
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/
|
|
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
|
|
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
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
21763
|
-
|
|
21764
|
-
|
|
21765
|
-
|
|
21766
|
-
|
|
21767
|
-
|
|
21768
|
-
|
|
21769
|
-
|
|
21770
|
-
|
|
21771
|
-
|
|
21772
|
-
|
|
21773
|
-
|
|
21774
|
-
|
|
21775
|
-
|
|
21776
|
-
|
|
21777
|
-
|
|
21778
|
-
|
|
21779
|
-
|
|
21780
|
-
|
|
21781
|
-
|
|
21782
|
-
|
|
21783
|
-
|
|
21784
|
-
|
|
21785
|
-
|
|
21786
|
-
|
|
21787
|
-
|
|
21788
|
-
|
|
21789
|
-
|
|
21790
|
-
|
|
21791
|
-
|
|
21792
|
-
|
|
21793
|
-
|
|
21794
|
-
|
|
21795
|
-
|
|
21796
|
-
|
|
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
|
|
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
|
-
|
|
28368
|
-
|
|
28369
|
-
|
|
28370
|
-
#
|
|
28371
|
-
|
|
28372
|
-
|
|
28373
|
-
|
|
28374
|
-
|
|
28375
|
-
|
|
28376
|
-
|
|
28377
|
-
|
|
28378
|
-
|
|
28379
|
-
|
|
28380
|
-
|
|
28381
|
-
|
|
28382
|
-
|
|
28383
|
-
|
|
28384
|
-
|
|
28385
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
28495
|
-
|
|
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
|
-
|
|
28498
|
-
|
|
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
|
|
28631
|
-
if hasattr(self, '
|
|
28632
|
-
self.
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
28781
|
-
|
|
28782
|
-
|
|
28783
|
-
|
|
28784
|
-
|
|
28785
|
-
|
|
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
|
-
#
|
|
29765
|
+
# MATCH PANEL ZOOM METHODS
|
|
29304
29766
|
# =========================================================================
|
|
29305
29767
|
|
|
29306
|
-
# Class variable for
|
|
29307
|
-
|
|
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
|
|
29310
|
-
"""Increase font size in
|
|
29311
|
-
SupervertalerQt.
|
|
29312
|
-
self.
|
|
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"
|
|
29776
|
+
self.log(f"Match Panel font size: {SupervertalerQt.match_panel_font_size}pt")
|
|
29315
29777
|
|
|
29316
|
-
def
|
|
29317
|
-
"""Decrease font size in
|
|
29318
|
-
SupervertalerQt.
|
|
29319
|
-
self.
|
|
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"
|
|
29783
|
+
self.log(f"Match Panel font size: {SupervertalerQt.match_panel_font_size}pt")
|
|
29322
29784
|
|
|
29323
|
-
def
|
|
29324
|
-
"""Reset font size in
|
|
29325
|
-
SupervertalerQt.
|
|
29326
|
-
self.
|
|
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("
|
|
29790
|
+
self.log("Match Panel font size reset to 10pt")
|
|
29329
29791
|
|
|
29330
|
-
def
|
|
29331
|
-
"""Apply current font size to
|
|
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.
|
|
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
|
|
29365
|
-
general_settings['
|
|
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
|
|
29710
|
-
|
|
29711
|
-
if 7 <=
|
|
29712
|
-
SupervertalerQt.
|
|
29713
|
-
self.
|
|
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
|
|
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
|
-
|
|
29851
|
-
|
|
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
|
-
#
|
|
30032
|
-
|
|
30033
|
-
|
|
30034
|
-
|
|
30035
|
-
|
|
30036
|
-
|
|
30037
|
-
|
|
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
|
-
#
|
|
30082
|
-
|
|
30083
|
-
|
|
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
|
-
|
|
30086
|
-
|
|
30087
|
-
|
|
30088
|
-
|
|
30089
|
-
|
|
30090
|
-
|
|
30091
|
-
|
|
30092
|
-
|
|
30093
|
-
|
|
30094
|
-
|
|
30095
|
-
|
|
30096
|
-
|
|
30097
|
-
|
|
30098
|
-
|
|
30099
|
-
|
|
30100
|
-
|
|
30101
|
-
|
|
30102
|
-
|
|
30103
|
-
|
|
30104
|
-
|
|
30105
|
-
|
|
30106
|
-
|
|
30107
|
-
|
|
30108
|
-
|
|
30109
|
-
|
|
30110
|
-
|
|
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
|
-
#
|
|
30113
|
-
|
|
30114
|
-
|
|
30115
|
-
|
|
30116
|
-
|
|
30117
|
-
|
|
30118
|
-
|
|
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
|
-
|
|
30121
|
-
|
|
30122
|
-
|
|
30123
|
-
|
|
30124
|
-
|
|
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
|
-
#
|
|
30612
|
+
# 🧪 EXPERIMENTAL: Skip termbase cache if cache kill switch is enabled
|
|
30137
30613
|
cache_checked = False
|
|
30138
|
-
|
|
30139
|
-
|
|
30140
|
-
|
|
30141
|
-
|
|
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
|
-
|
|
30148
|
-
|
|
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
|
-
#
|
|
30842
|
+
# Check if already cached
|
|
30359
30843
|
with self.translation_matches_cache_lock:
|
|
30360
|
-
|
|
30361
|
-
|
|
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
|
|
42177
|
-
|
|
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
|
-
|
|
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
|
|
42243
|
-
|
|
42244
|
-
|
|
42245
|
-
|
|
42246
|
-
|
|
42247
|
-
|
|
42248
|
-
|
|
42249
|
-
|
|
42250
|
-
|
|
42251
|
-
|
|
42252
|
-
|
|
42253
|
-
|
|
42254
|
-
|
|
42255
|
-
|
|
42256
|
-
|
|
42257
|
-
|
|
42258
|
-
|
|
42259
|
-
|
|
42260
|
-
|
|
42261
|
-
|
|
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
|
-
|
|
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:
|