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