supervertaler 1.9.116__py3-none-any.whl → 1.9.131__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 +438 -103
- modules/shortcut_manager.py +29 -1
- modules/termview_widget.py +9 -2
- modules/unified_prompt_library.py +12 -9
- modules/unified_prompt_manager_qt.py +355 -112
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.131.dist-info}/METADATA +37 -3
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.131.dist-info}/RECORD +11 -11
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.131.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.131.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.131.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.116.dist-info → supervertaler-1.9.131.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.131"
|
|
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
|
|
@@ -123,10 +123,10 @@ try:
|
|
|
123
123
|
QButtonGroup, QDialogButtonBox, QTabWidget, QGroupBox, QGridLayout, QCheckBox,
|
|
124
124
|
QProgressBar, QProgressDialog, QFormLayout, QTabBar, QPlainTextEdit, QAbstractItemDelegate,
|
|
125
125
|
QFrame, QListWidget, QListWidgetItem, QStackedWidget, QTreeWidget, QTreeWidgetItem,
|
|
126
|
-
QScrollArea, QSizePolicy, QSlider, QToolButton
|
|
126
|
+
QScrollArea, QSizePolicy, QSlider, QToolButton, QAbstractItemView
|
|
127
127
|
)
|
|
128
128
|
from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal, QObject, QUrl
|
|
129
|
-
from PyQt6.QtGui import QFont, QAction, QKeySequence, QIcon, QTextOption, QColor, QDesktopServices, QTextCharFormat, QTextCursor, QBrush, QSyntaxHighlighter, QPalette, QTextBlockFormat
|
|
129
|
+
from PyQt6.QtGui import QFont, QAction, QKeySequence, QIcon, QTextOption, QColor, QDesktopServices, QTextCharFormat, QTextCursor, QBrush, QSyntaxHighlighter, QPalette, QTextBlockFormat, QCursor
|
|
130
130
|
from PyQt6.QtWidgets import QStyleOptionViewItem, QStyle
|
|
131
131
|
from PyQt6.QtCore import QRectF
|
|
132
132
|
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
|
@@ -170,11 +170,11 @@ def cleanup_ahk_process():
|
|
|
170
170
|
try:
|
|
171
171
|
_ahk_process.terminate()
|
|
172
172
|
_ahk_process.wait(timeout=1)
|
|
173
|
-
print("[
|
|
173
|
+
print("[Hotkeys] AHK process terminated on exit")
|
|
174
174
|
except:
|
|
175
175
|
try:
|
|
176
176
|
_ahk_process.kill()
|
|
177
|
-
print("[
|
|
177
|
+
print("[Hotkeys] AHK process killed on exit")
|
|
178
178
|
except:
|
|
179
179
|
pass
|
|
180
180
|
|
|
@@ -910,6 +910,7 @@ class _CtrlReturnEventFilter(QObject):
|
|
|
910
910
|
|
|
911
911
|
return False
|
|
912
912
|
|
|
913
|
+
|
|
913
914
|
class GridTableEventFilter:
|
|
914
915
|
"""Mixin to pass keyboard shortcuts from editor to table"""
|
|
915
916
|
pass
|
|
@@ -2559,7 +2560,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
2559
2560
|
menu.addSeparator()
|
|
2560
2561
|
|
|
2561
2562
|
# Add to dictionary action
|
|
2562
|
-
add_to_dict_action = QAction(f"📖 Add '{misspelled_word}' to Dictionary", self)
|
|
2563
|
+
add_to_dict_action = QAction(f"📖 Add '{misspelled_word}' to Dictionary (Alt+D)", self)
|
|
2563
2564
|
add_to_dict_action.triggered.connect(
|
|
2564
2565
|
lambda checked, w=misspelled_word: self._add_to_dictionary(w)
|
|
2565
2566
|
)
|
|
@@ -5592,6 +5593,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
5592
5593
|
self.termview_shortcuts = []
|
|
5593
5594
|
self._termview_last_key = None # Track last key for double-tap detection
|
|
5594
5595
|
self._termview_last_time = 0 # Track timing for double-tap
|
|
5596
|
+
|
|
5597
|
+
# Double-tap Shift detection for context menu
|
|
5598
|
+
self._shift_last_press_time = 0
|
|
5599
|
+
self._shift_double_tap_threshold = 0.35 # 350ms window
|
|
5600
|
+
|
|
5595
5601
|
for i in range(0, 10): # 0-9
|
|
5596
5602
|
shortcut_id = f"termview_insert_{i}"
|
|
5597
5603
|
default_key = f"Alt+{i}"
|
|
@@ -5635,6 +5641,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
5635
5641
|
self._ctrl_return_event_filter = _CtrlReturnEventFilter(self)
|
|
5636
5642
|
QApplication.instance().installEventFilter(self._ctrl_return_event_filter)
|
|
5637
5643
|
|
|
5644
|
+
# Note: Double-tap Shift for context menu is handled by AutoHotkey (double_shift_menu.ahk)
|
|
5645
|
+
# Qt's event system makes reliable double-tap detection difficult in Python
|
|
5646
|
+
|
|
5638
5647
|
# Ctrl+Shift+Enter - Always confirm all selected segments
|
|
5639
5648
|
create_shortcut("editor_confirm_selected", "Ctrl+Shift+Return", self.confirm_selected_segments)
|
|
5640
5649
|
|
|
@@ -5668,6 +5677,84 @@ class SupervertalerQt(QMainWindow):
|
|
|
5668
5677
|
|
|
5669
5678
|
# Ctrl+Shift+2 - Quick add term with Priority 2
|
|
5670
5679
|
create_shortcut("editor_quick_add_priority_2", "Ctrl+Shift+2", lambda: self._quick_add_term_with_priority(2))
|
|
5680
|
+
|
|
5681
|
+
# Alt+D - Add word at cursor to dictionary
|
|
5682
|
+
create_shortcut("editor_add_to_dictionary", "Alt+D", self.add_word_to_dictionary_shortcut)
|
|
5683
|
+
|
|
5684
|
+
# Ctrl+N - Focus Segment Note tab
|
|
5685
|
+
create_shortcut("editor_focus_notes", "Ctrl+N", self.focus_segment_notes)
|
|
5686
|
+
|
|
5687
|
+
# Alt+K - Open QuickMenu directly
|
|
5688
|
+
create_shortcut("editor_open_quickmenu", "Alt+K", self.open_quickmenu)
|
|
5689
|
+
|
|
5690
|
+
def focus_segment_notes(self):
|
|
5691
|
+
"""Switch to Segment Note tab and focus the notes editor so user can start typing immediately"""
|
|
5692
|
+
if not hasattr(self, 'bottom_tabs'):
|
|
5693
|
+
return
|
|
5694
|
+
|
|
5695
|
+
# Switch to Segment note tab (index 1)
|
|
5696
|
+
self.bottom_tabs.setCurrentIndex(1)
|
|
5697
|
+
|
|
5698
|
+
# Focus the notes editor so user can start typing
|
|
5699
|
+
if hasattr(self, 'bottom_notes_edit'):
|
|
5700
|
+
self.bottom_notes_edit.setFocus()
|
|
5701
|
+
|
|
5702
|
+
def open_quickmenu(self):
|
|
5703
|
+
"""Open QuickMenu popup at current cursor position for quick AI prompt selection.
|
|
5704
|
+
|
|
5705
|
+
User can navigate with arrow keys and press Enter to select a prompt.
|
|
5706
|
+
"""
|
|
5707
|
+
try:
|
|
5708
|
+
# Get QuickMenu items from prompt library
|
|
5709
|
+
quickmenu_items = []
|
|
5710
|
+
if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt:
|
|
5711
|
+
lib = getattr(self.prompt_manager_qt, 'library', None)
|
|
5712
|
+
if lib and hasattr(lib, 'get_quickmenu_grid_prompts'):
|
|
5713
|
+
quickmenu_items = lib.get_quickmenu_grid_prompts() or []
|
|
5714
|
+
|
|
5715
|
+
if not quickmenu_items:
|
|
5716
|
+
self.log("⚠️ No QuickMenu prompts available. Add prompts with 'Show in Supervertaler QuickMenu' enabled.")
|
|
5717
|
+
return
|
|
5718
|
+
|
|
5719
|
+
# Find the currently focused widget (source or target cell)
|
|
5720
|
+
focus_widget = QApplication.focusWidget()
|
|
5721
|
+
|
|
5722
|
+
# Build the menu
|
|
5723
|
+
menu = QMenu(self)
|
|
5724
|
+
menu.setTitle("⚡ QuickMenu")
|
|
5725
|
+
|
|
5726
|
+
for rel_path, label in sorted(quickmenu_items, key=lambda x: (x[1] or x[0]).lower()):
|
|
5727
|
+
prompt_menu = menu.addMenu(label or rel_path)
|
|
5728
|
+
|
|
5729
|
+
run_show = QAction("▶ Run (show response)…", self)
|
|
5730
|
+
run_show.triggered.connect(
|
|
5731
|
+
lambda checked=False, p=rel_path, w=focus_widget: self.run_grid_quickmenu_prompt(p, origin_widget=w, behavior="show")
|
|
5732
|
+
)
|
|
5733
|
+
prompt_menu.addAction(run_show)
|
|
5734
|
+
|
|
5735
|
+
run_replace = QAction("↺ Run and replace target selection", self)
|
|
5736
|
+
run_replace.triggered.connect(
|
|
5737
|
+
lambda checked=False, p=rel_path, w=focus_widget: self.run_grid_quickmenu_prompt(p, origin_widget=w, behavior="replace")
|
|
5738
|
+
)
|
|
5739
|
+
prompt_menu.addAction(run_replace)
|
|
5740
|
+
|
|
5741
|
+
# Show menu at cursor position (or center of focused widget)
|
|
5742
|
+
if focus_widget:
|
|
5743
|
+
# Get cursor rectangle if it's a text editor
|
|
5744
|
+
if hasattr(focus_widget, 'cursorRect'):
|
|
5745
|
+
cursor_rect = focus_widget.cursorRect()
|
|
5746
|
+
pos = focus_widget.mapToGlobal(cursor_rect.bottomLeft())
|
|
5747
|
+
else:
|
|
5748
|
+
# Fallback to center of widget
|
|
5749
|
+
pos = focus_widget.mapToGlobal(focus_widget.rect().center())
|
|
5750
|
+
else:
|
|
5751
|
+
# Fallback to mouse cursor position
|
|
5752
|
+
pos = QCursor.pos()
|
|
5753
|
+
|
|
5754
|
+
menu.exec(pos)
|
|
5755
|
+
|
|
5756
|
+
except Exception as e:
|
|
5757
|
+
self.log(f"❌ Error opening QuickMenu: {e}")
|
|
5671
5758
|
|
|
5672
5759
|
def refresh_shortcut_enabled_states(self):
|
|
5673
5760
|
"""Refresh enabled/disabled states and key bindings of all global shortcuts from shortcut manager.
|
|
@@ -10836,6 +10923,38 @@ class SupervertalerQt(QMainWindow):
|
|
|
10836
10923
|
self._play_sound_effect('glossary_term_error')
|
|
10837
10924
|
self.statusBar().showMessage(f"Error adding term: {e}", 3000)
|
|
10838
10925
|
|
|
10926
|
+
def add_word_to_dictionary_shortcut(self):
|
|
10927
|
+
"""Add word at cursor position to custom dictionary (Alt+D shortcut)
|
|
10928
|
+
|
|
10929
|
+
Finds the misspelled word at the current cursor position and adds it to the
|
|
10930
|
+
custom dictionary. Works when focus is in a grid target cell.
|
|
10931
|
+
"""
|
|
10932
|
+
# Get currently focused widget
|
|
10933
|
+
focused_widget = QApplication.focusWidget()
|
|
10934
|
+
|
|
10935
|
+
# Check if we're in an editable grid cell
|
|
10936
|
+
if not isinstance(focused_widget, EditableGridTextEditor):
|
|
10937
|
+
self.statusBar().showMessage("Alt+D: Place cursor on a misspelled word in the target cell first", 3000)
|
|
10938
|
+
return
|
|
10939
|
+
|
|
10940
|
+
# Get cursor position
|
|
10941
|
+
cursor = focused_widget.textCursor()
|
|
10942
|
+
|
|
10943
|
+
# Try to find misspelled word at cursor
|
|
10944
|
+
word_info = focused_widget._get_misspelled_word_at_cursor(cursor)
|
|
10945
|
+
|
|
10946
|
+
if word_info[0] is None:
|
|
10947
|
+
# No misspelled word found at cursor
|
|
10948
|
+
self.statusBar().showMessage("No misspelled word at cursor position", 3000)
|
|
10949
|
+
return
|
|
10950
|
+
|
|
10951
|
+
word, start_pos, end_pos = word_info
|
|
10952
|
+
|
|
10953
|
+
# Add to dictionary
|
|
10954
|
+
focused_widget._add_to_dictionary(word)
|
|
10955
|
+
|
|
10956
|
+
# Status message already shown by _add_to_dictionary
|
|
10957
|
+
|
|
10839
10958
|
def add_text_to_non_translatables(self, text: str):
|
|
10840
10959
|
"""Add selected text to active non-translatable list(s)"""
|
|
10841
10960
|
if not text or not text.strip():
|
|
@@ -13956,6 +14075,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
13956
14075
|
full_context_cb.stateChanged.connect(lambda: context_slider.setEnabled(full_context_cb.isChecked()))
|
|
13957
14076
|
update_context_label(context_window_size)
|
|
13958
14077
|
|
|
14078
|
+
prefs_layout.addSpacing(10)
|
|
14079
|
+
|
|
14080
|
+
# QuickMenu document context
|
|
14081
|
+
quickmenu_context_label = QLabel("<b>QuickMenu Document Context:</b>")
|
|
14082
|
+
prefs_layout.addWidget(quickmenu_context_label)
|
|
14083
|
+
|
|
14084
|
+
quickmenu_context_percent = general_prefs.get('quickmenu_context_percent', 50)
|
|
14085
|
+
quickmenu_context_layout = QHBoxLayout()
|
|
14086
|
+
quickmenu_context_layout.addWidget(QLabel(" Document context size:"))
|
|
14087
|
+
quickmenu_context_slider = QSlider(Qt.Orientation.Horizontal)
|
|
14088
|
+
quickmenu_context_slider.setMinimum(0)
|
|
14089
|
+
quickmenu_context_slider.setMaximum(100)
|
|
14090
|
+
quickmenu_context_slider.setValue(quickmenu_context_percent)
|
|
14091
|
+
quickmenu_context_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
14092
|
+
quickmenu_context_slider.setTickInterval(10)
|
|
14093
|
+
quickmenu_context_layout.addWidget(quickmenu_context_slider)
|
|
14094
|
+
quickmenu_context_value_label = QLabel(f"{quickmenu_context_percent}%")
|
|
14095
|
+
quickmenu_context_value_label.setMinimumWidth(60)
|
|
14096
|
+
quickmenu_context_layout.addWidget(quickmenu_context_value_label)
|
|
14097
|
+
quickmenu_context_layout.addStretch()
|
|
14098
|
+
prefs_layout.addLayout(quickmenu_context_layout)
|
|
14099
|
+
|
|
14100
|
+
quickmenu_context_info = QLabel(
|
|
14101
|
+
" ⓘ When using {{SOURCE+TARGET_CONTEXT}} or {{SOURCE_CONTEXT}} placeholders in QuickMenu prompts.\n"
|
|
14102
|
+
" 0% = disabled, 50% = half the document (default), 100% = entire document.\n"
|
|
14103
|
+
" Limit: maximum 100 segments for performance."
|
|
14104
|
+
)
|
|
14105
|
+
quickmenu_context_info.setStyleSheet("font-size: 9pt; color: #666; padding-left: 20px;")
|
|
14106
|
+
quickmenu_context_info.setWordWrap(True)
|
|
14107
|
+
prefs_layout.addWidget(quickmenu_context_info)
|
|
14108
|
+
|
|
14109
|
+
def update_quickmenu_context_label(value):
|
|
14110
|
+
if value == 0:
|
|
14111
|
+
quickmenu_context_value_label.setText("0% (disabled)")
|
|
14112
|
+
else:
|
|
14113
|
+
quickmenu_context_value_label.setText(f"{value}%")
|
|
14114
|
+
|
|
14115
|
+
quickmenu_context_slider.valueChanged.connect(update_quickmenu_context_label)
|
|
14116
|
+
|
|
13959
14117
|
prefs_layout.addSpacing(5)
|
|
13960
14118
|
|
|
13961
14119
|
# Check TM before API call
|
|
@@ -14100,7 +14258,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
14100
14258
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
14101
14259
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
14102
14260
|
ollama_keepwarm_cb,
|
|
14103
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
14261
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
14262
|
+
quickmenu_context_slider
|
|
14104
14263
|
))
|
|
14105
14264
|
layout.addWidget(save_btn)
|
|
14106
14265
|
|
|
@@ -17098,7 +17257,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
17098
17257
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
17099
17258
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
17100
17259
|
ollama_keepwarm_cb,
|
|
17101
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
17260
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
17261
|
+
quickmenu_context_slider):
|
|
17102
17262
|
"""Save all AI settings from the unified AI Settings tab"""
|
|
17103
17263
|
# Determine selected provider
|
|
17104
17264
|
if openai_radio.isChecked():
|
|
@@ -17157,6 +17317,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
17157
17317
|
general_prefs['surrounding_segments'] = surrounding_spin.value()
|
|
17158
17318
|
general_prefs['use_full_context'] = full_context_cb.isChecked()
|
|
17159
17319
|
general_prefs['context_window_size'] = context_slider.value()
|
|
17320
|
+
general_prefs['quickmenu_context_percent'] = quickmenu_context_slider.value()
|
|
17160
17321
|
general_prefs['check_tm_before_api'] = check_tm_cb.isChecked()
|
|
17161
17322
|
general_prefs['auto_propagate_100'] = auto_propagate_cb.isChecked()
|
|
17162
17323
|
general_prefs['lookup_delay'] = delay_spin.value()
|
|
@@ -28206,6 +28367,20 @@ class SupervertalerQt(QMainWindow):
|
|
|
28206
28367
|
# Full processing for non-arrow-key navigation (click, etc.)
|
|
28207
28368
|
self._on_cell_selected_full(current_row, current_col, previous_row, previous_col)
|
|
28208
28369
|
|
|
28370
|
+
def _center_row_in_viewport(self, row: int):
|
|
28371
|
+
"""Center the given row vertically in the visible table viewport.
|
|
28372
|
+
|
|
28373
|
+
Uses Qt's built-in scrollTo() with PositionAtCenter hint.
|
|
28374
|
+
"""
|
|
28375
|
+
if row < 0 or row >= self.table.rowCount():
|
|
28376
|
+
return
|
|
28377
|
+
|
|
28378
|
+
# Get the model index for any cell in this row (use column 0)
|
|
28379
|
+
index = self.table.model().index(row, 0)
|
|
28380
|
+
if index.isValid():
|
|
28381
|
+
# Use Qt's built-in centering - PositionAtCenter puts the item in the center of the viewport
|
|
28382
|
+
self.table.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter)
|
|
28383
|
+
|
|
28209
28384
|
def _on_cell_selected_minimal(self, current_row, previous_row):
|
|
28210
28385
|
"""Minimal UI update for fast arrow key navigation - just highlight and scroll"""
|
|
28211
28386
|
try:
|
|
@@ -28227,7 +28402,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
28227
28402
|
|
|
28228
28403
|
# Auto-center if enabled
|
|
28229
28404
|
if getattr(self, 'auto_center_active_segment', False) and not getattr(self, 'filtering_active', False):
|
|
28230
|
-
self.
|
|
28405
|
+
self._center_row_in_viewport(current_row)
|
|
28231
28406
|
except Exception as e:
|
|
28232
28407
|
if self.debug_mode_enabled:
|
|
28233
28408
|
self.log(f"Error in minimal cell selection: {e}")
|
|
@@ -28272,7 +28447,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
28272
28447
|
current_id_item.setForeground(QColor("white"))
|
|
28273
28448
|
|
|
28274
28449
|
if getattr(self, 'auto_center_active_segment', False) and not getattr(self, 'filtering_active', False):
|
|
28275
|
-
self.
|
|
28450
|
+
self._center_row_in_viewport(current_row)
|
|
28276
28451
|
|
|
28277
28452
|
if not self.current_project or current_row < 0:
|
|
28278
28453
|
return
|
|
@@ -31042,11 +31217,18 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31042
31217
|
normalized_source = clean_source_lower
|
|
31043
31218
|
for quote_char in '\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A':
|
|
31044
31219
|
normalized_source = normalized_source.replace(quote_char, ' ')
|
|
31220
|
+
|
|
31221
|
+
# CRITICAL FIX: Strip trailing punctuation from glossary term before matching
|
|
31222
|
+
# This allows entries like "De huidige uitvinding...problemen." (with period)
|
|
31223
|
+
# to match source text "...problemen." where tokenization strips the period
|
|
31224
|
+
# Strip from both ends to handle quotes and trailing punctuation
|
|
31225
|
+
normalized_term = source_term.lower().rstrip(PUNCT_CHARS).lstrip(PUNCT_CHARS)
|
|
31226
|
+
|
|
31045
31227
|
# Check if term has punctuation - use different pattern
|
|
31046
|
-
if any(char in
|
|
31047
|
-
pattern = re.compile(r'(?<!\w)' + re.escape(
|
|
31228
|
+
if any(char in normalized_term for char in ['.', '%', ',', '-', '/']):
|
|
31229
|
+
pattern = re.compile(r'(?<!\w)' + re.escape(normalized_term) + r'(?!\w)')
|
|
31048
31230
|
else:
|
|
31049
|
-
pattern = re.compile(r"\b" + re.escape(
|
|
31231
|
+
pattern = re.compile(r"\b" + re.escape(normalized_term) + r"\b")
|
|
31050
31232
|
|
|
31051
31233
|
# Try matching on normalized (tag-stripped, quote-stripped) text first,
|
|
31052
31234
|
# then tag-stripped, then original with tags
|
|
@@ -31503,7 +31685,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31503
31685
|
self.case_sensitive_cb.setChecked(op.case_sensitive)
|
|
31504
31686
|
|
|
31505
31687
|
def _fr_run_set_batch(self, fr_set: FindReplaceSet):
|
|
31506
|
-
"""Run all enabled operations in a F&R Set as a batch."""
|
|
31688
|
+
"""Run all enabled operations in a F&R Set as a batch (optimized for speed)."""
|
|
31507
31689
|
enabled_ops = [op for op in fr_set.operations if op.enabled and op.find_text]
|
|
31508
31690
|
|
|
31509
31691
|
if not enabled_ops:
|
|
@@ -31522,17 +31704,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31522
31704
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31523
31705
|
return
|
|
31524
31706
|
|
|
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)")
|
|
31707
|
+
# OPTIMIZATION: Disable UI updates during batch processing
|
|
31708
|
+
self.table.setUpdatesEnabled(False)
|
|
31531
31709
|
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
31710
|
+
try:
|
|
31711
|
+
# Run each operation
|
|
31712
|
+
total_replaced = 0
|
|
31713
|
+
for op in enabled_ops:
|
|
31714
|
+
count = self._execute_single_fr_operation(op)
|
|
31715
|
+
total_replaced += count
|
|
31716
|
+
self.log(f" '{op.find_text}' → '{op.replace_text}': {count} replacement(s)")
|
|
31717
|
+
|
|
31718
|
+
self.project_modified = True
|
|
31719
|
+
self.update_window_title()
|
|
31720
|
+
|
|
31721
|
+
finally:
|
|
31722
|
+
# OPTIMIZATION: Re-enable UI updates and refresh only target column cells
|
|
31723
|
+
self.table.setUpdatesEnabled(True)
|
|
31724
|
+
# Update all target cells in-place (batch operations can affect many segments)
|
|
31725
|
+
for row in range(len(self.current_project.segments)):
|
|
31726
|
+
segment = self.current_project.segments[row]
|
|
31727
|
+
target_widget = self.table.cellWidget(row, 3)
|
|
31728
|
+
if target_widget and hasattr(target_widget, 'setPlainText'):
|
|
31729
|
+
target_widget.blockSignals(True)
|
|
31730
|
+
target_widget.setPlainText(segment.target)
|
|
31731
|
+
target_widget.blockSignals(False)
|
|
31732
|
+
self.table.viewport().update()
|
|
31536
31733
|
|
|
31537
31734
|
QMessageBox.information(
|
|
31538
31735
|
self.find_replace_dialog,
|
|
@@ -31541,12 +31738,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31541
31738
|
)
|
|
31542
31739
|
|
|
31543
31740
|
def _execute_single_fr_operation(self, op: FindReplaceOperation) -> int:
|
|
31544
|
-
"""Execute a single F&R operation on all segments. Returns replacement count."""
|
|
31741
|
+
"""Execute a single F&R operation on all segments (optimized). Returns replacement count."""
|
|
31545
31742
|
import re
|
|
31546
31743
|
count = 0
|
|
31547
31744
|
|
|
31745
|
+
# OPTIMIZATION: Pre-filter segments - only check segments that might contain the text
|
|
31746
|
+
# Quick case-insensitive check to skip segments that definitely don't match
|
|
31747
|
+
search_text_lower = op.find_text.lower() if not op.case_sensitive else None
|
|
31748
|
+
|
|
31548
31749
|
for segment in self.current_project.segments:
|
|
31549
31750
|
texts_to_check = []
|
|
31751
|
+
|
|
31752
|
+
# Pre-filter: skip segments that can't possibly match
|
|
31753
|
+
if not op.case_sensitive:
|
|
31754
|
+
# Quick check: does the segment contain the search text at all?
|
|
31755
|
+
skip_segment = True
|
|
31756
|
+
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
31757
|
+
if search_text_lower in segment.source.lower():
|
|
31758
|
+
skip_segment = False
|
|
31759
|
+
if op.search_in in ("target", "both"):
|
|
31760
|
+
if search_text_lower in segment.target.lower():
|
|
31761
|
+
skip_segment = False
|
|
31762
|
+
if skip_segment:
|
|
31763
|
+
continue
|
|
31764
|
+
|
|
31550
31765
|
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
31551
31766
|
texts_to_check.append(("source", segment.source))
|
|
31552
31767
|
if op.search_in in ("target", "both"):
|
|
@@ -31842,7 +32057,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31842
32057
|
self.find_next_match()
|
|
31843
32058
|
|
|
31844
32059
|
def replace_all_matches(self):
|
|
31845
|
-
"""Replace all matches in target segments"""
|
|
32060
|
+
"""Replace all matches in target segments (optimized for speed)"""
|
|
31846
32061
|
find_text = self.find_input.text()
|
|
31847
32062
|
replace_text = self.replace_input.text()
|
|
31848
32063
|
|
|
@@ -31915,52 +32130,67 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31915
32130
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31916
32131
|
return
|
|
31917
32132
|
|
|
31918
|
-
#
|
|
31919
|
-
|
|
31920
|
-
replaced_count = 0
|
|
32133
|
+
# OPTIMIZATION: Disable UI updates during batch replacement
|
|
32134
|
+
self.table.setUpdatesEnabled(False)
|
|
31921
32135
|
|
|
31922
|
-
|
|
31923
|
-
|
|
31924
|
-
|
|
31925
|
-
|
|
31926
|
-
|
|
31927
|
-
old_text = segment.source
|
|
31928
|
-
else: # col == 3, Target
|
|
31929
|
-
old_text = segment.target
|
|
32136
|
+
try:
|
|
32137
|
+
# Perform replacements
|
|
32138
|
+
import re
|
|
32139
|
+
replaced_count = 0
|
|
32140
|
+
updated_rows = set() # Track which rows need UI updates
|
|
31930
32141
|
|
|
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
|
|
32142
|
+
for row, col in self.find_matches:
|
|
32143
|
+
segment = self.current_project.segments[row]
|
|
32144
|
+
|
|
32145
|
+
# Get the appropriate field
|
|
32146
|
+
if col == 2: # Source
|
|
32147
|
+
old_text = segment.source
|
|
32148
|
+
else: # col == 3, Target
|
|
32149
|
+
old_text = segment.target
|
|
32150
|
+
|
|
32151
|
+
# Perform replacement
|
|
32152
|
+
if match_mode == 2: # Entire segment
|
|
32153
|
+
new_text = replace_text
|
|
31946
32154
|
else:
|
|
31947
|
-
|
|
31948
|
-
|
|
31949
|
-
|
|
31950
|
-
|
|
31951
|
-
|
|
32155
|
+
if case_sensitive:
|
|
32156
|
+
new_text = old_text.replace(find_text, replace_text)
|
|
32157
|
+
else:
|
|
32158
|
+
pattern = re.escape(find_text)
|
|
32159
|
+
new_text = re.sub(pattern, replace_text, old_text, flags=re.IGNORECASE)
|
|
31952
32160
|
|
|
31953
|
-
|
|
31954
|
-
|
|
31955
|
-
|
|
31956
|
-
|
|
31957
|
-
|
|
31958
|
-
|
|
31959
|
-
|
|
31960
|
-
|
|
31961
|
-
|
|
31962
|
-
|
|
31963
|
-
|
|
32161
|
+
if new_text != old_text:
|
|
32162
|
+
replaced_count += 1
|
|
32163
|
+
updated_rows.add(row)
|
|
32164
|
+
|
|
32165
|
+
# Update the appropriate field
|
|
32166
|
+
if col == 2:
|
|
32167
|
+
segment.source = new_text
|
|
32168
|
+
else:
|
|
32169
|
+
old_target = segment.target
|
|
32170
|
+
old_status = segment.status
|
|
32171
|
+
segment.target = new_text
|
|
32172
|
+
# Record undo state for find/replace operation
|
|
32173
|
+
self.record_undo_state(segment.id, old_target, new_text, old_status, old_status)
|
|
32174
|
+
|
|
32175
|
+
# OPTIMIZATION: Update only the affected cell widget in-place
|
|
32176
|
+
cell_widget = self.table.cellWidget(row, col)
|
|
32177
|
+
if cell_widget and hasattr(cell_widget, 'setPlainText'):
|
|
32178
|
+
cell_widget.blockSignals(True)
|
|
32179
|
+
cell_widget.setPlainText(new_text)
|
|
32180
|
+
cell_widget.blockSignals(False)
|
|
32181
|
+
|
|
32182
|
+
self.project_modified = True
|
|
32183
|
+
self.update_window_title()
|
|
32184
|
+
|
|
32185
|
+
# Clear matches
|
|
32186
|
+
self.find_matches = []
|
|
32187
|
+
|
|
32188
|
+
finally:
|
|
32189
|
+
# OPTIMIZATION: Re-enable UI updates without full grid reload
|
|
32190
|
+
self.table.setUpdatesEnabled(True)
|
|
32191
|
+
# Trigger a repaint of updated rows only
|
|
32192
|
+
for row in updated_rows:
|
|
32193
|
+
self.table.viewport().update()
|
|
31964
32194
|
|
|
31965
32195
|
QMessageBox.information(self.find_replace_dialog, "Replace All", f"Replaced {replaced_count} occurrence(s).")
|
|
31966
32196
|
self.log(f"✓ Replaced {replaced_count} occurrence(s) of '{find_text}'")
|
|
@@ -37564,7 +37794,19 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37564
37794
|
return text.strip()
|
|
37565
37795
|
|
|
37566
37796
|
def _quickmenu_build_custom_prompt(self, prompt_relative_path: str, source_text: str, source_lang: str, target_lang: str) -> str:
|
|
37567
|
-
"""Build a
|
|
37797
|
+
"""Build a prompt for QuickMenu using the selected prompt as instructions.
|
|
37798
|
+
|
|
37799
|
+
This is a GENERIC prompt builder (not translation-specific) that allows QuickMenu prompts
|
|
37800
|
+
to do anything: explain, define, search, translate, analyze, etc.
|
|
37801
|
+
|
|
37802
|
+
Supports placeholders:
|
|
37803
|
+
- {{SELECTION}} or {{SOURCE_TEXT}} - The selected text
|
|
37804
|
+
- {{SOURCE_LANGUAGE}} - Source language name
|
|
37805
|
+
- {{TARGET_LANGUAGE}} - Target language name
|
|
37806
|
+
- {{SOURCE+TARGET_CONTEXT}} - Project segments with both source and target (for proofreading)
|
|
37807
|
+
- {{SOURCE_CONTEXT}} - Project segments with source only (for translation questions)
|
|
37808
|
+
- {{TARGET_CONTEXT}} - Project segments with target only (for consistency/style analysis)
|
|
37809
|
+
"""
|
|
37568
37810
|
if not hasattr(self, 'prompt_manager_qt') or not self.prompt_manager_qt:
|
|
37569
37811
|
raise RuntimeError("Prompt manager not available")
|
|
37570
37812
|
|
|
@@ -37581,25 +37823,114 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37581
37823
|
if not prompt_content:
|
|
37582
37824
|
raise RuntimeError("Prompt content is empty")
|
|
37583
37825
|
|
|
37584
|
-
|
|
37585
|
-
|
|
37586
|
-
|
|
37587
|
-
|
|
37588
|
-
|
|
37589
|
-
|
|
37590
|
-
|
|
37591
|
-
|
|
37592
|
-
|
|
37593
|
-
|
|
37826
|
+
# Build document context if requested (three variants)
|
|
37827
|
+
source_target_context = ""
|
|
37828
|
+
source_only_context = ""
|
|
37829
|
+
target_only_context = ""
|
|
37830
|
+
|
|
37831
|
+
has_source_target = "{{SOURCE+TARGET_CONTEXT}}" in prompt_content
|
|
37832
|
+
has_source_only = "{{SOURCE_CONTEXT}}" in prompt_content
|
|
37833
|
+
has_target_only = "{{TARGET_CONTEXT}}" in prompt_content
|
|
37834
|
+
|
|
37835
|
+
if (has_source_target or has_source_only or has_target_only) and hasattr(self, 'current_project') and self.current_project:
|
|
37836
|
+
if has_source_target:
|
|
37837
|
+
source_target_context = self._build_quickmenu_document_context(mode="both")
|
|
37838
|
+
self.log(f"🔍 QuickMenu: Built SOURCE+TARGET context ({len(source_target_context)} chars)")
|
|
37839
|
+
if has_source_only:
|
|
37840
|
+
source_only_context = self._build_quickmenu_document_context(mode="source")
|
|
37841
|
+
self.log(f"🔍 QuickMenu: Built SOURCE_ONLY context ({len(source_only_context)} chars)")
|
|
37842
|
+
if has_target_only:
|
|
37843
|
+
target_only_context = self._build_quickmenu_document_context(mode="target")
|
|
37844
|
+
self.log(f"🔍 QuickMenu: Built TARGET_ONLY context ({len(target_only_context)} chars)")
|
|
37845
|
+
else:
|
|
37846
|
+
if has_source_target:
|
|
37847
|
+
self.log("⚠️ QuickMenu: {{SOURCE+TARGET_CONTEXT}} requested but no project loaded")
|
|
37848
|
+
if has_source_only:
|
|
37849
|
+
self.log("⚠️ QuickMenu: {{SOURCE_CONTEXT}} requested but no project loaded")
|
|
37850
|
+
if has_target_only:
|
|
37851
|
+
self.log("⚠️ QuickMenu: {{TARGET_CONTEXT}} requested but no project loaded")
|
|
37852
|
+
|
|
37853
|
+
# Replace placeholders in the prompt content
|
|
37854
|
+
prompt_content = prompt_content.replace("{{SOURCE_LANGUAGE}}", source_lang)
|
|
37855
|
+
prompt_content = prompt_content.replace("{{TARGET_LANGUAGE}}", target_lang)
|
|
37856
|
+
prompt_content = prompt_content.replace("{{SOURCE_TEXT}}", source_text)
|
|
37857
|
+
prompt_content = prompt_content.replace("{{SELECTION}}", source_text) # Alternative placeholder
|
|
37858
|
+
prompt_content = prompt_content.replace("{{SOURCE+TARGET_CONTEXT}}", source_target_context)
|
|
37859
|
+
prompt_content = prompt_content.replace("{{SOURCE_CONTEXT}}", source_only_context)
|
|
37860
|
+
prompt_content = prompt_content.replace("{{TARGET_CONTEXT}}", target_only_context)
|
|
37861
|
+
|
|
37862
|
+
# Debug: Log the final prompt being sent
|
|
37863
|
+
self.log(f"📝 QuickMenu: Final prompt ({len(prompt_content)} chars):")
|
|
37864
|
+
self.log("─" * 80)
|
|
37865
|
+
self.log(prompt_content[:500] + ("..." if len(prompt_content) > 500 else ""))
|
|
37866
|
+
self.log("─" * 80)
|
|
37867
|
+
|
|
37868
|
+
# If the prompt doesn't contain the selection/text, append it
|
|
37869
|
+
if "{{SOURCE_TEXT}}" not in prompt_data.get('content', '') and "{{SELECTION}}" not in prompt_data.get('content', ''):
|
|
37870
|
+
prompt_content += f"\n\nText:\n{source_text}"
|
|
37871
|
+
|
|
37872
|
+
return prompt_content
|
|
37873
|
+
|
|
37874
|
+
def _build_quickmenu_document_context(self, mode: str = "both") -> str:
|
|
37875
|
+
"""Build document context for QuickMenu prompts.
|
|
37876
|
+
|
|
37877
|
+
Args:
|
|
37878
|
+
mode: One of:
|
|
37879
|
+
- "both": Include both source and target text (for proofreading)
|
|
37880
|
+
- "source": Include only source text (for translation/terminology questions)
|
|
37881
|
+
- "target": Include only target text (for consistency/style analysis)
|
|
37882
|
+
|
|
37883
|
+
Returns a formatted string with segments from the project for context.
|
|
37884
|
+
Uses the 'quickmenu_context_segments' setting (default: 50% of total segments).
|
|
37885
|
+
"""
|
|
37886
|
+
if not hasattr(self, 'current_project') or not self.current_project or not self.current_project.segments:
|
|
37887
|
+
return "(No project context available)"
|
|
37888
|
+
|
|
37594
37889
|
try:
|
|
37595
|
-
|
|
37596
|
-
|
|
37597
|
-
|
|
37598
|
-
|
|
37599
|
-
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
37890
|
+
# Get settings
|
|
37891
|
+
general_prefs = self.load_general_settings()
|
|
37892
|
+
context_percent = general_prefs.get('quickmenu_context_percent', 50) # Default: 50%
|
|
37893
|
+
max_context_segments = general_prefs.get('quickmenu_context_max', 100) # Safety limit
|
|
37894
|
+
|
|
37895
|
+
# Calculate how many segments to include
|
|
37896
|
+
total_segments = len(self.current_project.segments)
|
|
37897
|
+
num_segments = min(
|
|
37898
|
+
int(total_segments * context_percent / 100),
|
|
37899
|
+
max_context_segments
|
|
37900
|
+
)
|
|
37901
|
+
|
|
37902
|
+
if num_segments == 0:
|
|
37903
|
+
return "(Document context disabled)"
|
|
37904
|
+
|
|
37905
|
+
# Get segments (from start of document)
|
|
37906
|
+
context_segments = self.current_project.segments[:num_segments]
|
|
37907
|
+
|
|
37908
|
+
# Format segments based on mode
|
|
37909
|
+
context_parts = []
|
|
37910
|
+
mode_labels = {"both": "SOURCE + TARGET", "source": "SOURCE ONLY", "target": "TARGET ONLY"}
|
|
37911
|
+
context_parts.append(f"=== DOCUMENT CONTEXT ({mode_labels.get(mode, 'UNKNOWN')}) ===")
|
|
37912
|
+
context_parts.append(f"(Showing {num_segments} of {total_segments} segments - {context_percent}%)")
|
|
37913
|
+
context_parts.append("")
|
|
37914
|
+
|
|
37915
|
+
for seg in context_segments:
|
|
37916
|
+
if mode == "both":
|
|
37917
|
+
# Source + Target
|
|
37918
|
+
context_parts.append(f"[{seg.id}] {seg.source}")
|
|
37919
|
+
if seg.target and seg.target.strip():
|
|
37920
|
+
context_parts.append(f" → {seg.target}")
|
|
37921
|
+
elif mode == "source":
|
|
37922
|
+
# Source only
|
|
37923
|
+
context_parts.append(f"[{seg.id}] {seg.source}")
|
|
37924
|
+
elif mode == "target":
|
|
37925
|
+
# Target only (skip empty targets)
|
|
37926
|
+
if seg.target and seg.target.strip():
|
|
37927
|
+
context_parts.append(f"[{seg.id}] {seg.target}")
|
|
37928
|
+
context_parts.append("") # Blank line between segments
|
|
37929
|
+
|
|
37930
|
+
return "\n".join(context_parts)
|
|
37931
|
+
|
|
37932
|
+
except Exception as e:
|
|
37933
|
+
return f"(Error building document context: {e})"
|
|
37603
37934
|
|
|
37604
37935
|
def _quickmenu_show_result_dialog(self, title: str, output_text: str, apply_callback=None):
|
|
37605
37936
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QPushButton, QApplication
|
|
@@ -37679,10 +38010,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
37679
38010
|
)
|
|
37680
38011
|
|
|
37681
38012
|
client = LLMClient(api_key=api_key, provider=provider, model=model)
|
|
38013
|
+
|
|
38014
|
+
# Use translate() with empty text and custom_prompt for generic AI completion
|
|
38015
|
+
# This allows QuickMenu prompts to do anything (explain, define, search, etc.)
|
|
38016
|
+
# not just translation. Same pattern as AI Assistant.
|
|
37682
38017
|
output_text = client.translate(
|
|
37683
|
-
text=
|
|
37684
|
-
source_lang=
|
|
37685
|
-
target_lang=
|
|
38018
|
+
text="", # Empty - we're using custom_prompt for everything
|
|
38019
|
+
source_lang="en", # Dummy values
|
|
38020
|
+
target_lang="en",
|
|
37686
38021
|
custom_prompt=custom_prompt
|
|
37687
38022
|
)
|
|
37688
38023
|
|
|
@@ -44268,7 +44603,7 @@ class SuperlookupTab(QWidget):
|
|
|
44268
44603
|
try:
|
|
44269
44604
|
# Use multiple methods to ensure cleanup
|
|
44270
44605
|
# Method 1: Kill by window title
|
|
44271
|
-
subprocess.run(['taskkill', '/F', '/FI', 'WINDOWTITLE eq
|
|
44606
|
+
subprocess.run(['taskkill', '/F', '/FI', 'WINDOWTITLE eq supervertaler_hotkeys.ahk*'],
|
|
44272
44607
|
capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
44273
44608
|
|
|
44274
44609
|
# Method 2: Kill AutoHotkey processes more aggressively
|
|
@@ -44280,7 +44615,7 @@ class SuperlookupTab(QWidget):
|
|
|
44280
44615
|
try:
|
|
44281
44616
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
|
44282
44617
|
try:
|
|
44283
|
-
if '
|
|
44618
|
+
if 'supervertaler_hotkeys' in ' '.join(proc.cmdline() or []):
|
|
44284
44619
|
proc.kill()
|
|
44285
44620
|
except:
|
|
44286
44621
|
pass
|
|
@@ -44293,8 +44628,8 @@ class SuperlookupTab(QWidget):
|
|
|
44293
44628
|
ahk_exe, source = self._find_autohotkey_executable()
|
|
44294
44629
|
|
|
44295
44630
|
if not ahk_exe:
|
|
44296
|
-
print("[
|
|
44297
|
-
print("[
|
|
44631
|
+
print("[Hotkeys] AutoHotkey not found.")
|
|
44632
|
+
print("[Hotkeys] Global hotkeys (Ctrl+Alt+L, Shift+Shift) will not be available.")
|
|
44298
44633
|
self.hotkey_registered = False
|
|
44299
44634
|
# Show setup dialog (deferred to avoid blocking startup) - unless user opted out
|
|
44300
44635
|
if self.main_window and hasattr(self.main_window, 'general_settings'):
|
|
@@ -44304,11 +44639,11 @@ class SuperlookupTab(QWidget):
|
|
|
44304
44639
|
QTimer.singleShot(2000, self._show_autohotkey_setup_dialog)
|
|
44305
44640
|
return
|
|
44306
44641
|
|
|
44307
|
-
print(f"[
|
|
44642
|
+
print(f"[Hotkeys] Found AutoHotkey at: {ahk_exe} (source: {source})")
|
|
44308
44643
|
|
|
44309
|
-
ahk_script = Path(__file__).parent / "
|
|
44310
|
-
print(f"[
|
|
44311
|
-
print(f"[
|
|
44644
|
+
ahk_script = Path(__file__).parent / "supervertaler_hotkeys.ahk"
|
|
44645
|
+
print(f"[Hotkeys] Looking for script at: {ahk_script}")
|
|
44646
|
+
print(f"[Hotkeys] Script exists: {ahk_script.exists()}")
|
|
44312
44647
|
|
|
44313
44648
|
if ahk_script.exists():
|
|
44314
44649
|
# Start AHK script in background (hidden)
|
|
@@ -44316,16 +44651,16 @@ class SuperlookupTab(QWidget):
|
|
|
44316
44651
|
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
|
44317
44652
|
# Store in global variable for atexit cleanup
|
|
44318
44653
|
_ahk_process = self.ahk_process
|
|
44319
|
-
print(f"[
|
|
44654
|
+
print(f"[Hotkeys] AHK hotkeys registered (Ctrl+Alt+L, Shift+Shift)")
|
|
44320
44655
|
|
|
44321
44656
|
# Start file watcher
|
|
44322
44657
|
self.start_file_watcher()
|
|
44323
44658
|
self.hotkey_registered = True
|
|
44324
44659
|
else:
|
|
44325
|
-
print(f"[
|
|
44660
|
+
print(f"[Hotkeys] AHK script not found: {ahk_script}")
|
|
44326
44661
|
self.hotkey_registered = False
|
|
44327
44662
|
except Exception as e:
|
|
44328
|
-
print(f"[
|
|
44663
|
+
print(f"[Hotkeys] Could not start AHK hotkeys: {e}")
|
|
44329
44664
|
self.hotkey_registered = False
|
|
44330
44665
|
|
|
44331
44666
|
def start_file_watcher(self):
|