supervertaler 1.9.116__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 +291 -85
- 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.116.dist-info → supervertaler-1.9.128.dist-info}/METADATA +33 -3
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.128.dist-info}/RECORD +11 -11
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.128.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.128.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.128.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.116.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
|
)
|
|
@@ -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.
|
|
@@ -10836,6 +10854,38 @@ class SupervertalerQt(QMainWindow):
|
|
|
10836
10854
|
self._play_sound_effect('glossary_term_error')
|
|
10837
10855
|
self.statusBar().showMessage(f"Error adding term: {e}", 3000)
|
|
10838
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
|
+
|
|
10839
10889
|
def add_text_to_non_translatables(self, text: str):
|
|
10840
10890
|
"""Add selected text to active non-translatable list(s)"""
|
|
10841
10891
|
if not text or not text.strip():
|
|
@@ -13956,6 +14006,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
13956
14006
|
full_context_cb.stateChanged.connect(lambda: context_slider.setEnabled(full_context_cb.isChecked()))
|
|
13957
14007
|
update_context_label(context_window_size)
|
|
13958
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
|
+
|
|
13959
14048
|
prefs_layout.addSpacing(5)
|
|
13960
14049
|
|
|
13961
14050
|
# Check TM before API call
|
|
@@ -14100,7 +14189,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
14100
14189
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
14101
14190
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
14102
14191
|
ollama_keepwarm_cb,
|
|
14103
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
14192
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
14193
|
+
quickmenu_context_slider
|
|
14104
14194
|
))
|
|
14105
14195
|
layout.addWidget(save_btn)
|
|
14106
14196
|
|
|
@@ -17098,7 +17188,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
17098
17188
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
17099
17189
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
17100
17190
|
ollama_keepwarm_cb,
|
|
17101
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
17191
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
17192
|
+
quickmenu_context_slider):
|
|
17102
17193
|
"""Save all AI settings from the unified AI Settings tab"""
|
|
17103
17194
|
# Determine selected provider
|
|
17104
17195
|
if openai_radio.isChecked():
|
|
@@ -17157,6 +17248,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
17157
17248
|
general_prefs['surrounding_segments'] = surrounding_spin.value()
|
|
17158
17249
|
general_prefs['use_full_context'] = full_context_cb.isChecked()
|
|
17159
17250
|
general_prefs['context_window_size'] = context_slider.value()
|
|
17251
|
+
general_prefs['quickmenu_context_percent'] = quickmenu_context_slider.value()
|
|
17160
17252
|
general_prefs['check_tm_before_api'] = check_tm_cb.isChecked()
|
|
17161
17253
|
general_prefs['auto_propagate_100'] = auto_propagate_cb.isChecked()
|
|
17162
17254
|
general_prefs['lookup_delay'] = delay_spin.value()
|
|
@@ -31042,11 +31134,18 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31042
31134
|
normalized_source = clean_source_lower
|
|
31043
31135
|
for quote_char in '\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A':
|
|
31044
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
|
+
|
|
31045
31144
|
# Check if term has punctuation - use different pattern
|
|
31046
|
-
if any(char in
|
|
31047
|
-
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)')
|
|
31048
31147
|
else:
|
|
31049
|
-
pattern = re.compile(r"\b" + re.escape(
|
|
31148
|
+
pattern = re.compile(r"\b" + re.escape(normalized_term) + r"\b")
|
|
31050
31149
|
|
|
31051
31150
|
# Try matching on normalized (tag-stripped, quote-stripped) text first,
|
|
31052
31151
|
# then tag-stripped, then original with tags
|
|
@@ -31503,7 +31602,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31503
31602
|
self.case_sensitive_cb.setChecked(op.case_sensitive)
|
|
31504
31603
|
|
|
31505
31604
|
def _fr_run_set_batch(self, fr_set: FindReplaceSet):
|
|
31506
|
-
"""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)."""
|
|
31507
31606
|
enabled_ops = [op for op in fr_set.operations if op.enabled and op.find_text]
|
|
31508
31607
|
|
|
31509
31608
|
if not enabled_ops:
|
|
@@ -31522,17 +31621,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31522
31621
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31523
31622
|
return
|
|
31524
31623
|
|
|
31525
|
-
#
|
|
31526
|
-
|
|
31527
|
-
for op in enabled_ops:
|
|
31528
|
-
count = self._execute_single_fr_operation(op)
|
|
31529
|
-
total_replaced += count
|
|
31530
|
-
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)
|
|
31531
31626
|
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
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()
|
|
31536
31650
|
|
|
31537
31651
|
QMessageBox.information(
|
|
31538
31652
|
self.find_replace_dialog,
|
|
@@ -31541,12 +31655,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31541
31655
|
)
|
|
31542
31656
|
|
|
31543
31657
|
def _execute_single_fr_operation(self, op: FindReplaceOperation) -> int:
|
|
31544
|
-
"""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."""
|
|
31545
31659
|
import re
|
|
31546
31660
|
count = 0
|
|
31547
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
|
+
|
|
31548
31666
|
for segment in self.current_project.segments:
|
|
31549
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
|
+
|
|
31550
31682
|
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
31551
31683
|
texts_to_check.append(("source", segment.source))
|
|
31552
31684
|
if op.search_in in ("target", "both"):
|
|
@@ -31842,7 +31974,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31842
31974
|
self.find_next_match()
|
|
31843
31975
|
|
|
31844
31976
|
def replace_all_matches(self):
|
|
31845
|
-
"""Replace all matches in target segments"""
|
|
31977
|
+
"""Replace all matches in target segments (optimized for speed)"""
|
|
31846
31978
|
find_text = self.find_input.text()
|
|
31847
31979
|
replace_text = self.replace_input.text()
|
|
31848
31980
|
|
|
@@ -31915,52 +32047,67 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31915
32047
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31916
32048
|
return
|
|
31917
32049
|
|
|
31918
|
-
#
|
|
31919
|
-
|
|
31920
|
-
replaced_count = 0
|
|
32050
|
+
# OPTIMIZATION: Disable UI updates during batch replacement
|
|
32051
|
+
self.table.setUpdatesEnabled(False)
|
|
31921
32052
|
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
|
|
31927
|
-
old_text = segment.source
|
|
31928
|
-
else: # col == 3, Target
|
|
31929
|
-
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
|
|
31930
32058
|
|
|
31931
|
-
|
|
31932
|
-
|
|
31933
|
-
|
|
31934
|
-
|
|
31935
|
-
if
|
|
31936
|
-
|
|
31937
|
-
else:
|
|
31938
|
-
|
|
31939
|
-
|
|
31940
|
-
|
|
31941
|
-
|
|
31942
|
-
|
|
31943
|
-
# Update the appropriate field
|
|
31944
|
-
if col == 2:
|
|
31945
|
-
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
|
|
31946
32071
|
else:
|
|
31947
|
-
|
|
31948
|
-
|
|
31949
|
-
|
|
31950
|
-
|
|
31951
|
-
|
|
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)
|
|
31952
32077
|
|
|
31953
|
-
|
|
31954
|
-
|
|
31955
|
-
|
|
31956
|
-
|
|
31957
|
-
|
|
31958
|
-
|
|
31959
|
-
|
|
31960
|
-
|
|
31961
|
-
|
|
31962
|
-
|
|
31963
|
-
|
|
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()
|
|
31964
32111
|
|
|
31965
32112
|
QMessageBox.information(self.find_replace_dialog, "Replace All", f"Replaced {replaced_count} occurrence(s).")
|
|
31966
32113
|
self.log(f"✓ Replaced {replaced_count} occurrence(s) of '{find_text}'")
|
|
@@ -37564,7 +37711,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37564
37711
|
return text.strip()
|
|
37565
37712
|
|
|
37566
37713
|
def _quickmenu_build_custom_prompt(self, prompt_relative_path: str, source_text: str, source_lang: str, target_lang: str) -> str:
|
|
37567
|
-
"""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
|
+
"""
|
|
37568
37725
|
if not hasattr(self, 'prompt_manager_qt') or not self.prompt_manager_qt:
|
|
37569
37726
|
raise RuntimeError("Prompt manager not available")
|
|
37570
37727
|
|
|
@@ -37581,25 +37738,70 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37581
37738
|
if not prompt_content:
|
|
37582
37739
|
raise RuntimeError("Prompt content is empty")
|
|
37583
37740
|
|
|
37584
|
-
|
|
37585
|
-
|
|
37586
|
-
|
|
37587
|
-
|
|
37588
|
-
system_template = system_template.replace("{{TARGET_LANGUAGE}}", target_lang)
|
|
37589
|
-
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()
|
|
37590
37745
|
|
|
37591
|
-
|
|
37592
|
-
|
|
37593
|
-
|
|
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
|
+
|
|
37594
37768
|
try:
|
|
37595
|
-
|
|
37596
|
-
|
|
37597
|
-
|
|
37598
|
-
|
|
37599
|
-
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
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})"
|
|
37603
37805
|
|
|
37604
37806
|
def _quickmenu_show_result_dialog(self, title: str, output_text: str, apply_callback=None):
|
|
37605
37807
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QPushButton, QApplication
|
|
@@ -37679,10 +37881,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37679
37881
|
)
|
|
37680
37882
|
|
|
37681
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.
|
|
37682
37888
|
output_text = client.translate(
|
|
37683
|
-
text=
|
|
37684
|
-
source_lang=
|
|
37685
|
-
target_lang=
|
|
37889
|
+
text="", # Empty - we're using custom_prompt for everything
|
|
37890
|
+
source_lang="en", # Dummy values
|
|
37891
|
+
target_lang="en",
|
|
37686
37892
|
custom_prompt=custom_prompt
|
|
37687
37893
|
)
|
|
37688
37894
|
|
modules/shortcut_manager.py
CHANGED
|
@@ -18,9 +18,15 @@ class ShortcutManager:
|
|
|
18
18
|
"file_new": {
|
|
19
19
|
"category": "File",
|
|
20
20
|
"description": "New Project",
|
|
21
|
-
"default": "
|
|
21
|
+
"default": "",
|
|
22
22
|
"action": "new_project"
|
|
23
23
|
},
|
|
24
|
+
"editor_focus_notes": {
|
|
25
|
+
"category": "Edit",
|
|
26
|
+
"description": "Focus Segment Note Tab (Ctrl+N)",
|
|
27
|
+
"default": "Ctrl+N",
|
|
28
|
+
"action": "focus_segment_notes"
|
|
29
|
+
},
|
|
24
30
|
"file_open": {
|
|
25
31
|
"category": "File",
|
|
26
32
|
"description": "Open Project",
|
|
@@ -542,6 +548,13 @@ class ShortcutManager:
|
|
|
542
548
|
"action": "copy_source_to_target",
|
|
543
549
|
"context": "grid_editor"
|
|
544
550
|
},
|
|
551
|
+
"editor_add_to_dictionary": {
|
|
552
|
+
"category": "Editor",
|
|
553
|
+
"description": "Add word at cursor to custom dictionary",
|
|
554
|
+
"default": "Alt+D",
|
|
555
|
+
"action": "add_word_to_dictionary",
|
|
556
|
+
"context": "grid_editor"
|
|
557
|
+
},
|
|
545
558
|
|
|
546
559
|
# Filter Operations
|
|
547
560
|
"filter_selected_text": {
|
modules/termview_widget.py
CHANGED
|
@@ -941,13 +941,20 @@ class TermviewWidget(QWidget):
|
|
|
941
941
|
for quote_char in '\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A':
|
|
942
942
|
normalized_text = normalized_text.replace(quote_char, ' ')
|
|
943
943
|
|
|
944
|
+
# CRITICAL FIX v1.9.118: Strip punctuation from glossary term before matching
|
|
945
|
+
# This allows entries like "...problemen." (with period) to match source text
|
|
946
|
+
# where tokenization strips the period during word splitting
|
|
947
|
+
# Comprehensive set of quote and punctuation characters to strip
|
|
948
|
+
PUNCT_CHARS = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A'
|
|
949
|
+
normalized_term = source_lower.rstrip(PUNCT_CHARS).lstrip(PUNCT_CHARS)
|
|
950
|
+
|
|
944
951
|
# Use word boundaries to match complete words/phrases only
|
|
945
952
|
if ' ' in source_term:
|
|
946
953
|
# Multi-word term - must exist as exact phrase
|
|
947
|
-
pattern = r'\b' + re.escape(
|
|
954
|
+
pattern = r'\b' + re.escape(normalized_term) + r'\b'
|
|
948
955
|
else:
|
|
949
956
|
# Single word
|
|
950
|
-
pattern = r'\b' + re.escape(
|
|
957
|
+
pattern = r'\b' + re.escape(normalized_term) + r'\b'
|
|
951
958
|
|
|
952
959
|
# Try matching on normalized text first, then original
|
|
953
960
|
if not re.search(pattern, normalized_text) and not re.search(pattern, text_lower):
|
|
@@ -171,11 +171,14 @@ class UnifiedPromptLibrary:
|
|
|
171
171
|
# Backward compatibility: quick_run is the legacy field; internally we
|
|
172
172
|
# treat it as the "QuickMenu (future app menu)" flag.
|
|
173
173
|
prompt_data.setdefault('quick_run', False)
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
# Support legacy quickmenu_quickmenu field (rename to sv_quickmenu)
|
|
175
|
+
if 'quickmenu_quickmenu' in prompt_data:
|
|
176
|
+
prompt_data['sv_quickmenu'] = prompt_data['quickmenu_quickmenu']
|
|
177
|
+
prompt_data['sv_quickmenu'] = bool(
|
|
178
|
+
prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))
|
|
176
179
|
)
|
|
177
180
|
# Keep legacy field in sync so older code/versions still behave.
|
|
178
|
-
prompt_data['quick_run'] = bool(prompt_data['
|
|
181
|
+
prompt_data['quick_run'] = bool(prompt_data['sv_quickmenu'])
|
|
179
182
|
|
|
180
183
|
# New QuickMenu fields
|
|
181
184
|
prompt_data.setdefault('quickmenu_grid', False)
|
|
@@ -270,7 +273,7 @@ class UnifiedPromptLibrary:
|
|
|
270
273
|
'name', 'description', 'domain', 'version', 'task_type',
|
|
271
274
|
'favorite',
|
|
272
275
|
# QuickMenu
|
|
273
|
-
'quickmenu_label', 'quickmenu_grid', '
|
|
276
|
+
'quickmenu_label', 'quickmenu_grid', 'sv_quickmenu',
|
|
274
277
|
# Legacy (kept for backward compatibility)
|
|
275
278
|
'quick_run',
|
|
276
279
|
'folder', 'tags',
|
|
@@ -309,8 +312,8 @@ class UnifiedPromptLibrary:
|
|
|
309
312
|
prompt_data['_relative_path'] = relative_path
|
|
310
313
|
|
|
311
314
|
# Keep legacy field in sync
|
|
312
|
-
if '
|
|
313
|
-
prompt_data['quick_run'] = bool(prompt_data.get('
|
|
315
|
+
if 'sv_quickmenu' in prompt_data:
|
|
316
|
+
prompt_data['quick_run'] = bool(prompt_data.get('sv_quickmenu', False))
|
|
314
317
|
self.prompts[relative_path] = prompt_data
|
|
315
318
|
|
|
316
319
|
self.log(f"✓ Saved prompt: {prompt_data.get('name', relative_path)}")
|
|
@@ -456,8 +459,8 @@ class UnifiedPromptLibrary:
|
|
|
456
459
|
return False
|
|
457
460
|
|
|
458
461
|
prompt_data = self.prompts[relative_path]
|
|
459
|
-
new_value = not bool(prompt_data.get('
|
|
460
|
-
prompt_data['
|
|
462
|
+
new_value = not bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)))
|
|
463
|
+
prompt_data['sv_quickmenu'] = new_value
|
|
461
464
|
prompt_data['quick_run'] = new_value # keep legacy in sync
|
|
462
465
|
prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
|
|
463
466
|
|
|
@@ -493,7 +496,7 @@ class UnifiedPromptLibrary:
|
|
|
493
496
|
"""Update cached QuickMenu (future app menu) list (legacy name: quick_run)."""
|
|
494
497
|
self._quick_run = []
|
|
495
498
|
for path, data in self.prompts.items():
|
|
496
|
-
is_enabled = bool(data.get('
|
|
499
|
+
is_enabled = bool(data.get('sv_quickmenu', data.get('quick_run', False)))
|
|
497
500
|
if not is_enabled:
|
|
498
501
|
continue
|
|
499
502
|
label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
|
|
@@ -18,7 +18,7 @@ from PyQt6.QtWidgets import (
|
|
|
18
18
|
QTextEdit, QPlainTextEdit, QSplitter, QGroupBox, QMessageBox, QFileDialog,
|
|
19
19
|
QInputDialog, QLineEdit, QFrame, QMenu, QCheckBox, QSizePolicy, QScrollArea, QTabWidget,
|
|
20
20
|
QListWidget, QListWidgetItem, QStyledItemDelegate, QStyleOptionViewItem, QApplication, QDialog,
|
|
21
|
-
QAbstractItemView
|
|
21
|
+
QAbstractItemView, QTableWidget, QTableWidgetItem, QHeaderView
|
|
22
22
|
)
|
|
23
23
|
from PyQt6.QtCore import Qt, QSettings, pyqtSignal, QThread, QSize, QRect, QRectF
|
|
24
24
|
from PyQt6.QtGui import QFont, QColor, QAction, QIcon, QPainter, QPen, QBrush, QPainterPath, QLinearGradient
|
|
@@ -31,6 +31,89 @@ from modules.ai_file_viewer_dialog import FileViewerDialog, FileRemoveConfirmDia
|
|
|
31
31
|
from modules.ai_actions import AIActionSystem
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class CheckmarkCheckBox(QCheckBox):
|
|
35
|
+
"""Custom checkbox with green background and white checkmark when checked"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, text="", parent=None):
|
|
38
|
+
super().__init__(text, parent)
|
|
39
|
+
self.setCheckable(True)
|
|
40
|
+
self.setEnabled(True)
|
|
41
|
+
self.setStyleSheet("""
|
|
42
|
+
QCheckBox {
|
|
43
|
+
font-size: 9pt;
|
|
44
|
+
spacing: 6px;
|
|
45
|
+
}
|
|
46
|
+
QCheckBox::indicator {
|
|
47
|
+
width: 16px;
|
|
48
|
+
height: 16px;
|
|
49
|
+
border: 2px solid #999;
|
|
50
|
+
border-radius: 3px;
|
|
51
|
+
background-color: white;
|
|
52
|
+
}
|
|
53
|
+
QCheckBox::indicator:checked {
|
|
54
|
+
background-color: #4CAF50;
|
|
55
|
+
border-color: #4CAF50;
|
|
56
|
+
}
|
|
57
|
+
QCheckBox::indicator:hover {
|
|
58
|
+
border-color: #666;
|
|
59
|
+
}
|
|
60
|
+
QCheckBox::indicator:checked:hover {
|
|
61
|
+
background-color: #45a049;
|
|
62
|
+
border-color: #45a049;
|
|
63
|
+
}
|
|
64
|
+
""")
|
|
65
|
+
|
|
66
|
+
def paintEvent(self, event):
|
|
67
|
+
"""Override paint event to draw white checkmark when checked"""
|
|
68
|
+
super().paintEvent(event)
|
|
69
|
+
|
|
70
|
+
if self.isChecked():
|
|
71
|
+
# Get the indicator rectangle using QStyle
|
|
72
|
+
from PyQt6.QtWidgets import QStyleOptionButton
|
|
73
|
+
from PyQt6.QtCore import QPointF
|
|
74
|
+
|
|
75
|
+
opt = QStyleOptionButton()
|
|
76
|
+
self.initStyleOption(opt)
|
|
77
|
+
indicator_rect = self.style().subElementRect(
|
|
78
|
+
self.style().SubElement.SE_CheckBoxIndicator,
|
|
79
|
+
opt,
|
|
80
|
+
self
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if indicator_rect.isValid():
|
|
84
|
+
# Draw white checkmark
|
|
85
|
+
painter = QPainter(self)
|
|
86
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
87
|
+
pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
|
|
88
|
+
painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
|
|
89
|
+
painter.setBrush(QColor(255, 255, 255))
|
|
90
|
+
|
|
91
|
+
# Draw checkmark (✓ shape) - coordinates relative to indicator
|
|
92
|
+
x = indicator_rect.x()
|
|
93
|
+
y = indicator_rect.y()
|
|
94
|
+
w = indicator_rect.width()
|
|
95
|
+
h = indicator_rect.height()
|
|
96
|
+
|
|
97
|
+
# Add padding (15% on all sides)
|
|
98
|
+
padding = min(w, h) * 0.15
|
|
99
|
+
x += padding
|
|
100
|
+
y += padding
|
|
101
|
+
w -= padding * 2
|
|
102
|
+
h -= padding * 2
|
|
103
|
+
|
|
104
|
+
# Checkmark path
|
|
105
|
+
check_x1 = x + w * 0.10
|
|
106
|
+
check_y1 = y + h * 0.50
|
|
107
|
+
check_x2 = x + w * 0.35
|
|
108
|
+
check_y2 = y + h * 0.70
|
|
109
|
+
check_x3 = x + w * 0.90
|
|
110
|
+
check_y3 = y + h * 0.25
|
|
111
|
+
|
|
112
|
+
# Draw two lines forming the checkmark
|
|
113
|
+
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
114
|
+
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
115
|
+
|
|
116
|
+
|
|
34
117
|
class PromptLibraryTreeWidget(QTreeWidget):
|
|
35
118
|
"""Tree widget that supports drag-and-drop moves for prompt files."""
|
|
36
119
|
|
|
@@ -563,6 +646,10 @@ class UnifiedPromptManagerQt:
|
|
|
563
646
|
assistant_tab = self._create_ai_assistant_tab()
|
|
564
647
|
self.sub_tabs.addTab(assistant_tab, "✨ AI Assistant")
|
|
565
648
|
|
|
649
|
+
# Tab 3: Placeholders Reference
|
|
650
|
+
placeholders_tab = self._create_placeholders_tab()
|
|
651
|
+
self.sub_tabs.addTab(placeholders_tab, "📝 Placeholders")
|
|
652
|
+
|
|
566
653
|
# Connect tab change signal to update context
|
|
567
654
|
self.sub_tabs.currentChanged.connect(self._on_tab_changed)
|
|
568
655
|
|
|
@@ -764,6 +851,143 @@ class UnifiedPromptManagerQt:
|
|
|
764
851
|
|
|
765
852
|
return tab
|
|
766
853
|
|
|
854
|
+
def _create_placeholders_tab(self) -> QWidget:
|
|
855
|
+
"""Create the Placeholders Reference sub-tab"""
|
|
856
|
+
tab = QWidget()
|
|
857
|
+
layout = QVBoxLayout(tab)
|
|
858
|
+
layout.setContentsMargins(10, 10, 10, 10)
|
|
859
|
+
layout.setSpacing(5)
|
|
860
|
+
|
|
861
|
+
# Header (matches standard tool style: Superbench, AutoFingers, TMX Editor)
|
|
862
|
+
header = QLabel("📝 Available Placeholders")
|
|
863
|
+
header.setStyleSheet("font-size: 16pt; font-weight: bold; color: #1976D2;")
|
|
864
|
+
layout.addWidget(header, 0)
|
|
865
|
+
|
|
866
|
+
# Description box (matches standard tool style)
|
|
867
|
+
description = QLabel(
|
|
868
|
+
"Use these placeholders in your prompts. They will be replaced with actual values when the prompt runs."
|
|
869
|
+
)
|
|
870
|
+
description.setWordWrap(True)
|
|
871
|
+
description.setStyleSheet("color: #666; padding: 5px; background-color: #E3F2FD; border-radius: 3px;")
|
|
872
|
+
layout.addWidget(description, 0)
|
|
873
|
+
|
|
874
|
+
# Horizontal splitter for table and tips
|
|
875
|
+
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
876
|
+
splitter.setHandleWidth(3)
|
|
877
|
+
|
|
878
|
+
# Left: Table with placeholders
|
|
879
|
+
table = QTableWidget()
|
|
880
|
+
table.setColumnCount(3)
|
|
881
|
+
table.setHorizontalHeaderLabels(["Placeholder", "Description", "Example"])
|
|
882
|
+
table.horizontalHeader().setStretchLastSection(True)
|
|
883
|
+
table.verticalHeader().setVisible(False)
|
|
884
|
+
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
885
|
+
table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
886
|
+
table.setAlternatingRowColors(True)
|
|
887
|
+
|
|
888
|
+
# Placeholder data
|
|
889
|
+
placeholders = [
|
|
890
|
+
(
|
|
891
|
+
"{{SELECTION}}",
|
|
892
|
+
"Currently selected text in the grid (source or target cell)",
|
|
893
|
+
"If you select 'translation memory' in the grid, this will contain that text"
|
|
894
|
+
),
|
|
895
|
+
(
|
|
896
|
+
"{{SOURCE_TEXT}}",
|
|
897
|
+
"Full text of the current source segment",
|
|
898
|
+
"The complete source sentence/paragraph from the active segment"
|
|
899
|
+
),
|
|
900
|
+
(
|
|
901
|
+
"{{SOURCE_LANGUAGE}}",
|
|
902
|
+
"Project's source language",
|
|
903
|
+
"Dutch, English, German, French, etc."
|
|
904
|
+
),
|
|
905
|
+
(
|
|
906
|
+
"{{TARGET_LANGUAGE}}",
|
|
907
|
+
"Project's target language",
|
|
908
|
+
"English, Spanish, Portuguese, etc."
|
|
909
|
+
),
|
|
910
|
+
(
|
|
911
|
+
"{{DOCUMENT_CONTEXT}}",
|
|
912
|
+
"Formatted list of project segments (configurable % in Settings → AI Settings)",
|
|
913
|
+
"[1] Source text\\n → Target text\\n\\n[2] Source text\\n → Target text\\n\\n..."
|
|
914
|
+
)
|
|
915
|
+
]
|
|
916
|
+
|
|
917
|
+
table.setRowCount(len(placeholders))
|
|
918
|
+
for row, (placeholder, description, example) in enumerate(placeholders):
|
|
919
|
+
# Placeholder column (monospace, bold)
|
|
920
|
+
item_placeholder = QTableWidgetItem(placeholder)
|
|
921
|
+
item_placeholder.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
|
|
922
|
+
table.setItem(row, 0, item_placeholder)
|
|
923
|
+
|
|
924
|
+
# Description column
|
|
925
|
+
item_desc = QTableWidgetItem(description)
|
|
926
|
+
item_desc.setToolTip(description)
|
|
927
|
+
table.setItem(row, 1, item_desc)
|
|
928
|
+
|
|
929
|
+
# Example column (monospace, italic)
|
|
930
|
+
item_example = QTableWidgetItem(example)
|
|
931
|
+
item_example.setFont(QFont("Courier New", 9))
|
|
932
|
+
item_example.setToolTip(example)
|
|
933
|
+
table.setItem(row, 2, item_example)
|
|
934
|
+
|
|
935
|
+
# Set column widths
|
|
936
|
+
table.setColumnWidth(0, 200)
|
|
937
|
+
table.setColumnWidth(1, 300)
|
|
938
|
+
header = table.horizontalHeader()
|
|
939
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
|
940
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive)
|
|
941
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
942
|
+
|
|
943
|
+
# Adjust row heights for readability
|
|
944
|
+
for row in range(table.rowCount()):
|
|
945
|
+
table.setRowHeight(row, 60)
|
|
946
|
+
|
|
947
|
+
splitter.addWidget(table)
|
|
948
|
+
|
|
949
|
+
# Right: Usage tips panel
|
|
950
|
+
tips_panel = QWidget()
|
|
951
|
+
tips_layout = QVBoxLayout(tips_panel)
|
|
952
|
+
tips_layout.setContentsMargins(10, 0, 0, 0)
|
|
953
|
+
|
|
954
|
+
tips_header = QLabel("💡 Usage Tips")
|
|
955
|
+
tips_header.setStyleSheet("font-weight: bold; font-size: 11pt; color: #2196F3; margin-bottom: 8px;")
|
|
956
|
+
tips_layout.addWidget(tips_header)
|
|
957
|
+
|
|
958
|
+
tips_intro = QLabel(
|
|
959
|
+
"Use these placeholders in your prompts. They will be replaced with actual values when the prompt runs."
|
|
960
|
+
)
|
|
961
|
+
tips_intro.setWordWrap(True)
|
|
962
|
+
tips_intro.setStyleSheet("color: #666; margin-bottom: 15px; font-style: italic;")
|
|
963
|
+
tips_layout.addWidget(tips_intro)
|
|
964
|
+
|
|
965
|
+
tips_text = QLabel(
|
|
966
|
+
"• Placeholders are case-sensitive (use UPPERCASE)\n\n"
|
|
967
|
+
"• Surround placeholders with double curly braces: {{ }}\n\n"
|
|
968
|
+
"• You can combine multiple placeholders in one prompt\n\n"
|
|
969
|
+
"• Use {{DOCUMENT_CONTEXT}} for context-aware translations\n\n"
|
|
970
|
+
"• Configure {{DOCUMENT_CONTEXT}} percentage in Settings → AI Settings"
|
|
971
|
+
)
|
|
972
|
+
tips_text.setWordWrap(True)
|
|
973
|
+
tips_text.setStyleSheet("color: #666; line-height: 1.6;")
|
|
974
|
+
tips_layout.addWidget(tips_text)
|
|
975
|
+
|
|
976
|
+
tips_layout.addStretch()
|
|
977
|
+
|
|
978
|
+
tips_panel.setMinimumWidth(280)
|
|
979
|
+
tips_panel.setMaximumWidth(400)
|
|
980
|
+
splitter.addWidget(tips_panel)
|
|
981
|
+
|
|
982
|
+
# Set splitter proportions (75% table, 25% tips)
|
|
983
|
+
splitter.setSizes([750, 250])
|
|
984
|
+
splitter.setStretchFactor(0, 1) # Table expands
|
|
985
|
+
splitter.setStretchFactor(1, 0) # Tips panel fixed-ish
|
|
986
|
+
|
|
987
|
+
layout.addWidget(splitter, 1) # 1 = stretch to fill all available space
|
|
988
|
+
|
|
989
|
+
return tab
|
|
990
|
+
|
|
767
991
|
def _create_context_sidebar(self) -> QWidget:
|
|
768
992
|
"""Create context sidebar showing available resources"""
|
|
769
993
|
panel = QWidget()
|
|
@@ -1470,10 +1694,10 @@ class UnifiedPromptManagerQt:
|
|
|
1470
1694
|
self.editor_quickmenu_label_input.setPlaceholderText("Label shown in QuickMenu")
|
|
1471
1695
|
quickmenu_layout.addWidget(self.editor_quickmenu_label_input, 2)
|
|
1472
1696
|
|
|
1473
|
-
self.editor_quickmenu_in_grid_cb =
|
|
1697
|
+
self.editor_quickmenu_in_grid_cb = CheckmarkCheckBox("Show in Grid right-click QuickMenu")
|
|
1474
1698
|
quickmenu_layout.addWidget(self.editor_quickmenu_in_grid_cb, 2)
|
|
1475
1699
|
|
|
1476
|
-
self.editor_quickmenu_in_quickmenu_cb =
|
|
1700
|
+
self.editor_quickmenu_in_quickmenu_cb = CheckmarkCheckBox("Show in Supervertaler QuickMenu")
|
|
1477
1701
|
quickmenu_layout.addWidget(self.editor_quickmenu_in_quickmenu_cb, 1)
|
|
1478
1702
|
|
|
1479
1703
|
layout.addLayout(quickmenu_layout)
|
|
@@ -1950,7 +2174,7 @@ class UnifiedPromptManagerQt:
|
|
|
1950
2174
|
indicators = []
|
|
1951
2175
|
if prompt_data.get('favorite'):
|
|
1952
2176
|
indicators.append("⭐")
|
|
1953
|
-
if prompt_data.get('
|
|
2177
|
+
if prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)):
|
|
1954
2178
|
indicators.append("⚡")
|
|
1955
2179
|
if prompt_data.get('quickmenu_grid', False):
|
|
1956
2180
|
indicators.append("🖱️")
|
|
@@ -2018,7 +2242,7 @@ class UnifiedPromptManagerQt:
|
|
|
2018
2242
|
action_fav.triggered.connect(lambda: self._toggle_favorite(path))
|
|
2019
2243
|
|
|
2020
2244
|
# Toggle QuickMenu (legacy: quick_run)
|
|
2021
|
-
if prompt_data.get('
|
|
2245
|
+
if prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)):
|
|
2022
2246
|
action_qr = menu.addAction("⚡ Remove from QuickMenu")
|
|
2023
2247
|
else:
|
|
2024
2248
|
action_qr = menu.addAction("⚡ Add to QuickMenu")
|
|
@@ -2070,7 +2294,7 @@ class UnifiedPromptManagerQt:
|
|
|
2070
2294
|
if hasattr(self, 'editor_quickmenu_in_grid_cb'):
|
|
2071
2295
|
self.editor_quickmenu_in_grid_cb.setChecked(bool(prompt_data.get('quickmenu_grid', False)))
|
|
2072
2296
|
if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
|
|
2073
|
-
self.editor_quickmenu_in_quickmenu_cb.setChecked(bool(prompt_data.get('
|
|
2297
|
+
self.editor_quickmenu_in_quickmenu_cb.setChecked(bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))))
|
|
2074
2298
|
self.editor_content.setPlainText(prompt_data.get('content', ''))
|
|
2075
2299
|
|
|
2076
2300
|
# Store current path for saving
|
|
@@ -2083,119 +2307,127 @@ class UnifiedPromptManagerQt:
|
|
|
2083
2307
|
|
|
2084
2308
|
def _save_current_prompt(self):
|
|
2085
2309
|
"""Save currently edited prompt"""
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
# Name field now represents the complete filename with extension
|
|
2091
|
-
# No stripping needed - user sees and edits the full filename
|
|
2092
|
-
|
|
2093
|
-
quickmenu_label = ''
|
|
2094
|
-
quickmenu_grid = False
|
|
2095
|
-
quickmenu_quickmenu = False
|
|
2096
|
-
if hasattr(self, 'editor_quickmenu_label_input'):
|
|
2097
|
-
quickmenu_label = self.editor_quickmenu_label_input.text().strip()
|
|
2098
|
-
if hasattr(self, 'editor_quickmenu_in_grid_cb'):
|
|
2099
|
-
quickmenu_grid = bool(self.editor_quickmenu_in_grid_cb.isChecked())
|
|
2100
|
-
if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
|
|
2101
|
-
quickmenu_quickmenu = bool(self.editor_quickmenu_in_quickmenu_cb.isChecked())
|
|
2102
|
-
|
|
2103
|
-
if not name or not content:
|
|
2104
|
-
QMessageBox.warning(self.main_widget, "Error", "Name and content are required")
|
|
2105
|
-
return
|
|
2106
|
-
|
|
2107
|
-
# Check if this is a new prompt or editing existing
|
|
2108
|
-
if hasattr(self, 'editor_current_path') and self.editor_current_path:
|
|
2109
|
-
path = self.editor_current_path
|
|
2110
|
-
|
|
2111
|
-
# Handle external prompts (save back to external file)
|
|
2112
|
-
if path.startswith("[EXTERNAL] "):
|
|
2113
|
-
external_file_path = path[11:] # Remove "[EXTERNAL] " prefix
|
|
2114
|
-
self._save_external_prompt(external_file_path, name, description, content)
|
|
2115
|
-
return
|
|
2310
|
+
try:
|
|
2311
|
+
name = self.editor_name_input.text().strip()
|
|
2312
|
+
description = self.editor_desc_input.text().strip()
|
|
2313
|
+
content = self.editor_content.toPlainText().strip()
|
|
2116
2314
|
|
|
2117
|
-
#
|
|
2118
|
-
|
|
2119
|
-
|
|
2315
|
+
# Name field now represents the complete filename with extension
|
|
2316
|
+
# No stripping needed - user sees and edits the full filename
|
|
2317
|
+
|
|
2318
|
+
quickmenu_label = ''
|
|
2319
|
+
quickmenu_grid = False
|
|
2320
|
+
sv_quickmenu = False
|
|
2321
|
+
if hasattr(self, 'editor_quickmenu_label_input'):
|
|
2322
|
+
quickmenu_label = self.editor_quickmenu_label_input.text().strip()
|
|
2323
|
+
if hasattr(self, 'editor_quickmenu_in_grid_cb'):
|
|
2324
|
+
quickmenu_grid = bool(self.editor_quickmenu_in_grid_cb.isChecked())
|
|
2325
|
+
if hasattr(self, 'editor_quickmenu_in_quickmenu_cb'):
|
|
2326
|
+
sv_quickmenu = bool(self.editor_quickmenu_in_quickmenu_cb.isChecked())
|
|
2327
|
+
|
|
2328
|
+
if not name or not content:
|
|
2329
|
+
QMessageBox.warning(self.main_widget, "Error", "Name and content are required")
|
|
2120
2330
|
return
|
|
2121
2331
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
# Extract name without extension for metadata
|
|
2126
|
-
name_without_ext = Path(name).stem
|
|
2127
|
-
|
|
2128
|
-
prompt_data['name'] = name_without_ext
|
|
2129
|
-
prompt_data['description'] = description
|
|
2130
|
-
prompt_data['content'] = content
|
|
2131
|
-
prompt_data['quickmenu_label'] = quickmenu_label or name_without_ext
|
|
2132
|
-
prompt_data['quickmenu_grid'] = quickmenu_grid
|
|
2133
|
-
prompt_data['quickmenu_quickmenu'] = quickmenu_quickmenu
|
|
2134
|
-
# Keep legacy field in sync
|
|
2135
|
-
prompt_data['quick_run'] = quickmenu_quickmenu
|
|
2136
|
-
|
|
2137
|
-
# Check if filename changed - need to rename file
|
|
2138
|
-
if old_filename != name:
|
|
2139
|
-
from pathlib import Path
|
|
2140
|
-
old_path = Path(path)
|
|
2141
|
-
folder = str(old_path.parent) if old_path.parent != Path('.') else ''
|
|
2142
|
-
new_path = f"{folder}/{name}" if folder else name
|
|
2332
|
+
# Check if this is a new prompt or editing existing
|
|
2333
|
+
if hasattr(self, 'editor_current_path') and self.editor_current_path:
|
|
2334
|
+
path = self.editor_current_path
|
|
2143
2335
|
|
|
2144
|
-
#
|
|
2145
|
-
if
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2336
|
+
# Handle external prompts (save back to external file)
|
|
2337
|
+
if path.startswith("[EXTERNAL] "):
|
|
2338
|
+
external_file_path = path[11:] # Remove "[EXTERNAL] " prefix
|
|
2339
|
+
self._save_external_prompt(external_file_path, name, description, content)
|
|
2340
|
+
return
|
|
2341
|
+
|
|
2342
|
+
# Editing existing library prompt
|
|
2343
|
+
if path not in self.library.prompts:
|
|
2344
|
+
QMessageBox.warning(self.main_widget, "Error", "Prompt no longer exists")
|
|
2345
|
+
return
|
|
2346
|
+
|
|
2347
|
+
prompt_data = self.library.prompts[path].copy()
|
|
2348
|
+
old_filename = Path(path).name
|
|
2349
|
+
|
|
2350
|
+
# Extract name without extension for metadata
|
|
2351
|
+
name_without_ext = Path(name).stem
|
|
2352
|
+
|
|
2353
|
+
prompt_data['name'] = name_without_ext
|
|
2354
|
+
prompt_data['description'] = description
|
|
2355
|
+
prompt_data['content'] = content
|
|
2356
|
+
prompt_data['quickmenu_label'] = quickmenu_label or name_without_ext
|
|
2357
|
+
prompt_data['quickmenu_grid'] = quickmenu_grid
|
|
2358
|
+
prompt_data['sv_quickmenu'] = sv_quickmenu
|
|
2359
|
+
# Keep legacy field in sync
|
|
2360
|
+
prompt_data['quick_run'] = sv_quickmenu
|
|
2361
|
+
|
|
2362
|
+
# Check if filename changed - need to rename file
|
|
2363
|
+
if old_filename != name:
|
|
2364
|
+
old_path = Path(path)
|
|
2365
|
+
folder = str(old_path.parent) if old_path.parent != Path('.') else ''
|
|
2366
|
+
new_path = f"{folder}/{name}" if folder else name
|
|
2367
|
+
|
|
2368
|
+
# Delete old file and save to new location
|
|
2369
|
+
if self.library.delete_prompt(path):
|
|
2370
|
+
if self.library.save_prompt(new_path, prompt_data):
|
|
2371
|
+
self.library.load_all_prompts()
|
|
2372
|
+
self._refresh_tree()
|
|
2373
|
+
self._select_and_reveal_prompt(new_path)
|
|
2374
|
+
self.editor_current_path = new_path # Update to new path
|
|
2375
|
+
QMessageBox.information(self.main_widget, "Saved", f"Prompt renamed to '{name}' successfully!")
|
|
2376
|
+
self.log_message(f"✓ Renamed prompt: {old_filename} → {name}")
|
|
2377
|
+
else:
|
|
2378
|
+
QMessageBox.warning(self.main_widget, "Error", "Failed to rename prompt")
|
|
2153
2379
|
else:
|
|
2154
|
-
QMessageBox.warning(self.main_widget, "Error", "Failed to
|
|
2380
|
+
QMessageBox.warning(self.main_widget, "Error", "Failed to delete old prompt file")
|
|
2155
2381
|
else:
|
|
2156
|
-
|
|
2382
|
+
# Name unchanged, just update in place
|
|
2383
|
+
if self.library.save_prompt(path, prompt_data):
|
|
2384
|
+
QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
|
|
2385
|
+
self._refresh_tree()
|
|
2386
|
+
else:
|
|
2387
|
+
QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
|
|
2157
2388
|
else:
|
|
2158
|
-
#
|
|
2159
|
-
|
|
2160
|
-
|
|
2389
|
+
# Creating new prompt
|
|
2390
|
+
folder = getattr(self, 'editor_target_folder', 'Project Prompts')
|
|
2391
|
+
|
|
2392
|
+
# Create new prompt data
|
|
2393
|
+
from datetime import datetime
|
|
2394
|
+
prompt_data = {
|
|
2395
|
+
'name': name,
|
|
2396
|
+
'description': description,
|
|
2397
|
+
'content': content,
|
|
2398
|
+
'domain': '',
|
|
2399
|
+
'version': '1.0',
|
|
2400
|
+
'task_type': 'Translation',
|
|
2401
|
+
'favorite': False,
|
|
2402
|
+
# QuickMenu
|
|
2403
|
+
'quickmenu_label': quickmenu_label or name,
|
|
2404
|
+
'quickmenu_grid': quickmenu_grid,
|
|
2405
|
+
'sv_quickmenu': sv_quickmenu,
|
|
2406
|
+
# Legacy
|
|
2407
|
+
'quick_run': sv_quickmenu,
|
|
2408
|
+
'folder': folder,
|
|
2409
|
+
'tags': [],
|
|
2410
|
+
'created': datetime.now().strftime('%Y-%m-%d'),
|
|
2411
|
+
'modified': datetime.now().strftime('%Y-%m-%d')
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
# Create the prompt file (save_prompt creates new file if it doesn't exist)
|
|
2415
|
+
relative_path = f"{folder}/{name}.svprompt"
|
|
2416
|
+
if self.library.save_prompt(relative_path, prompt_data):
|
|
2417
|
+
QMessageBox.information(self.main_widget, "Created", f"Prompt '{name}' created successfully!")
|
|
2418
|
+
self.library.load_all_prompts() # Reload to get new prompt in memory
|
|
2161
2419
|
self._refresh_tree()
|
|
2420
|
+
self.editor_current_path = relative_path # Now editing this prompt
|
|
2162
2421
|
else:
|
|
2163
|
-
QMessageBox.warning(self.main_widget, "Error", "Failed to
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
'description': description,
|
|
2173
|
-
'content': content,
|
|
2174
|
-
'domain': '',
|
|
2175
|
-
'version': '1.0',
|
|
2176
|
-
'task_type': 'Translation',
|
|
2177
|
-
'favorite': False,
|
|
2178
|
-
# QuickMenu
|
|
2179
|
-
'quickmenu_label': quickmenu_label or name,
|
|
2180
|
-
'quickmenu_grid': quickmenu_grid,
|
|
2181
|
-
'quickmenu_quickmenu': quickmenu_quickmenu,
|
|
2182
|
-
# Legacy
|
|
2183
|
-
'quick_run': quickmenu_quickmenu,
|
|
2184
|
-
'folder': folder,
|
|
2185
|
-
'tags': [],
|
|
2186
|
-
'created': datetime.now().strftime('%Y-%m-%d'),
|
|
2187
|
-
'modified': datetime.now().strftime('%Y-%m-%d')
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
# Create the prompt file (save_prompt creates new file if it doesn't exist)
|
|
2191
|
-
relative_path = f"{folder}/{name}.svprompt"
|
|
2192
|
-
if self.library.save_prompt(relative_path, prompt_data):
|
|
2193
|
-
QMessageBox.information(self.main_widget, "Created", f"Prompt '{name}' created successfully!")
|
|
2194
|
-
self.library.load_all_prompts() # Reload to get new prompt in memory
|
|
2195
|
-
self._refresh_tree()
|
|
2196
|
-
self.editor_current_path = relative_path # Now editing this prompt
|
|
2197
|
-
else:
|
|
2198
|
-
QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
|
|
2422
|
+
QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
|
|
2423
|
+
|
|
2424
|
+
except Exception as e:
|
|
2425
|
+
import traceback
|
|
2426
|
+
error_msg = f"Prompt save error: {str(e)}\n{traceback.format_exc()}"
|
|
2427
|
+
print(f"[ERROR] {error_msg}")
|
|
2428
|
+
self.log_message(f"❌ Prompt save error: {str(e)}")
|
|
2429
|
+
QMessageBox.critical(self.main_widget, "Save Error", f"Failed to save prompt:\n\n{str(e)}")
|
|
2430
|
+
return
|
|
2199
2431
|
|
|
2200
2432
|
def _save_external_prompt(self, file_path: str, name: str, description: str, content: str):
|
|
2201
2433
|
"""Save changes to an external prompt file"""
|
|
@@ -2421,7 +2653,7 @@ class UnifiedPromptManagerQt:
|
|
|
2421
2653
|
'favorite': False,
|
|
2422
2654
|
'quickmenu_label': name_without_ext,
|
|
2423
2655
|
'quickmenu_grid': False,
|
|
2424
|
-
'
|
|
2656
|
+
'sv_quickmenu': False,
|
|
2425
2657
|
'quick_run': False,
|
|
2426
2658
|
'folder': folder_path,
|
|
2427
2659
|
'tags': [],
|
|
@@ -2437,6 +2669,7 @@ class UnifiedPromptManagerQt:
|
|
|
2437
2669
|
self._refresh_tree()
|
|
2438
2670
|
self._select_and_reveal_prompt(relative_path)
|
|
2439
2671
|
self._load_prompt_in_editor(relative_path)
|
|
2672
|
+
self.btn_save_prompt.setEnabled(True) # Ensure Save button is enabled for new prompt
|
|
2440
2673
|
self.log_message(f"✓ Created new prompt '{name}' in folder: {folder_path}")
|
|
2441
2674
|
else:
|
|
2442
2675
|
QMessageBox.warning(self.main_widget, "Error", "Failed to create prompt")
|
|
@@ -2469,7 +2702,7 @@ class UnifiedPromptManagerQt:
|
|
|
2469
2702
|
src_data['favorite'] = False
|
|
2470
2703
|
src_data['quick_run'] = False
|
|
2471
2704
|
src_data['quickmenu_grid'] = False
|
|
2472
|
-
src_data['
|
|
2705
|
+
src_data['sv_quickmenu'] = False
|
|
2473
2706
|
src_data['folder'] = folder
|
|
2474
2707
|
src_data['created'] = datetime.now().strftime('%Y-%m-%d')
|
|
2475
2708
|
src_data['modified'] = datetime.now().strftime('%Y-%m-%d')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supervertaler
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.128
|
|
4
4
|
Summary: Professional AI-powered translation workbench with multi-LLM support, glossary system, TM, spellcheck, voice commands, and PyQt6 interface. Batteries included (core).
|
|
5
5
|
Home-page: https://supervertaler.com
|
|
6
6
|
Author: Michael Beijer
|
|
@@ -71,7 +71,7 @@ Dynamic: home-page
|
|
|
71
71
|
Dynamic: license-file
|
|
72
72
|
Dynamic: requires-python
|
|
73
73
|
|
|
74
|
-
# 🚀 Supervertaler v1.9.
|
|
74
|
+
# 🚀 Supervertaler v1.9.125
|
|
75
75
|
|
|
76
76
|
[](https://pypi.org/project/Supervertaler/)
|
|
77
77
|
[](https://www.python.org/downloads/)
|
|
@@ -79,7 +79,37 @@ Dynamic: requires-python
|
|
|
79
79
|
|
|
80
80
|
AI-enhanced CAT tool with multi-LLM support (GPT-4, Claude, Gemini, Ollama), innovative Superlookup concordance system offering access to multiple terminology sources (TMs, glossaries, web resources, etc.), and seamless CAT tool integration (memoQ, Trados, CafeTran, Phrase).
|
|
81
81
|
|
|
82
|
-
**Current Version:** v1.9.
|
|
82
|
+
**Current Version:** v1.9.125 (January 19, 2026)
|
|
83
|
+
|
|
84
|
+
### FIXED in v1.9.125 - 🐛 Prompt Save Crash
|
|
85
|
+
|
|
86
|
+
**Critical Fix:** Wrapped prompt save logic in comprehensive error handling to prevent silent crashes. Now shows detailed error messages if save fails instead of crashing the app.
|
|
87
|
+
|
|
88
|
+
### ADDED in v1.9.124 - 📄 QuickMenu Document Context
|
|
89
|
+
|
|
90
|
+
**Context-Aware AI Suggestions:** QuickMenu prompts can now access the full document context! Use the new `{{DOCUMENT_CONTEXT}}` placeholder to give the AI access to your project's source segments. Configure what percentage of segments to include (0-100%, default 50%) in Settings → AI Settings. Perfect for: "Suggest the best translation of '{{SELECTION}}' within the context of this project."
|
|
91
|
+
|
|
92
|
+
### FIXED in v1.9.123 - 🤖 QuickMenu Generic AI Support
|
|
93
|
+
|
|
94
|
+
**QuickMenu Now Works for Any Task:** Fixed bug where QuickMenu prompts were forced into translation mode. Now supports any AI task: explain terms, define concepts, suggest multiple options, analyze tone, etc.
|
|
95
|
+
|
|
96
|
+
### ENHANCED in v1.9.122 - ⌨️ Ctrl+N for Quick Notes
|
|
97
|
+
|
|
98
|
+
**Faster Note-Taking:** Ctrl+N now focuses the Segment Note tab and places your cursor in the notes field, ready to type. Perfect for quick proofreading notes, context reminders, or translation decisions.
|
|
99
|
+
|
|
100
|
+
### FIXED in v1.9.121 - 🐛 Find & Replace Performance
|
|
101
|
+
|
|
102
|
+
**Critical Fix:** v1.9.120 accidentally made Find & Replace slower (37+ seconds). v1.9.121 actually fixes it by updating cells in-place instead of recreating all widgets. Now near-instant!
|
|
103
|
+
|
|
104
|
+
### OPTIMIZED in v1.9.120 - ⚡ Find & Replace Speed (BUGGY - USE v1.9.121)
|
|
105
|
+
|
|
106
|
+
### ADDED in v1.9.119 - ⌨️ Alt+D Dictionary Shortcut
|
|
107
|
+
|
|
108
|
+
**Quick Dictionary Addition:** Press Alt+D to instantly add misspelled words to your custom dictionary without using the right-click menu.
|
|
109
|
+
|
|
110
|
+
### FIXED in v1.9.117 - 🐛 Glossary Punctuation Matching
|
|
111
|
+
|
|
112
|
+
**Critical Fix:** Glossary entries with trailing punctuation (periods, quotes, etc.) now match correctly! Previously, "sentence." in glossary wouldn't match "sentence." in source text due to tokenization stripping punctuation from source but not from glossary entries.
|
|
83
113
|
|
|
84
114
|
### FIXED in v1.9.116 - 🐛 Tab Navigation & Startup
|
|
85
115
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Supervertaler.py,sha256=
|
|
1
|
+
Supervertaler.py,sha256=SZo1QhKtxynjmTAgmUXA5js44gwWsIEZidWgrccrXp0,2137119
|
|
2
2
|
modules/__init__.py,sha256=G58XleS-EJ2sX4Kehm-3N2m618_W2Es0Kg8CW_eBG7g,327
|
|
3
3
|
modules/ai_actions.py,sha256=i5MJcM-7Y6CAvKUwxmxrVHeoZAVtAP7aRDdWM5KLkO0,33877
|
|
4
4
|
modules/ai_attachment_manager.py,sha256=mA5ISI22qN9mH3DQFF4gOTciDyBt5xVR7sHTkgkTIlw,11361
|
|
@@ -41,7 +41,7 @@ modules/quick_access_sidebar.py,sha256=RPn5ssvYXlitNMWFZN9Yxv7So8u_z5RGNpHN6N-SF
|
|
|
41
41
|
modules/ribbon_widget.py,sha256=QNGKxmit_oM5C5nJViadYYEzeRlIdIsla8Bzu_RNGO0,21990
|
|
42
42
|
modules/sdlppx_handler.py,sha256=o6Rj_T0B94toiYlvDDwMYLSz4q6kANgegFaDK5i3yhs,33538
|
|
43
43
|
modules/setup_wizard.py,sha256=1prK5GPrUU4U4CqdT3G1RA3iy8FG1Z4jgPmPPZXOOEA,13115
|
|
44
|
-
modules/shortcut_manager.py,sha256
|
|
44
|
+
modules/shortcut_manager.py,sha256=h8a9-ENKpXGvdTHFcemOH3GRYQVqGRQnHnQAP7xgqnY,31305
|
|
45
45
|
modules/simple_segmenter.py,sha256=-V7L-tjajW1M3DADxvcYEgBu0VLJvmRQl6VB9OshiuM,4480
|
|
46
46
|
modules/spellcheck_manager.py,sha256=jwduHJ66pOKv1MtzSAltxpP8LPgz11FvF6j8h7BiRZY,27592
|
|
47
47
|
modules/statuses.py,sha256=t6TCA9pNZHDw3SbKTxT73uKezJhwWk9gFLr0NOgEufs,6911
|
|
@@ -59,7 +59,7 @@ modules/term_extractor.py,sha256=qPvKNCVXFTGEGwXNvvC0cfCmdb5c3WhzE38EOgKdKUI,112
|
|
|
59
59
|
modules/termbase_entry_editor.py,sha256=iWO9CgLjMomGAqBXDsGAX7TFJvDOp2s_taS4gBL1rZY,35818
|
|
60
60
|
modules/termbase_import_export.py,sha256=16IAY04IS_rgt0GH5UOUzUI5NoqAli4JMfMquxmFBm0,23552
|
|
61
61
|
modules/termbase_manager.py,sha256=-PlGF6fIA7KYCteoQ8FZ_0SQZNRRBFAtLimHPbmhQ6w,44544
|
|
62
|
-
modules/termview_widget.py,sha256=
|
|
62
|
+
modules/termview_widget.py,sha256=KNzgQ7dEFW5ANcbCsUPHJ268MYwBmV5MQG-9GQJoobY,52745
|
|
63
63
|
modules/theme_manager.py,sha256=EOI_5pM2bXAadw08bbl92TLN-w28lbw4Zi1E8vQ-kM0,16694
|
|
64
64
|
modules/tm_editor_dialog.py,sha256=AzGwq4QW641uFJdF8DljLTRRp4FLoYX3Pe4rlTjQWNg,3517
|
|
65
65
|
modules/tm_manager_qt.py,sha256=h2bvXkRuboHf_RRz9-5FX35GVRlpXgRDWeXyj1QWtPs,54406
|
|
@@ -72,14 +72,14 @@ modules/trados_docx_handler.py,sha256=VPRAQ73cUHs_SEj6x81z1PmSxfjnwPBp9P4fXeK3Kp
|
|
|
72
72
|
modules/translation_memory.py,sha256=U-deNPybG2PKeEt2LSTZv0Ziu5VwxkH2BsuCCXbpalc,26453
|
|
73
73
|
modules/translation_results_panel.py,sha256=DmEe0pZRSfcZFg2cWeEREK7H9vrTcPkgeuMW54Pgrys,92505
|
|
74
74
|
modules/translation_services.py,sha256=lyVpWuZK1wtVtYZMDMdLoq1DHBoSaeAnp-Yejb0TlVQ,10530
|
|
75
|
-
modules/unified_prompt_library.py,sha256=
|
|
76
|
-
modules/unified_prompt_manager_qt.py,sha256=
|
|
75
|
+
modules/unified_prompt_library.py,sha256=L3do_T-7NHnC5CxJ1TDq63iVM60XLkvovDJ9g4cJmZQ,26016
|
|
76
|
+
modules/unified_prompt_manager_qt.py,sha256=2EvOEryM699xJHIYJrqRSPUqpypnVJOErc-W0PfjaaQ,170988
|
|
77
77
|
modules/voice_commands.py,sha256=iBb-gjWxRMLhFH7-InSRjYJz1EIDBNA2Pog8V7TtJaY,38516
|
|
78
78
|
modules/voice_dictation.py,sha256=QmitXfkG-vRt5hIQATjphHdhXfqmwhzcQcbXB6aRzIg,16386
|
|
79
79
|
modules/voice_dictation_lite.py,sha256=jorY0BmWE-8VczbtGrWwt1zbnOctMoSlWOsQrcufBcc,9423
|
|
80
|
-
supervertaler-1.9.
|
|
81
|
-
supervertaler-1.9.
|
|
82
|
-
supervertaler-1.9.
|
|
83
|
-
supervertaler-1.9.
|
|
84
|
-
supervertaler-1.9.
|
|
85
|
-
supervertaler-1.9.
|
|
80
|
+
supervertaler-1.9.128.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
|
|
81
|
+
supervertaler-1.9.128.dist-info/METADATA,sha256=TeUD7EbvGmLtukvAIep4czAC7P8YNm4wLoGH244ug8U,42109
|
|
82
|
+
supervertaler-1.9.128.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
83
|
+
supervertaler-1.9.128.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
|
|
84
|
+
supervertaler-1.9.128.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
|
|
85
|
+
supervertaler-1.9.128.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|