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 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.114 (AI Assistant diagnostic logging)
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.114"
37
+ __version__ = "1.9.128"
38
38
  __phase__ = "0.9"
39
- __release_date__ = "2026-01-18"
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(3) # Settings tab
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(2) if hasattr(self, 'main_tabs') else None)
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(3) if hasattr(self, 'main_tabs') else None)
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
- settings_tabs.addTab(scroll_area_wrapper(ai_tab), "🤖 AI Settings")
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 source_term for char in ['.', '%', ',', '-', '/']):
31038
- pattern = re.compile(r'(?<!\w)' + re.escape(source_term.lower()) + r'(?!\w)')
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(source_term.lower()) + r"\b")
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
- # Run each operation
31517
- total_replaced = 0
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
- # Refresh grid
31524
- self.load_segments_to_grid()
31525
- self.project_modified = True
31526
- self.update_window_title()
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
- # Perform replacements
31910
- import re
31911
- replaced_count = 0
32050
+ # OPTIMIZATION: Disable UI updates during batch replacement
32051
+ self.table.setUpdatesEnabled(False)
31912
32052
 
31913
- for row, col in self.find_matches:
31914
- segment = self.current_project.segments[row]
31915
-
31916
- # Get the appropriate field
31917
- if col == 2: # Source
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
- # Perform replacement
31923
- if match_mode == 2: # Entire segment
31924
- new_text = replace_text
31925
- else:
31926
- if case_sensitive:
31927
- new_text = old_text.replace(find_text, replace_text)
31928
- else:
31929
- pattern = re.escape(find_text)
31930
- new_text = re.sub(pattern, replace_text, old_text, flags=re.IGNORECASE)
31931
-
31932
- if new_text != old_text:
31933
- replaced_count += 1
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
- old_target = segment.target
31939
- old_status = segment.status
31940
- segment.target = new_text
31941
- # Record undo state for find/replace operation
31942
- self.record_undo_state(segment.id, old_target, new_text, old_status, old_status)
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
- # Update table
31945
- item = self.table.item(row, col)
31946
- if item:
31947
- item.setText(new_text)
31948
-
31949
- self.project_modified = True
31950
- self.update_window_title()
31951
-
31952
- # Clear matches and reload
31953
- self.find_matches = []
31954
- self.load_segments_to_grid()
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, Project resources=1, Tools=2, Settings=3
35948
- self.main_tabs.setCurrentIndex(3)
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 complete translation prompt using the chosen QuickMenu prompt as PRIMARY instructions."""
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
- mode = getattr(pm, 'current_mode', None) or "single"
37557
- system_template = pm.get_system_template(mode)
37558
-
37559
- system_template = system_template.replace("{{SOURCE_LANGUAGE}}", source_lang)
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
- library_prompts = "\n\n# PRIMARY INSTRUCTIONS\n\n" + prompt_content
37564
-
37565
- # Keep any globally attached prompts as additional instructions
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
- for attached_content in lib.attached_prompts:
37568
- library_prompts += "\n\n# ADDITIONAL INSTRUCTIONS\n\n" + (attached_content or "")
37569
- except Exception:
37570
- pass
37571
-
37572
- final_prompt = system_template + library_prompts
37573
- final_prompt += "\n\n**YOUR TRANSLATION (provide ONLY the translated text, no numbering or labels):**\n"
37574
- return final_prompt
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=input_text,
37656
- source_lang=source_lang,
37657
- target_lang=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 2)
39790
+ # AutoFingers is in Tools tab (main_tabs index 3)
39557
39791
  if hasattr(self, 'main_tabs'):
39558
- self.main_tabs.setCurrentIndex(2) # Switch to Tools tab
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 2)
44384
- # Tab structure: Grid=0, Project resources=1, Tools=2, Settings=3
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(2) # Tools tab is at index 2
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: