supervertaler 1.9.114__py3-none-any.whl → 1.9.128__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Supervertaler.py +333 -99
- modules/shortcut_manager.py +14 -1
- modules/termview_widget.py +9 -2
- modules/unified_prompt_library.py +12 -9
- modules/unified_prompt_manager_qt.py +345 -112
- {supervertaler-1.9.114.dist-info → supervertaler-1.9.128.dist-info}/METADATA +43 -8
- {supervertaler-1.9.114.dist-info → supervertaler-1.9.128.dist-info}/RECORD +11 -11
- {supervertaler-1.9.114.dist-info → supervertaler-1.9.128.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.114.dist-info → supervertaler-1.9.128.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.114.dist-info → supervertaler-1.9.128.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.114.dist-info → supervertaler-1.9.128.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -3,7 +3,7 @@ Supervertaler
|
|
|
3
3
|
=============
|
|
4
4
|
The Ultimate Translation Workbench.
|
|
5
5
|
Modern PyQt6 interface with specialised modules to handle any problem.
|
|
6
|
-
Version: 1.9.
|
|
6
|
+
Version: 1.9.123 (QuickMenu now supports generic AI tasks)
|
|
7
7
|
Release Date: January 19, 2026
|
|
8
8
|
Framework: PyQt6
|
|
9
9
|
|
|
@@ -34,9 +34,9 @@ License: MIT
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
37
|
+
__version__ = "1.9.128"
|
|
38
38
|
__phase__ = "0.9"
|
|
39
|
-
__release_date__ = "2026-01-
|
|
39
|
+
__release_date__ = "2026-01-19"
|
|
40
40
|
__edition__ = "Qt"
|
|
41
41
|
|
|
42
42
|
import sys
|
|
@@ -2559,7 +2559,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
2559
2559
|
menu.addSeparator()
|
|
2560
2560
|
|
|
2561
2561
|
# Add to dictionary action
|
|
2562
|
-
add_to_dict_action = QAction(f"📖 Add '{misspelled_word}' to Dictionary", self)
|
|
2562
|
+
add_to_dict_action = QAction(f"📖 Add '{misspelled_word}' to Dictionary (Alt+D)", self)
|
|
2563
2563
|
add_to_dict_action.triggered.connect(
|
|
2564
2564
|
lambda checked, w=misspelled_word: self._add_to_dictionary(w)
|
|
2565
2565
|
)
|
|
@@ -5322,7 +5322,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
5322
5322
|
dialog.exec()
|
|
5323
5323
|
|
|
5324
5324
|
# Navigate to Settings → Features tab
|
|
5325
|
-
self.main_tabs.setCurrentIndex(
|
|
5325
|
+
self.main_tabs.setCurrentIndex(4) # Settings tab
|
|
5326
5326
|
if hasattr(self, 'settings_tabs'):
|
|
5327
5327
|
# Find the Features tab index
|
|
5328
5328
|
for i in range(self.settings_tabs.count()):
|
|
@@ -5668,6 +5668,24 @@ class SupervertalerQt(QMainWindow):
|
|
|
5668
5668
|
|
|
5669
5669
|
# Ctrl+Shift+2 - Quick add term with Priority 2
|
|
5670
5670
|
create_shortcut("editor_quick_add_priority_2", "Ctrl+Shift+2", lambda: self._quick_add_term_with_priority(2))
|
|
5671
|
+
|
|
5672
|
+
# Alt+D - Add word at cursor to dictionary
|
|
5673
|
+
create_shortcut("editor_add_to_dictionary", "Alt+D", self.add_word_to_dictionary_shortcut)
|
|
5674
|
+
|
|
5675
|
+
# Ctrl+N - Focus Segment Note tab
|
|
5676
|
+
create_shortcut("editor_focus_notes", "Ctrl+N", self.focus_segment_notes)
|
|
5677
|
+
|
|
5678
|
+
def focus_segment_notes(self):
|
|
5679
|
+
"""Switch to Segment Note tab and focus the notes editor so user can start typing immediately"""
|
|
5680
|
+
if not hasattr(self, 'bottom_tabs'):
|
|
5681
|
+
return
|
|
5682
|
+
|
|
5683
|
+
# Switch to Segment note tab (index 1)
|
|
5684
|
+
self.bottom_tabs.setCurrentIndex(1)
|
|
5685
|
+
|
|
5686
|
+
# Focus the notes editor so user can start typing
|
|
5687
|
+
if hasattr(self, 'bottom_notes_edit'):
|
|
5688
|
+
self.bottom_notes_edit.setFocus()
|
|
5671
5689
|
|
|
5672
5690
|
def refresh_shortcut_enabled_states(self):
|
|
5673
5691
|
"""Refresh enabled/disabled states and key bindings of all global shortcuts from shortcut manager.
|
|
@@ -6307,12 +6325,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
6307
6325
|
go_resources_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(1) if hasattr(self, 'main_tabs') else None)
|
|
6308
6326
|
nav_menu.addAction(go_resources_action)
|
|
6309
6327
|
|
|
6328
|
+
go_prompt_manager_action = QAction("🤖 &Prompt Manager", self)
|
|
6329
|
+
go_prompt_manager_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(2) if hasattr(self, 'main_tabs') else None)
|
|
6330
|
+
nav_menu.addAction(go_prompt_manager_action)
|
|
6331
|
+
|
|
6310
6332
|
go_tools_action = QAction("🛠️ &Tools", self)
|
|
6311
|
-
go_tools_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(
|
|
6333
|
+
go_tools_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(3) if hasattr(self, 'main_tabs') else None)
|
|
6312
6334
|
nav_menu.addAction(go_tools_action)
|
|
6313
6335
|
|
|
6314
6336
|
go_settings_action = QAction("⚙️ &Settings", self)
|
|
6315
|
-
go_settings_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(
|
|
6337
|
+
go_settings_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(4) if hasattr(self, 'main_tabs') else None)
|
|
6316
6338
|
nav_menu.addAction(go_settings_action)
|
|
6317
6339
|
|
|
6318
6340
|
view_menu.addSeparator()
|
|
@@ -6800,6 +6822,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
6800
6822
|
settings_tab = self.create_settings_tab()
|
|
6801
6823
|
self.main_tabs.addTab(settings_tab, "⚙️ Settings")
|
|
6802
6824
|
|
|
6825
|
+
# Set startup tab to Grid (index 0)
|
|
6826
|
+
self.main_tabs.setCurrentIndex(0)
|
|
6827
|
+
|
|
6803
6828
|
main_layout.addWidget(self.main_tabs)
|
|
6804
6829
|
|
|
6805
6830
|
# Connect tab changes to handle view refreshes
|
|
@@ -10829,6 +10854,38 @@ class SupervertalerQt(QMainWindow):
|
|
|
10829
10854
|
self._play_sound_effect('glossary_term_error')
|
|
10830
10855
|
self.statusBar().showMessage(f"Error adding term: {e}", 3000)
|
|
10831
10856
|
|
|
10857
|
+
def add_word_to_dictionary_shortcut(self):
|
|
10858
|
+
"""Add word at cursor position to custom dictionary (Alt+D shortcut)
|
|
10859
|
+
|
|
10860
|
+
Finds the misspelled word at the current cursor position and adds it to the
|
|
10861
|
+
custom dictionary. Works when focus is in a grid target cell.
|
|
10862
|
+
"""
|
|
10863
|
+
# Get currently focused widget
|
|
10864
|
+
focused_widget = QApplication.focusWidget()
|
|
10865
|
+
|
|
10866
|
+
# Check if we're in an editable grid cell
|
|
10867
|
+
if not isinstance(focused_widget, EditableGridTextEditor):
|
|
10868
|
+
self.statusBar().showMessage("Alt+D: Place cursor on a misspelled word in the target cell first", 3000)
|
|
10869
|
+
return
|
|
10870
|
+
|
|
10871
|
+
# Get cursor position
|
|
10872
|
+
cursor = focused_widget.textCursor()
|
|
10873
|
+
|
|
10874
|
+
# Try to find misspelled word at cursor
|
|
10875
|
+
word_info = focused_widget._get_misspelled_word_at_cursor(cursor)
|
|
10876
|
+
|
|
10877
|
+
if word_info[0] is None:
|
|
10878
|
+
# No misspelled word found at cursor
|
|
10879
|
+
self.statusBar().showMessage("No misspelled word at cursor position", 3000)
|
|
10880
|
+
return
|
|
10881
|
+
|
|
10882
|
+
word, start_pos, end_pos = word_info
|
|
10883
|
+
|
|
10884
|
+
# Add to dictionary
|
|
10885
|
+
focused_widget._add_to_dictionary(word)
|
|
10886
|
+
|
|
10887
|
+
# Status message already shown by _add_to_dictionary
|
|
10888
|
+
|
|
10832
10889
|
def add_text_to_non_translatables(self, text: str):
|
|
10833
10890
|
"""Add selected text to active non-translatable list(s)"""
|
|
10834
10891
|
if not text or not text.strip():
|
|
@@ -13427,7 +13484,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
13427
13484
|
|
|
13428
13485
|
# ===== TAB 2: AI Settings (LLM, Ollama) =====
|
|
13429
13486
|
ai_tab = self._create_ai_settings_tab()
|
|
13430
|
-
|
|
13487
|
+
ai_scroll = scroll_area_wrapper(ai_tab)
|
|
13488
|
+
settings_tabs.addTab(ai_scroll, "🤖 AI Settings")
|
|
13489
|
+
self.ai_settings_scroll = ai_scroll # Store reference for scrolling to API keys
|
|
13431
13490
|
|
|
13432
13491
|
# ===== TAB 3: Language Pair Settings =====
|
|
13433
13492
|
lang_tab = self._create_language_pair_tab()
|
|
@@ -13947,6 +14006,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
13947
14006
|
full_context_cb.stateChanged.connect(lambda: context_slider.setEnabled(full_context_cb.isChecked()))
|
|
13948
14007
|
update_context_label(context_window_size)
|
|
13949
14008
|
|
|
14009
|
+
prefs_layout.addSpacing(10)
|
|
14010
|
+
|
|
14011
|
+
# QuickMenu document context
|
|
14012
|
+
quickmenu_context_label = QLabel("<b>QuickMenu Document Context:</b>")
|
|
14013
|
+
prefs_layout.addWidget(quickmenu_context_label)
|
|
14014
|
+
|
|
14015
|
+
quickmenu_context_percent = general_prefs.get('quickmenu_context_percent', 50)
|
|
14016
|
+
quickmenu_context_layout = QHBoxLayout()
|
|
14017
|
+
quickmenu_context_layout.addWidget(QLabel(" Document context size:"))
|
|
14018
|
+
quickmenu_context_slider = QSlider(Qt.Orientation.Horizontal)
|
|
14019
|
+
quickmenu_context_slider.setMinimum(0)
|
|
14020
|
+
quickmenu_context_slider.setMaximum(100)
|
|
14021
|
+
quickmenu_context_slider.setValue(quickmenu_context_percent)
|
|
14022
|
+
quickmenu_context_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
14023
|
+
quickmenu_context_slider.setTickInterval(10)
|
|
14024
|
+
quickmenu_context_layout.addWidget(quickmenu_context_slider)
|
|
14025
|
+
quickmenu_context_value_label = QLabel(f"{quickmenu_context_percent}%")
|
|
14026
|
+
quickmenu_context_value_label.setMinimumWidth(60)
|
|
14027
|
+
quickmenu_context_layout.addWidget(quickmenu_context_value_label)
|
|
14028
|
+
quickmenu_context_layout.addStretch()
|
|
14029
|
+
prefs_layout.addLayout(quickmenu_context_layout)
|
|
14030
|
+
|
|
14031
|
+
quickmenu_context_info = QLabel(
|
|
14032
|
+
" ⓘ When using {{DOCUMENT_CONTEXT}} placeholder in QuickMenu prompts.\n"
|
|
14033
|
+
" 0% = disabled, 50% = half the document (default), 100% = entire document.\n"
|
|
14034
|
+
" Limit: maximum 100 segments for performance."
|
|
14035
|
+
)
|
|
14036
|
+
quickmenu_context_info.setStyleSheet("font-size: 9pt; color: #666; padding-left: 20px;")
|
|
14037
|
+
quickmenu_context_info.setWordWrap(True)
|
|
14038
|
+
prefs_layout.addWidget(quickmenu_context_info)
|
|
14039
|
+
|
|
14040
|
+
def update_quickmenu_context_label(value):
|
|
14041
|
+
if value == 0:
|
|
14042
|
+
quickmenu_context_value_label.setText("0% (disabled)")
|
|
14043
|
+
else:
|
|
14044
|
+
quickmenu_context_value_label.setText(f"{value}%")
|
|
14045
|
+
|
|
14046
|
+
quickmenu_context_slider.valueChanged.connect(update_quickmenu_context_label)
|
|
14047
|
+
|
|
13950
14048
|
prefs_layout.addSpacing(5)
|
|
13951
14049
|
|
|
13952
14050
|
# Check TM before API call
|
|
@@ -14091,7 +14189,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
14091
14189
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
14092
14190
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
14093
14191
|
ollama_keepwarm_cb,
|
|
14094
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
14192
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
14193
|
+
quickmenu_context_slider
|
|
14095
14194
|
))
|
|
14096
14195
|
layout.addWidget(save_btn)
|
|
14097
14196
|
|
|
@@ -17089,7 +17188,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
17089
17188
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
17090
17189
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
17091
17190
|
ollama_keepwarm_cb,
|
|
17092
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
17191
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
17192
|
+
quickmenu_context_slider):
|
|
17093
17193
|
"""Save all AI settings from the unified AI Settings tab"""
|
|
17094
17194
|
# Determine selected provider
|
|
17095
17195
|
if openai_radio.isChecked():
|
|
@@ -17148,6 +17248,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
17148
17248
|
general_prefs['surrounding_segments'] = surrounding_spin.value()
|
|
17149
17249
|
general_prefs['use_full_context'] = full_context_cb.isChecked()
|
|
17150
17250
|
general_prefs['context_window_size'] = context_slider.value()
|
|
17251
|
+
general_prefs['quickmenu_context_percent'] = quickmenu_context_slider.value()
|
|
17151
17252
|
general_prefs['check_tm_before_api'] = check_tm_cb.isChecked()
|
|
17152
17253
|
general_prefs['auto_propagate_100'] = auto_propagate_cb.isChecked()
|
|
17153
17254
|
general_prefs['lookup_delay'] = delay_spin.value()
|
|
@@ -31033,11 +31134,18 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31033
31134
|
normalized_source = clean_source_lower
|
|
31034
31135
|
for quote_char in '\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A':
|
|
31035
31136
|
normalized_source = normalized_source.replace(quote_char, ' ')
|
|
31137
|
+
|
|
31138
|
+
# CRITICAL FIX: Strip trailing punctuation from glossary term before matching
|
|
31139
|
+
# This allows entries like "De huidige uitvinding...problemen." (with period)
|
|
31140
|
+
# to match source text "...problemen." where tokenization strips the period
|
|
31141
|
+
# Strip from both ends to handle quotes and trailing punctuation
|
|
31142
|
+
normalized_term = source_term.lower().rstrip(PUNCT_CHARS).lstrip(PUNCT_CHARS)
|
|
31143
|
+
|
|
31036
31144
|
# Check if term has punctuation - use different pattern
|
|
31037
|
-
if any(char in
|
|
31038
|
-
pattern = re.compile(r'(?<!\w)' + re.escape(
|
|
31145
|
+
if any(char in normalized_term for char in ['.', '%', ',', '-', '/']):
|
|
31146
|
+
pattern = re.compile(r'(?<!\w)' + re.escape(normalized_term) + r'(?!\w)')
|
|
31039
31147
|
else:
|
|
31040
|
-
pattern = re.compile(r"\b" + re.escape(
|
|
31148
|
+
pattern = re.compile(r"\b" + re.escape(normalized_term) + r"\b")
|
|
31041
31149
|
|
|
31042
31150
|
# Try matching on normalized (tag-stripped, quote-stripped) text first,
|
|
31043
31151
|
# then tag-stripped, then original with tags
|
|
@@ -31494,7 +31602,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31494
31602
|
self.case_sensitive_cb.setChecked(op.case_sensitive)
|
|
31495
31603
|
|
|
31496
31604
|
def _fr_run_set_batch(self, fr_set: FindReplaceSet):
|
|
31497
|
-
"""Run all enabled operations in a F&R Set as a batch."""
|
|
31605
|
+
"""Run all enabled operations in a F&R Set as a batch (optimized for speed)."""
|
|
31498
31606
|
enabled_ops = [op for op in fr_set.operations if op.enabled and op.find_text]
|
|
31499
31607
|
|
|
31500
31608
|
if not enabled_ops:
|
|
@@ -31513,17 +31621,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31513
31621
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31514
31622
|
return
|
|
31515
31623
|
|
|
31516
|
-
#
|
|
31517
|
-
|
|
31518
|
-
for op in enabled_ops:
|
|
31519
|
-
count = self._execute_single_fr_operation(op)
|
|
31520
|
-
total_replaced += count
|
|
31521
|
-
self.log(f" '{op.find_text}' → '{op.replace_text}': {count} replacement(s)")
|
|
31624
|
+
# OPTIMIZATION: Disable UI updates during batch processing
|
|
31625
|
+
self.table.setUpdatesEnabled(False)
|
|
31522
31626
|
|
|
31523
|
-
|
|
31524
|
-
|
|
31525
|
-
|
|
31526
|
-
|
|
31627
|
+
try:
|
|
31628
|
+
# Run each operation
|
|
31629
|
+
total_replaced = 0
|
|
31630
|
+
for op in enabled_ops:
|
|
31631
|
+
count = self._execute_single_fr_operation(op)
|
|
31632
|
+
total_replaced += count
|
|
31633
|
+
self.log(f" '{op.find_text}' → '{op.replace_text}': {count} replacement(s)")
|
|
31634
|
+
|
|
31635
|
+
self.project_modified = True
|
|
31636
|
+
self.update_window_title()
|
|
31637
|
+
|
|
31638
|
+
finally:
|
|
31639
|
+
# OPTIMIZATION: Re-enable UI updates and refresh only target column cells
|
|
31640
|
+
self.table.setUpdatesEnabled(True)
|
|
31641
|
+
# Update all target cells in-place (batch operations can affect many segments)
|
|
31642
|
+
for row in range(len(self.current_project.segments)):
|
|
31643
|
+
segment = self.current_project.segments[row]
|
|
31644
|
+
target_widget = self.table.cellWidget(row, 3)
|
|
31645
|
+
if target_widget and hasattr(target_widget, 'setPlainText'):
|
|
31646
|
+
target_widget.blockSignals(True)
|
|
31647
|
+
target_widget.setPlainText(segment.target)
|
|
31648
|
+
target_widget.blockSignals(False)
|
|
31649
|
+
self.table.viewport().update()
|
|
31527
31650
|
|
|
31528
31651
|
QMessageBox.information(
|
|
31529
31652
|
self.find_replace_dialog,
|
|
@@ -31532,12 +31655,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31532
31655
|
)
|
|
31533
31656
|
|
|
31534
31657
|
def _execute_single_fr_operation(self, op: FindReplaceOperation) -> int:
|
|
31535
|
-
"""Execute a single F&R operation on all segments. Returns replacement count."""
|
|
31658
|
+
"""Execute a single F&R operation on all segments (optimized). Returns replacement count."""
|
|
31536
31659
|
import re
|
|
31537
31660
|
count = 0
|
|
31538
31661
|
|
|
31662
|
+
# OPTIMIZATION: Pre-filter segments - only check segments that might contain the text
|
|
31663
|
+
# Quick case-insensitive check to skip segments that definitely don't match
|
|
31664
|
+
search_text_lower = op.find_text.lower() if not op.case_sensitive else None
|
|
31665
|
+
|
|
31539
31666
|
for segment in self.current_project.segments:
|
|
31540
31667
|
texts_to_check = []
|
|
31668
|
+
|
|
31669
|
+
# Pre-filter: skip segments that can't possibly match
|
|
31670
|
+
if not op.case_sensitive:
|
|
31671
|
+
# Quick check: does the segment contain the search text at all?
|
|
31672
|
+
skip_segment = True
|
|
31673
|
+
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
31674
|
+
if search_text_lower in segment.source.lower():
|
|
31675
|
+
skip_segment = False
|
|
31676
|
+
if op.search_in in ("target", "both"):
|
|
31677
|
+
if search_text_lower in segment.target.lower():
|
|
31678
|
+
skip_segment = False
|
|
31679
|
+
if skip_segment:
|
|
31680
|
+
continue
|
|
31681
|
+
|
|
31541
31682
|
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
31542
31683
|
texts_to_check.append(("source", segment.source))
|
|
31543
31684
|
if op.search_in in ("target", "both"):
|
|
@@ -31833,7 +31974,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31833
31974
|
self.find_next_match()
|
|
31834
31975
|
|
|
31835
31976
|
def replace_all_matches(self):
|
|
31836
|
-
"""Replace all matches in target segments"""
|
|
31977
|
+
"""Replace all matches in target segments (optimized for speed)"""
|
|
31837
31978
|
find_text = self.find_input.text()
|
|
31838
31979
|
replace_text = self.replace_input.text()
|
|
31839
31980
|
|
|
@@ -31906,52 +32047,67 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31906
32047
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31907
32048
|
return
|
|
31908
32049
|
|
|
31909
|
-
#
|
|
31910
|
-
|
|
31911
|
-
replaced_count = 0
|
|
32050
|
+
# OPTIMIZATION: Disable UI updates during batch replacement
|
|
32051
|
+
self.table.setUpdatesEnabled(False)
|
|
31912
32052
|
|
|
31913
|
-
|
|
31914
|
-
|
|
31915
|
-
|
|
31916
|
-
|
|
31917
|
-
|
|
31918
|
-
old_text = segment.source
|
|
31919
|
-
else: # col == 3, Target
|
|
31920
|
-
old_text = segment.target
|
|
32053
|
+
try:
|
|
32054
|
+
# Perform replacements
|
|
32055
|
+
import re
|
|
32056
|
+
replaced_count = 0
|
|
32057
|
+
updated_rows = set() # Track which rows need UI updates
|
|
31921
32058
|
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
if
|
|
31927
|
-
|
|
31928
|
-
else:
|
|
31929
|
-
|
|
31930
|
-
|
|
31931
|
-
|
|
31932
|
-
|
|
31933
|
-
|
|
31934
|
-
# Update the appropriate field
|
|
31935
|
-
if col == 2:
|
|
31936
|
-
segment.source = new_text
|
|
32059
|
+
for row, col in self.find_matches:
|
|
32060
|
+
segment = self.current_project.segments[row]
|
|
32061
|
+
|
|
32062
|
+
# Get the appropriate field
|
|
32063
|
+
if col == 2: # Source
|
|
32064
|
+
old_text = segment.source
|
|
32065
|
+
else: # col == 3, Target
|
|
32066
|
+
old_text = segment.target
|
|
32067
|
+
|
|
32068
|
+
# Perform replacement
|
|
32069
|
+
if match_mode == 2: # Entire segment
|
|
32070
|
+
new_text = replace_text
|
|
31937
32071
|
else:
|
|
31938
|
-
|
|
31939
|
-
|
|
31940
|
-
|
|
31941
|
-
|
|
31942
|
-
|
|
32072
|
+
if case_sensitive:
|
|
32073
|
+
new_text = old_text.replace(find_text, replace_text)
|
|
32074
|
+
else:
|
|
32075
|
+
pattern = re.escape(find_text)
|
|
32076
|
+
new_text = re.sub(pattern, replace_text, old_text, flags=re.IGNORECASE)
|
|
31943
32077
|
|
|
31944
|
-
|
|
31945
|
-
|
|
31946
|
-
|
|
31947
|
-
|
|
31948
|
-
|
|
31949
|
-
|
|
31950
|
-
|
|
31951
|
-
|
|
31952
|
-
|
|
31953
|
-
|
|
31954
|
-
|
|
32078
|
+
if new_text != old_text:
|
|
32079
|
+
replaced_count += 1
|
|
32080
|
+
updated_rows.add(row)
|
|
32081
|
+
|
|
32082
|
+
# Update the appropriate field
|
|
32083
|
+
if col == 2:
|
|
32084
|
+
segment.source = new_text
|
|
32085
|
+
else:
|
|
32086
|
+
old_target = segment.target
|
|
32087
|
+
old_status = segment.status
|
|
32088
|
+
segment.target = new_text
|
|
32089
|
+
# Record undo state for find/replace operation
|
|
32090
|
+
self.record_undo_state(segment.id, old_target, new_text, old_status, old_status)
|
|
32091
|
+
|
|
32092
|
+
# OPTIMIZATION: Update only the affected cell widget in-place
|
|
32093
|
+
cell_widget = self.table.cellWidget(row, col)
|
|
32094
|
+
if cell_widget and hasattr(cell_widget, 'setPlainText'):
|
|
32095
|
+
cell_widget.blockSignals(True)
|
|
32096
|
+
cell_widget.setPlainText(new_text)
|
|
32097
|
+
cell_widget.blockSignals(False)
|
|
32098
|
+
|
|
32099
|
+
self.project_modified = True
|
|
32100
|
+
self.update_window_title()
|
|
32101
|
+
|
|
32102
|
+
# Clear matches
|
|
32103
|
+
self.find_matches = []
|
|
32104
|
+
|
|
32105
|
+
finally:
|
|
32106
|
+
# OPTIMIZATION: Re-enable UI updates without full grid reload
|
|
32107
|
+
self.table.setUpdatesEnabled(True)
|
|
32108
|
+
# Trigger a repaint of updated rows only
|
|
32109
|
+
for row in updated_rows:
|
|
32110
|
+
self.table.viewport().update()
|
|
31955
32111
|
|
|
31956
32112
|
QMessageBox.information(self.find_replace_dialog, "Replace All", f"Replaced {replaced_count} occurrence(s).")
|
|
31957
32113
|
self.log(f"✓ Replaced {replaced_count} occurrence(s) of '{find_text}'")
|
|
@@ -35941,11 +36097,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35941
36097
|
except Exception as e:
|
|
35942
36098
|
QMessageBox.warning(self, "Error", f"Could not open api_keys.txt: {str(e)}")
|
|
35943
36099
|
|
|
35944
|
-
def _go_to_settings_tab(self):
|
|
35945
|
-
"""Navigate to Settings tab (from menu)
|
|
36100
|
+
def _go_to_settings_tab(self, subtab_name: str = None):
|
|
36101
|
+
"""Navigate to Settings tab (from menu), optionally to a specific sub-tab
|
|
36102
|
+
|
|
36103
|
+
Args:
|
|
36104
|
+
subtab_name: Name of the sub-tab to navigate to (e.g., "AI Settings")
|
|
36105
|
+
"""
|
|
35946
36106
|
if hasattr(self, 'main_tabs'):
|
|
35947
|
-
# Main tabs: Grid=0,
|
|
35948
|
-
self.main_tabs.setCurrentIndex(
|
|
36107
|
+
# Main tabs: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
|
|
36108
|
+
self.main_tabs.setCurrentIndex(4)
|
|
36109
|
+
|
|
36110
|
+
# Navigate to specific sub-tab if requested
|
|
36111
|
+
if subtab_name and hasattr(self, 'settings_tabs'):
|
|
36112
|
+
for i in range(self.settings_tabs.count()):
|
|
36113
|
+
if subtab_name.lower() in self.settings_tabs.tabText(i).lower():
|
|
36114
|
+
self.settings_tabs.setCurrentIndex(i)
|
|
36115
|
+
|
|
36116
|
+
# If navigating to AI Settings, scroll to bottom (API keys section)
|
|
36117
|
+
if "ai settings" in subtab_name.lower() and hasattr(self, 'ai_settings_scroll'):
|
|
36118
|
+
# Use QTimer to ensure the tab is fully rendered before scrolling
|
|
36119
|
+
from PyQt6.QtCore import QTimer
|
|
36120
|
+
QTimer.singleShot(100, lambda: self.ai_settings_scroll.verticalScrollBar().setValue(
|
|
36121
|
+
self.ai_settings_scroll.verticalScrollBar().maximum()
|
|
36122
|
+
))
|
|
36123
|
+
break
|
|
35949
36124
|
|
|
35950
36125
|
def _go_to_superlookup(self):
|
|
35951
36126
|
"""Navigate to Superlookup in Tools tab"""
|
|
@@ -37177,7 +37352,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37177
37352
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
37178
37353
|
)
|
|
37179
37354
|
if reply == QMessageBox.StandardButton.Yes:
|
|
37180
|
-
self._go_to_settings_tab()
|
|
37355
|
+
self._go_to_settings_tab("AI Settings")
|
|
37181
37356
|
return
|
|
37182
37357
|
|
|
37183
37358
|
# Check if API key exists for selected provider
|
|
@@ -37536,7 +37711,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37536
37711
|
return text.strip()
|
|
37537
37712
|
|
|
37538
37713
|
def _quickmenu_build_custom_prompt(self, prompt_relative_path: str, source_text: str, source_lang: str, target_lang: str) -> str:
|
|
37539
|
-
"""Build a
|
|
37714
|
+
"""Build a prompt for QuickMenu using the selected prompt as instructions.
|
|
37715
|
+
|
|
37716
|
+
This is a GENERIC prompt builder (not translation-specific) that allows QuickMenu prompts
|
|
37717
|
+
to do anything: explain, define, search, translate, analyze, etc.
|
|
37718
|
+
|
|
37719
|
+
Supports placeholders:
|
|
37720
|
+
- {{SELECTION}} or {{SOURCE_TEXT}} - The selected text
|
|
37721
|
+
- {{SOURCE_LANGUAGE}} - Source language name
|
|
37722
|
+
- {{TARGET_LANGUAGE}} - Target language name
|
|
37723
|
+
- {{DOCUMENT_CONTEXT}} - Surrounding segments from the project for context
|
|
37724
|
+
"""
|
|
37540
37725
|
if not hasattr(self, 'prompt_manager_qt') or not self.prompt_manager_qt:
|
|
37541
37726
|
raise RuntimeError("Prompt manager not available")
|
|
37542
37727
|
|
|
@@ -37553,25 +37738,70 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37553
37738
|
if not prompt_content:
|
|
37554
37739
|
raise RuntimeError("Prompt content is empty")
|
|
37555
37740
|
|
|
37556
|
-
|
|
37557
|
-
|
|
37558
|
-
|
|
37559
|
-
|
|
37560
|
-
system_template = system_template.replace("{{TARGET_LANGUAGE}}", target_lang)
|
|
37561
|
-
system_template = system_template.replace("{{SOURCE_TEXT}}", source_text)
|
|
37741
|
+
# Build document context if requested
|
|
37742
|
+
document_context = ""
|
|
37743
|
+
if "{{DOCUMENT_CONTEXT}}" in prompt_content and hasattr(self, 'current_project') and self.current_project:
|
|
37744
|
+
document_context = self._build_quickmenu_document_context()
|
|
37562
37745
|
|
|
37563
|
-
|
|
37564
|
-
|
|
37565
|
-
|
|
37746
|
+
# Replace placeholders in the prompt content
|
|
37747
|
+
prompt_content = prompt_content.replace("{{SOURCE_LANGUAGE}}", source_lang)
|
|
37748
|
+
prompt_content = prompt_content.replace("{{TARGET_LANGUAGE}}", target_lang)
|
|
37749
|
+
prompt_content = prompt_content.replace("{{SOURCE_TEXT}}", source_text)
|
|
37750
|
+
prompt_content = prompt_content.replace("{{SELECTION}}", source_text) # Alternative placeholder
|
|
37751
|
+
prompt_content = prompt_content.replace("{{DOCUMENT_CONTEXT}}", document_context)
|
|
37752
|
+
|
|
37753
|
+
# If the prompt doesn't contain the selection/text, append it
|
|
37754
|
+
if "{{SOURCE_TEXT}}" not in prompt_data.get('content', '') and "{{SELECTION}}" not in prompt_data.get('content', ''):
|
|
37755
|
+
prompt_content += f"\n\nText:\n{source_text}"
|
|
37756
|
+
|
|
37757
|
+
return prompt_content
|
|
37758
|
+
|
|
37759
|
+
def _build_quickmenu_document_context(self) -> str:
|
|
37760
|
+
"""Build document context for QuickMenu prompts.
|
|
37761
|
+
|
|
37762
|
+
Returns a formatted string with segments from the project for context.
|
|
37763
|
+
Uses the 'quickmenu_context_segments' setting (default: 50% of total segments).
|
|
37764
|
+
"""
|
|
37765
|
+
if not hasattr(self, 'current_project') or not self.current_project or not self.current_project.segments:
|
|
37766
|
+
return "(No project context available)"
|
|
37767
|
+
|
|
37566
37768
|
try:
|
|
37567
|
-
|
|
37568
|
-
|
|
37569
|
-
|
|
37570
|
-
|
|
37571
|
-
|
|
37572
|
-
|
|
37573
|
-
|
|
37574
|
-
|
|
37769
|
+
# Get settings
|
|
37770
|
+
general_prefs = self.load_general_settings_from_file()
|
|
37771
|
+
context_percent = general_prefs.get('quickmenu_context_percent', 50) # Default: 50%
|
|
37772
|
+
max_context_segments = general_prefs.get('quickmenu_context_max', 100) # Safety limit
|
|
37773
|
+
|
|
37774
|
+
# Calculate how many segments to include
|
|
37775
|
+
total_segments = len(self.current_project.segments)
|
|
37776
|
+
num_segments = min(
|
|
37777
|
+
int(total_segments * context_percent / 100),
|
|
37778
|
+
max_context_segments
|
|
37779
|
+
)
|
|
37780
|
+
|
|
37781
|
+
if num_segments == 0:
|
|
37782
|
+
return "(Document context disabled)"
|
|
37783
|
+
|
|
37784
|
+
# Get segments (from start of document)
|
|
37785
|
+
context_segments = self.current_project.segments[:num_segments]
|
|
37786
|
+
|
|
37787
|
+
# Format segments
|
|
37788
|
+
context_parts = []
|
|
37789
|
+
context_parts.append(f"=== DOCUMENT CONTEXT ===")
|
|
37790
|
+
context_parts.append(f"(Showing {num_segments} of {total_segments} segments - {context_percent}%)")
|
|
37791
|
+
context_parts.append("")
|
|
37792
|
+
|
|
37793
|
+
for seg in context_segments:
|
|
37794
|
+
# Source text
|
|
37795
|
+
context_parts.append(f"[{seg.id}] {seg.source}")
|
|
37796
|
+
# Target text if available
|
|
37797
|
+
if seg.target and seg.target.strip():
|
|
37798
|
+
context_parts.append(f" → {seg.target}")
|
|
37799
|
+
context_parts.append("") # Blank line between segments
|
|
37800
|
+
|
|
37801
|
+
return "\n".join(context_parts)
|
|
37802
|
+
|
|
37803
|
+
except Exception as e:
|
|
37804
|
+
return f"(Error building document context: {e})"
|
|
37575
37805
|
|
|
37576
37806
|
def _quickmenu_show_result_dialog(self, title: str, output_text: str, apply_callback=None):
|
|
37577
37807
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QPushButton, QApplication
|
|
@@ -37651,10 +37881,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37651
37881
|
)
|
|
37652
37882
|
|
|
37653
37883
|
client = LLMClient(api_key=api_key, provider=provider, model=model)
|
|
37884
|
+
|
|
37885
|
+
# Use translate() with empty text and custom_prompt for generic AI completion
|
|
37886
|
+
# This allows QuickMenu prompts to do anything (explain, define, search, etc.)
|
|
37887
|
+
# not just translation. Same pattern as AI Assistant.
|
|
37654
37888
|
output_text = client.translate(
|
|
37655
|
-
text=
|
|
37656
|
-
source_lang=
|
|
37657
|
-
target_lang=
|
|
37889
|
+
text="", # Empty - we're using custom_prompt for everything
|
|
37890
|
+
source_lang="en", # Dummy values
|
|
37891
|
+
target_lang="en",
|
|
37658
37892
|
custom_prompt=custom_prompt
|
|
37659
37893
|
)
|
|
37660
37894
|
|
|
@@ -39553,9 +39787,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
39553
39787
|
def show_autofingers(self):
|
|
39554
39788
|
"""Show AutoFingers by switching to the AutoFingers tab"""
|
|
39555
39789
|
# Find the AutoFingers tab index and activate it
|
|
39556
|
-
# AutoFingers is in Tools tab (main_tabs index
|
|
39790
|
+
# AutoFingers is in Tools tab (main_tabs index 3)
|
|
39557
39791
|
if hasattr(self, 'main_tabs'):
|
|
39558
|
-
self.main_tabs.setCurrentIndex(
|
|
39792
|
+
self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
|
|
39559
39793
|
# Then switch to AutoFingers sub-tab
|
|
39560
39794
|
if hasattr(self, 'modules_tabs'):
|
|
39561
39795
|
for i in range(self.modules_tabs.count()):
|
|
@@ -44380,11 +44614,11 @@ class SuperlookupTab(QWidget):
|
|
|
44380
44614
|
print(f"[Superlookup] Main window type: {type(main_window).__name__}")
|
|
44381
44615
|
print(f"[Superlookup] Has main_tabs: {hasattr(main_window, 'main_tabs')}")
|
|
44382
44616
|
|
|
44383
|
-
# Switch to Tools tab (main_tabs index
|
|
44384
|
-
# Tab structure: Grid=0,
|
|
44617
|
+
# Switch to Tools tab (main_tabs index 3)
|
|
44618
|
+
# Tab structure: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
|
|
44385
44619
|
if hasattr(main_window, 'main_tabs'):
|
|
44386
44620
|
print(f"[Superlookup] Current main_tab index: {main_window.main_tabs.currentIndex()}")
|
|
44387
|
-
main_window.main_tabs.setCurrentIndex(
|
|
44621
|
+
main_window.main_tabs.setCurrentIndex(3) # Tools tab
|
|
44388
44622
|
print(f"[Superlookup] Switched to Tools tab (index 2)")
|
|
44389
44623
|
QApplication.processEvents() # Force GUI update
|
|
44390
44624
|
else:
|