supervertaler 1.9.185__py3-none-any.whl → 1.9.196__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of supervertaler might be problematic. Click here for more details.
- Supervertaler.py +1123 -276
- modules/keyboard_shortcuts_widget.py +76 -8
- modules/llm_clients.py +58 -33
- modules/quicktrans.py +670 -0
- modules/shortcut_manager.py +19 -5
- modules/statuses.py +2 -2
- modules/superbrowser.py +22 -0
- modules/superlookup.py +3 -3
- modules/unified_prompt_manager_qt.py +22 -1
- {supervertaler-1.9.185.dist-info → supervertaler-1.9.196.dist-info}/METADATA +1 -1
- {supervertaler-1.9.185.dist-info → supervertaler-1.9.196.dist-info}/RECORD +15 -14
- {supervertaler-1.9.185.dist-info → supervertaler-1.9.196.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.185.dist-info → supervertaler-1.9.196.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.185.dist-info → supervertaler-1.9.196.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.185.dist-info → supervertaler-1.9.196.dist-info}/top_level.txt +0 -0
modules/quicktrans.py
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuickTrans - Instant translation popup (GT4T-style)
|
|
3
|
+
|
|
4
|
+
A popup window that shows translations from all enabled MT engines and LLMs.
|
|
5
|
+
Part of the Supervertaler tool suite. Triggered by Ctrl+M (in-app) or Ctrl+Alt+M (global).
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Shows source text at the top
|
|
9
|
+
- Displays numbered list of translations from MT engines and LLMs
|
|
10
|
+
- Press number key (1-9) or click to insert translation
|
|
11
|
+
- Escape to dismiss
|
|
12
|
+
- Translations fetched in parallel for speed
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from PyQt6.QtWidgets import (
|
|
16
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QFrame,
|
|
17
|
+
QScrollArea, QWidget, QPushButton, QApplication
|
|
18
|
+
)
|
|
19
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings
|
|
20
|
+
from PyQt6.QtGui import QKeySequence, QShortcut, QCursor, QFont
|
|
21
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
import threading
|
|
24
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MTSuggestion:
|
|
29
|
+
"""A single MT suggestion from a provider"""
|
|
30
|
+
provider_name: str # Full name: "Google Translate", "DeepL", etc.
|
|
31
|
+
provider_code: str # Short code: "GT", "DL", etc.
|
|
32
|
+
translation: str
|
|
33
|
+
is_error: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MTFetchWorker(QThread):
|
|
37
|
+
"""Background worker to fetch MT translations in parallel"""
|
|
38
|
+
|
|
39
|
+
result_ready = pyqtSignal(str, str, str, bool) # provider_name, provider_code, translation, is_error
|
|
40
|
+
all_complete = pyqtSignal()
|
|
41
|
+
|
|
42
|
+
def __init__(self, source_text: str, source_lang: str, target_lang: str,
|
|
43
|
+
providers: List[Tuple[str, str, callable]], parent=None):
|
|
44
|
+
super().__init__(parent)
|
|
45
|
+
self.source_text = source_text
|
|
46
|
+
self.source_lang = source_lang
|
|
47
|
+
self.target_lang = target_lang
|
|
48
|
+
self.providers = providers # List of (name, code, call_function)
|
|
49
|
+
|
|
50
|
+
def run(self):
|
|
51
|
+
"""Fetch translations from all providers in parallel"""
|
|
52
|
+
def fetch_single(provider_info):
|
|
53
|
+
name, code, call_func = provider_info
|
|
54
|
+
try:
|
|
55
|
+
result = call_func(self.source_text, self.source_lang, self.target_lang)
|
|
56
|
+
is_error = result.startswith('[') and 'error' in result.lower()
|
|
57
|
+
return (name, code, result, is_error)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return (name, code, f"[Error: {str(e)}]", True)
|
|
60
|
+
|
|
61
|
+
# Use ThreadPoolExecutor for parallel execution
|
|
62
|
+
with ThreadPoolExecutor(max_workers=6) as executor:
|
|
63
|
+
futures = {executor.submit(fetch_single, p): p for p in self.providers}
|
|
64
|
+
for future in as_completed(futures):
|
|
65
|
+
try:
|
|
66
|
+
name, code, translation, is_error = future.result()
|
|
67
|
+
self.result_ready.emit(name, code, translation, is_error)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
provider = futures[future]
|
|
70
|
+
self.result_ready.emit(provider[0], provider[1], f"[Error: {str(e)}]", True)
|
|
71
|
+
|
|
72
|
+
self.all_complete.emit()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MTSuggestionItem(QFrame):
|
|
76
|
+
"""A single MT suggestion row in the popup"""
|
|
77
|
+
|
|
78
|
+
clicked = pyqtSignal(str) # Emits the translation text when clicked
|
|
79
|
+
|
|
80
|
+
def __init__(self, number: int, suggestion: MTSuggestion, parent=None):
|
|
81
|
+
super().__init__(parent)
|
|
82
|
+
self.suggestion = suggestion
|
|
83
|
+
self.number = number
|
|
84
|
+
self.is_selected = False
|
|
85
|
+
|
|
86
|
+
self.setFrameStyle(QFrame.Shape.NoFrame)
|
|
87
|
+
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
88
|
+
|
|
89
|
+
layout = QHBoxLayout(self)
|
|
90
|
+
layout.setContentsMargins(8, 6, 8, 6)
|
|
91
|
+
layout.setSpacing(10)
|
|
92
|
+
|
|
93
|
+
# Number badge
|
|
94
|
+
num_label = QLabel(str(number))
|
|
95
|
+
num_label.setFixedSize(24, 24)
|
|
96
|
+
num_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
97
|
+
num_label.setStyleSheet("""
|
|
98
|
+
QLabel {
|
|
99
|
+
background-color: #ff9800;
|
|
100
|
+
color: #333;
|
|
101
|
+
font-weight: bold;
|
|
102
|
+
font-size: 11px;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
}
|
|
105
|
+
""")
|
|
106
|
+
layout.addWidget(num_label)
|
|
107
|
+
|
|
108
|
+
# Provider icon/code badge
|
|
109
|
+
provider_label = QLabel(suggestion.provider_code)
|
|
110
|
+
provider_label.setFixedWidth(36)
|
|
111
|
+
provider_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
112
|
+
provider_label.setToolTip(suggestion.provider_name)
|
|
113
|
+
|
|
114
|
+
# Color-code by provider
|
|
115
|
+
provider_colors = {
|
|
116
|
+
"GT": "#4285F4", # Google blue
|
|
117
|
+
"DL": "#042B48", # DeepL dark blue
|
|
118
|
+
"MS": "#00A4EF", # Microsoft blue
|
|
119
|
+
"AT": "#FF9900", # Amazon orange
|
|
120
|
+
"MMT": "#6B4EE6", # ModernMT purple
|
|
121
|
+
"MM": "#2ECC71", # MyMemory green
|
|
122
|
+
# LLM providers
|
|
123
|
+
"CL": "#D97706", # Claude orange/amber
|
|
124
|
+
"GPT": "#10A37F", # OpenAI green
|
|
125
|
+
"GEM": "#4285F4", # Gemini blue (Google)
|
|
126
|
+
}
|
|
127
|
+
bg_color = provider_colors.get(suggestion.provider_code, "#666")
|
|
128
|
+
provider_label.setStyleSheet(f"""
|
|
129
|
+
QLabel {{
|
|
130
|
+
background-color: {bg_color};
|
|
131
|
+
color: white;
|
|
132
|
+
font-weight: bold;
|
|
133
|
+
font-size: 9px;
|
|
134
|
+
border-radius: 3px;
|
|
135
|
+
padding: 2px 4px;
|
|
136
|
+
}}
|
|
137
|
+
""")
|
|
138
|
+
layout.addWidget(provider_label)
|
|
139
|
+
|
|
140
|
+
# Translation text
|
|
141
|
+
text_label = QLabel(suggestion.translation)
|
|
142
|
+
text_label.setWordWrap(True)
|
|
143
|
+
text_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
144
|
+
|
|
145
|
+
if suggestion.is_error:
|
|
146
|
+
text_label.setStyleSheet("color: #ff6b6b; font-size: 11px;")
|
|
147
|
+
else:
|
|
148
|
+
text_label.setStyleSheet("color: #333; font-size: 11px;")
|
|
149
|
+
|
|
150
|
+
layout.addWidget(text_label, 1)
|
|
151
|
+
|
|
152
|
+
self._update_style()
|
|
153
|
+
|
|
154
|
+
def _update_style(self):
|
|
155
|
+
"""Update visual style based on selection state"""
|
|
156
|
+
if self.is_selected:
|
|
157
|
+
self.setStyleSheet("""
|
|
158
|
+
MTSuggestionItem {
|
|
159
|
+
background-color: #e3f2fd;
|
|
160
|
+
border: 1px solid #2196F3;
|
|
161
|
+
border-radius: 4px;
|
|
162
|
+
}
|
|
163
|
+
""")
|
|
164
|
+
else:
|
|
165
|
+
self.setStyleSheet("""
|
|
166
|
+
MTSuggestionItem {
|
|
167
|
+
background-color: white;
|
|
168
|
+
border: 1px solid #e0e0e0;
|
|
169
|
+
border-radius: 4px;
|
|
170
|
+
}
|
|
171
|
+
MTSuggestionItem:hover {
|
|
172
|
+
background-color: #f5f5f5;
|
|
173
|
+
border: 1px solid #bdbdbd;
|
|
174
|
+
}
|
|
175
|
+
""")
|
|
176
|
+
|
|
177
|
+
def select(self):
|
|
178
|
+
"""Select this item"""
|
|
179
|
+
self.is_selected = True
|
|
180
|
+
self._update_style()
|
|
181
|
+
|
|
182
|
+
def deselect(self):
|
|
183
|
+
"""Deselect this item"""
|
|
184
|
+
self.is_selected = False
|
|
185
|
+
self._update_style()
|
|
186
|
+
|
|
187
|
+
def mousePressEvent(self, event):
|
|
188
|
+
"""Handle click to select this translation"""
|
|
189
|
+
if event.button() == Qt.MouseButton.LeftButton and not self.suggestion.is_error:
|
|
190
|
+
self.clicked.emit(self.suggestion.translation)
|
|
191
|
+
super().mousePressEvent(event)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class MTQuickPopup(QDialog):
|
|
195
|
+
"""
|
|
196
|
+
GT4T-style popup showing MT suggestions from all enabled providers
|
|
197
|
+
|
|
198
|
+
Usage:
|
|
199
|
+
popup = MTQuickPopup(parent_app, source_text, source_lang, target_lang)
|
|
200
|
+
popup.translation_selected.connect(on_translation_selected)
|
|
201
|
+
popup.show()
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
translation_selected = pyqtSignal(str) # Emitted when user selects a translation
|
|
205
|
+
|
|
206
|
+
def __init__(self, parent_app, source_text: str, source_lang: str = None,
|
|
207
|
+
target_lang: str = None, parent=None):
|
|
208
|
+
super().__init__(parent)
|
|
209
|
+
self.parent_app = parent_app
|
|
210
|
+
self.source_text = source_text
|
|
211
|
+
self.source_lang = source_lang or getattr(parent_app, 'source_language', 'en')
|
|
212
|
+
self.target_lang = target_lang or getattr(parent_app, 'target_language', 'nl')
|
|
213
|
+
|
|
214
|
+
self.suggestions: List[MTSuggestion] = []
|
|
215
|
+
self.suggestion_items: List[MTSuggestionItem] = []
|
|
216
|
+
self.selected_index = -1
|
|
217
|
+
self.worker = None
|
|
218
|
+
|
|
219
|
+
self.setup_ui()
|
|
220
|
+
self.setup_shortcuts()
|
|
221
|
+
self.start_fetching()
|
|
222
|
+
|
|
223
|
+
def setup_ui(self):
|
|
224
|
+
"""Setup the popup UI"""
|
|
225
|
+
self.setWindowTitle("⚡ Supervertaler QuickTrans")
|
|
226
|
+
# Use standard dialog with title bar for resize/move support
|
|
227
|
+
self.setWindowFlags(
|
|
228
|
+
Qt.WindowType.Dialog |
|
|
229
|
+
Qt.WindowType.WindowCloseButtonHint |
|
|
230
|
+
Qt.WindowType.WindowStaysOnTopHint
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Set size - allow resizing
|
|
234
|
+
self.setMinimumWidth(450)
|
|
235
|
+
self.setMinimumHeight(200)
|
|
236
|
+
|
|
237
|
+
# Restore saved size and position or use defaults
|
|
238
|
+
settings = QSettings("Supervertaler", "MTQuickPopup")
|
|
239
|
+
saved_width = settings.value("width", 650, type=int)
|
|
240
|
+
saved_height = settings.value("height", 400, type=int)
|
|
241
|
+
self.resize(saved_width, saved_height)
|
|
242
|
+
|
|
243
|
+
# Check if we have a saved position
|
|
244
|
+
self._has_saved_position = settings.contains("x") and settings.contains("y")
|
|
245
|
+
if self._has_saved_position:
|
|
246
|
+
saved_x = settings.value("x", 0, type=int)
|
|
247
|
+
saved_y = settings.value("y", 0, type=int)
|
|
248
|
+
self.move(saved_x, saved_y)
|
|
249
|
+
|
|
250
|
+
# Main layout
|
|
251
|
+
main_layout = QVBoxLayout(self)
|
|
252
|
+
main_layout.setContentsMargins(8, 8, 8, 8)
|
|
253
|
+
main_layout.setSpacing(0)
|
|
254
|
+
|
|
255
|
+
# Container with styling
|
|
256
|
+
container = QFrame()
|
|
257
|
+
container.setStyleSheet("""
|
|
258
|
+
QFrame {
|
|
259
|
+
background-color: white;
|
|
260
|
+
border: 1px solid #e0e0e0;
|
|
261
|
+
border-radius: 4px;
|
|
262
|
+
}
|
|
263
|
+
""")
|
|
264
|
+
container_layout = QVBoxLayout(container)
|
|
265
|
+
container_layout.setContentsMargins(12, 12, 12, 12)
|
|
266
|
+
container_layout.setSpacing(8)
|
|
267
|
+
|
|
268
|
+
# Header with title and settings button
|
|
269
|
+
header_layout = QHBoxLayout()
|
|
270
|
+
header_layout.setContentsMargins(0, 0, 0, 4)
|
|
271
|
+
|
|
272
|
+
title_label = QLabel("⚡ Supervertaler QuickTrans")
|
|
273
|
+
title_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #333;")
|
|
274
|
+
header_layout.addWidget(title_label)
|
|
275
|
+
|
|
276
|
+
header_layout.addStretch()
|
|
277
|
+
|
|
278
|
+
# Settings button
|
|
279
|
+
settings_btn = QPushButton("⚙️")
|
|
280
|
+
settings_btn.setFixedSize(24, 24)
|
|
281
|
+
settings_btn.setToolTip("Configure QuickTrans providers")
|
|
282
|
+
settings_btn.setStyleSheet("""
|
|
283
|
+
QPushButton {
|
|
284
|
+
border: none;
|
|
285
|
+
background: transparent;
|
|
286
|
+
font-size: 14px;
|
|
287
|
+
}
|
|
288
|
+
QPushButton:hover {
|
|
289
|
+
background-color: #e0e0e0;
|
|
290
|
+
border-radius: 4px;
|
|
291
|
+
}
|
|
292
|
+
""")
|
|
293
|
+
settings_btn.clicked.connect(self._open_settings)
|
|
294
|
+
header_layout.addWidget(settings_btn)
|
|
295
|
+
|
|
296
|
+
container_layout.addLayout(header_layout)
|
|
297
|
+
|
|
298
|
+
# Source text display
|
|
299
|
+
source_frame = QFrame()
|
|
300
|
+
source_frame.setStyleSheet("""
|
|
301
|
+
QFrame {
|
|
302
|
+
background-color: #f5f5f5;
|
|
303
|
+
border: 1px solid #e0e0e0;
|
|
304
|
+
border-radius: 4px;
|
|
305
|
+
}
|
|
306
|
+
""")
|
|
307
|
+
source_layout = QVBoxLayout(source_frame)
|
|
308
|
+
source_layout.setContentsMargins(8, 6, 8, 6)
|
|
309
|
+
|
|
310
|
+
source_header = QLabel("Source:")
|
|
311
|
+
source_header.setStyleSheet("font-size: 9px; color: #666; font-weight: bold;")
|
|
312
|
+
source_layout.addWidget(source_header)
|
|
313
|
+
|
|
314
|
+
source_text_label = QLabel(self.source_text)
|
|
315
|
+
source_text_label.setWordWrap(True)
|
|
316
|
+
source_text_label.setStyleSheet("font-size: 11px; color: #333;")
|
|
317
|
+
source_text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
318
|
+
source_layout.addWidget(source_text_label)
|
|
319
|
+
|
|
320
|
+
container_layout.addWidget(source_frame)
|
|
321
|
+
|
|
322
|
+
# Separator
|
|
323
|
+
sep = QFrame()
|
|
324
|
+
sep.setFrameShape(QFrame.Shape.HLine)
|
|
325
|
+
sep.setStyleSheet("background-color: #e0e0e0;")
|
|
326
|
+
sep.setFixedHeight(1)
|
|
327
|
+
container_layout.addWidget(sep)
|
|
328
|
+
|
|
329
|
+
# Suggestions scroll area
|
|
330
|
+
scroll = QScrollArea()
|
|
331
|
+
scroll.setWidgetResizable(True)
|
|
332
|
+
scroll.setStyleSheet("""
|
|
333
|
+
QScrollArea {
|
|
334
|
+
border: none;
|
|
335
|
+
background-color: transparent;
|
|
336
|
+
}
|
|
337
|
+
""")
|
|
338
|
+
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
339
|
+
|
|
340
|
+
self.suggestions_container = QWidget()
|
|
341
|
+
self.suggestions_layout = QVBoxLayout(self.suggestions_container)
|
|
342
|
+
self.suggestions_layout.setContentsMargins(0, 0, 0, 0)
|
|
343
|
+
self.suggestions_layout.setSpacing(4)
|
|
344
|
+
|
|
345
|
+
# Loading indicator
|
|
346
|
+
self.loading_label = QLabel("⏳ Fetching translations...")
|
|
347
|
+
self.loading_label.setStyleSheet("color: #666; font-size: 11px; padding: 20px;")
|
|
348
|
+
self.loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
349
|
+
self.suggestions_layout.addWidget(self.loading_label)
|
|
350
|
+
|
|
351
|
+
self.suggestions_layout.addStretch()
|
|
352
|
+
|
|
353
|
+
scroll.setWidget(self.suggestions_container)
|
|
354
|
+
container_layout.addWidget(scroll, 1)
|
|
355
|
+
|
|
356
|
+
# Footer with hint
|
|
357
|
+
hint = QLabel("Press 1-9 to insert • ↑↓ to navigate • Enter to insert selected • Esc to close")
|
|
358
|
+
hint.setStyleSheet("font-size: 9px; color: #999; padding-top: 4px;")
|
|
359
|
+
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
360
|
+
container_layout.addWidget(hint)
|
|
361
|
+
|
|
362
|
+
main_layout.addWidget(container)
|
|
363
|
+
|
|
364
|
+
# Position popup near cursor
|
|
365
|
+
self._position_near_cursor()
|
|
366
|
+
|
|
367
|
+
def _position_near_cursor(self):
|
|
368
|
+
"""Position the popup near the cursor (only if no saved position)"""
|
|
369
|
+
# Skip if we restored a saved position
|
|
370
|
+
if getattr(self, '_has_saved_position', False):
|
|
371
|
+
# Verify saved position is still on a valid screen
|
|
372
|
+
screen = QApplication.screenAt(self.pos())
|
|
373
|
+
if screen:
|
|
374
|
+
return # Saved position is valid, use it
|
|
375
|
+
|
|
376
|
+
# Position near cursor
|
|
377
|
+
cursor_pos = QCursor.pos()
|
|
378
|
+
screen = QApplication.screenAt(cursor_pos)
|
|
379
|
+
if screen:
|
|
380
|
+
screen_geo = screen.availableGeometry()
|
|
381
|
+
|
|
382
|
+
# Try to position popup below and to the right of cursor
|
|
383
|
+
x = cursor_pos.x() + 10
|
|
384
|
+
y = cursor_pos.y() + 10
|
|
385
|
+
|
|
386
|
+
# Ensure popup stays on screen
|
|
387
|
+
if x + self.width() > screen_geo.right():
|
|
388
|
+
x = cursor_pos.x() - self.width() - 10
|
|
389
|
+
if y + self.height() > screen_geo.bottom():
|
|
390
|
+
y = cursor_pos.y() - self.height() - 10
|
|
391
|
+
|
|
392
|
+
# Clamp to screen bounds
|
|
393
|
+
x = max(screen_geo.left(), min(x, screen_geo.right() - self.width()))
|
|
394
|
+
y = max(screen_geo.top(), min(y, screen_geo.bottom() - self.height()))
|
|
395
|
+
|
|
396
|
+
self.move(x, y)
|
|
397
|
+
|
|
398
|
+
def setup_shortcuts(self):
|
|
399
|
+
"""Setup keyboard shortcuts"""
|
|
400
|
+
# Number keys 1-9 for quick selection
|
|
401
|
+
for i in range(1, 10):
|
|
402
|
+
shortcut = QShortcut(QKeySequence(str(i)), self)
|
|
403
|
+
shortcut.activated.connect(lambda idx=i: self._select_by_number(idx))
|
|
404
|
+
|
|
405
|
+
# Navigation
|
|
406
|
+
QShortcut(QKeySequence(Qt.Key.Key_Up), self).activated.connect(self._navigate_up)
|
|
407
|
+
QShortcut(QKeySequence(Qt.Key.Key_Down), self).activated.connect(self._navigate_down)
|
|
408
|
+
QShortcut(QKeySequence(Qt.Key.Key_Return), self).activated.connect(self._insert_selected)
|
|
409
|
+
QShortcut(QKeySequence(Qt.Key.Key_Enter), self).activated.connect(self._insert_selected)
|
|
410
|
+
QShortcut(QKeySequence(Qt.Key.Key_Escape), self).activated.connect(self.close)
|
|
411
|
+
|
|
412
|
+
def start_fetching(self):
|
|
413
|
+
"""Start fetching translations from all enabled providers"""
|
|
414
|
+
providers = self._get_enabled_providers()
|
|
415
|
+
|
|
416
|
+
if not providers:
|
|
417
|
+
self.loading_label.setText("⚠️ No MT providers configured. Check Settings → MT Settings.")
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
self.worker = MTFetchWorker(
|
|
421
|
+
self.source_text,
|
|
422
|
+
self.source_lang,
|
|
423
|
+
self.target_lang,
|
|
424
|
+
providers,
|
|
425
|
+
self
|
|
426
|
+
)
|
|
427
|
+
self.worker.result_ready.connect(self._on_result_ready)
|
|
428
|
+
self.worker.all_complete.connect(self._on_all_complete)
|
|
429
|
+
self.worker.start()
|
|
430
|
+
|
|
431
|
+
def _get_enabled_providers(self) -> List[Tuple[str, str, callable]]:
|
|
432
|
+
"""Get list of enabled MT providers with their call functions"""
|
|
433
|
+
providers = []
|
|
434
|
+
|
|
435
|
+
if not self.parent_app:
|
|
436
|
+
return providers
|
|
437
|
+
|
|
438
|
+
api_keys = {}
|
|
439
|
+
enabled_providers = {}
|
|
440
|
+
|
|
441
|
+
if hasattr(self.parent_app, 'load_api_keys'):
|
|
442
|
+
api_keys = self.parent_app.load_api_keys()
|
|
443
|
+
if hasattr(self.parent_app, 'load_provider_enabled_states'):
|
|
444
|
+
enabled_providers = self.parent_app.load_provider_enabled_states()
|
|
445
|
+
|
|
446
|
+
# Load MT Quick Lookup specific settings
|
|
447
|
+
mt_quick_settings = self._load_mt_quick_settings()
|
|
448
|
+
|
|
449
|
+
# Define MT providers: (display_name, code, enabled_key, api_key_name, call_method_name)
|
|
450
|
+
mt_provider_defs = [
|
|
451
|
+
("Google Translate", "GT", "mt_google_translate", "google_translate", "call_google_translate"),
|
|
452
|
+
("DeepL", "DL", "mt_deepl", "deepl", "call_deepl"),
|
|
453
|
+
("Microsoft Translator", "MS", "mt_microsoft", "microsoft_translate", "call_microsoft_translate"),
|
|
454
|
+
("Amazon Translate", "AT", "mt_amazon", "amazon_translate", "call_amazon_translate"),
|
|
455
|
+
("ModernMT", "MMT", "mt_modernmt", "modernmt", "call_modernmt"),
|
|
456
|
+
("MyMemory", "MM", "mt_mymemory", None, "call_mymemory"), # MyMemory works without key
|
|
457
|
+
]
|
|
458
|
+
|
|
459
|
+
for name, code, enabled_key, api_key_name, method_name in mt_provider_defs:
|
|
460
|
+
# Check if provider is enabled in MT Quick Lookup settings (default: use MT Settings state)
|
|
461
|
+
quick_lookup_key = f"mtql_{code.lower()}"
|
|
462
|
+
if not mt_quick_settings.get(quick_lookup_key, enabled_providers.get(enabled_key, True)):
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
# Check if API key is available (MyMemory doesn't require one)
|
|
466
|
+
if api_key_name and not api_keys.get(api_key_name):
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
# Get the call method
|
|
470
|
+
if hasattr(self.parent_app, method_name):
|
|
471
|
+
call_method = getattr(self.parent_app, method_name)
|
|
472
|
+
|
|
473
|
+
# Create a wrapper that handles the API key
|
|
474
|
+
api_key = api_keys.get(api_key_name) if api_key_name else None
|
|
475
|
+
|
|
476
|
+
def make_caller(m, k):
|
|
477
|
+
return lambda text, src, tgt: m(text, src, tgt, k)
|
|
478
|
+
|
|
479
|
+
providers.append((name, code, make_caller(call_method, api_key)))
|
|
480
|
+
|
|
481
|
+
# Add LLM providers if enabled
|
|
482
|
+
self._add_llm_providers(providers, api_keys, mt_quick_settings)
|
|
483
|
+
|
|
484
|
+
return providers
|
|
485
|
+
|
|
486
|
+
def _load_mt_quick_settings(self) -> Dict[str, Any]:
|
|
487
|
+
"""Load MT Quick Lookup specific settings"""
|
|
488
|
+
if hasattr(self.parent_app, 'load_general_settings'):
|
|
489
|
+
settings = self.parent_app.load_general_settings()
|
|
490
|
+
return settings.get('mt_quick_lookup', {})
|
|
491
|
+
return {}
|
|
492
|
+
|
|
493
|
+
def _add_llm_providers(self, providers: List, api_keys: Dict, mt_quick_settings: Dict):
|
|
494
|
+
"""Add LLM providers (Claude, OpenAI, Gemini) to the providers list"""
|
|
495
|
+
# LLM provider definitions: (name, code, api_key_name, settings_key)
|
|
496
|
+
llm_defs = [
|
|
497
|
+
("Claude", "CL", "claude", "mtql_claude"),
|
|
498
|
+
("OpenAI", "GPT", "openai", "mtql_openai"),
|
|
499
|
+
("Gemini", "GEM", "gemini", "mtql_gemini"),
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
for name, code, api_key_name, settings_key in llm_defs:
|
|
503
|
+
# Check if LLM is enabled in MT Quick Lookup settings (default: disabled)
|
|
504
|
+
if not mt_quick_settings.get(settings_key, False):
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
# Check if API key is available
|
|
508
|
+
if not api_keys.get(api_key_name):
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
# Get model from settings or use default
|
|
512
|
+
model_key = f"{settings_key}_model"
|
|
513
|
+
model = mt_quick_settings.get(model_key, None)
|
|
514
|
+
|
|
515
|
+
# Create LLM translation caller
|
|
516
|
+
def make_llm_caller(provider_name, provider_key, provider_model):
|
|
517
|
+
def call_llm(text, src_lang, tgt_lang):
|
|
518
|
+
return self._call_llm_translation(provider_key, text, src_lang, tgt_lang, provider_model)
|
|
519
|
+
return call_llm
|
|
520
|
+
|
|
521
|
+
providers.append((name, code, make_llm_caller(name, api_key_name, model)))
|
|
522
|
+
|
|
523
|
+
def _call_llm_translation(self, provider: str, text: str, source_lang: str, target_lang: str, model: str = None) -> str:
|
|
524
|
+
"""Call LLM for translation"""
|
|
525
|
+
try:
|
|
526
|
+
from modules.llm_clients import LLMClient, load_api_keys
|
|
527
|
+
|
|
528
|
+
api_keys = load_api_keys()
|
|
529
|
+
api_key = api_keys.get(provider)
|
|
530
|
+
|
|
531
|
+
if not api_key:
|
|
532
|
+
return f"[Error: No API key for {provider}]"
|
|
533
|
+
|
|
534
|
+
client = LLMClient(
|
|
535
|
+
api_key=api_key,
|
|
536
|
+
provider=provider,
|
|
537
|
+
model=model
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Use the translate method
|
|
541
|
+
result = client.translate(
|
|
542
|
+
text=text,
|
|
543
|
+
source_lang=source_lang,
|
|
544
|
+
target_lang=target_lang
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Clean up result - remove quotes if present
|
|
548
|
+
if result:
|
|
549
|
+
result = result.strip()
|
|
550
|
+
if (result.startswith('"') and result.endswith('"')) or (result.startswith("'") and result.endswith("'")):
|
|
551
|
+
result = result[1:-1]
|
|
552
|
+
|
|
553
|
+
return result or "[No translation returned]"
|
|
554
|
+
|
|
555
|
+
except Exception as e:
|
|
556
|
+
return f"[Error: {str(e)}]"
|
|
557
|
+
|
|
558
|
+
def _open_settings(self):
|
|
559
|
+
"""Open MT Quick Lookup settings tab"""
|
|
560
|
+
if self.parent_app and hasattr(self.parent_app, 'open_mt_quick_lookup_settings'):
|
|
561
|
+
self.close() # Close popup first
|
|
562
|
+
self.parent_app.open_mt_quick_lookup_settings()
|
|
563
|
+
|
|
564
|
+
def _on_result_ready(self, provider_name: str, provider_code: str, translation: str, is_error: bool):
|
|
565
|
+
"""Handle a single MT result"""
|
|
566
|
+
# Hide loading label on first result
|
|
567
|
+
if self.loading_label.isVisible():
|
|
568
|
+
self.loading_label.hide()
|
|
569
|
+
|
|
570
|
+
# Create suggestion
|
|
571
|
+
suggestion = MTSuggestion(
|
|
572
|
+
provider_name=provider_name,
|
|
573
|
+
provider_code=provider_code,
|
|
574
|
+
translation=translation,
|
|
575
|
+
is_error=is_error
|
|
576
|
+
)
|
|
577
|
+
self.suggestions.append(suggestion)
|
|
578
|
+
|
|
579
|
+
# Create and add item widget
|
|
580
|
+
item = MTSuggestionItem(len(self.suggestions), suggestion)
|
|
581
|
+
item.clicked.connect(self._on_item_clicked)
|
|
582
|
+
self.suggestion_items.append(item)
|
|
583
|
+
|
|
584
|
+
# Insert before the stretch
|
|
585
|
+
self.suggestions_layout.insertWidget(self.suggestions_layout.count() - 1, item)
|
|
586
|
+
|
|
587
|
+
# Auto-select first non-error result
|
|
588
|
+
if self.selected_index == -1 and not is_error:
|
|
589
|
+
self._select_index(len(self.suggestion_items) - 1)
|
|
590
|
+
|
|
591
|
+
def _on_all_complete(self):
|
|
592
|
+
"""Handle completion of all MT fetches"""
|
|
593
|
+
if not self.suggestions:
|
|
594
|
+
self.loading_label.setText("⚠️ No translations available.")
|
|
595
|
+
self.loading_label.show()
|
|
596
|
+
# Don't call adjustSize() - it shrinks the window and loses user's preferred size
|
|
597
|
+
|
|
598
|
+
def _on_item_clicked(self, translation: str):
|
|
599
|
+
"""Handle click on a suggestion item"""
|
|
600
|
+
self.translation_selected.emit(translation)
|
|
601
|
+
self.close()
|
|
602
|
+
|
|
603
|
+
def _select_by_number(self, number: int):
|
|
604
|
+
"""Select suggestion by number (1-based)"""
|
|
605
|
+
idx = number - 1
|
|
606
|
+
if 0 <= idx < len(self.suggestion_items):
|
|
607
|
+
suggestion = self.suggestions[idx]
|
|
608
|
+
if not suggestion.is_error:
|
|
609
|
+
self.translation_selected.emit(suggestion.translation)
|
|
610
|
+
self.close()
|
|
611
|
+
|
|
612
|
+
def _select_index(self, index: int):
|
|
613
|
+
"""Select suggestion by index"""
|
|
614
|
+
# Deselect previous
|
|
615
|
+
if 0 <= self.selected_index < len(self.suggestion_items):
|
|
616
|
+
self.suggestion_items[self.selected_index].deselect()
|
|
617
|
+
|
|
618
|
+
# Select new (skip errors)
|
|
619
|
+
if 0 <= index < len(self.suggestion_items):
|
|
620
|
+
self.selected_index = index
|
|
621
|
+
self.suggestion_items[index].select()
|
|
622
|
+
# Ensure visible
|
|
623
|
+
self.suggestion_items[index].setFocus()
|
|
624
|
+
|
|
625
|
+
def _navigate_up(self):
|
|
626
|
+
"""Navigate to previous suggestion"""
|
|
627
|
+
if not self.suggestion_items:
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
new_idx = self.selected_index - 1
|
|
631
|
+
while new_idx >= 0:
|
|
632
|
+
if not self.suggestions[new_idx].is_error:
|
|
633
|
+
self._select_index(new_idx)
|
|
634
|
+
return
|
|
635
|
+
new_idx -= 1
|
|
636
|
+
|
|
637
|
+
def _navigate_down(self):
|
|
638
|
+
"""Navigate to next suggestion"""
|
|
639
|
+
if not self.suggestion_items:
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
new_idx = self.selected_index + 1
|
|
643
|
+
while new_idx < len(self.suggestions):
|
|
644
|
+
if not self.suggestions[new_idx].is_error:
|
|
645
|
+
self._select_index(new_idx)
|
|
646
|
+
return
|
|
647
|
+
new_idx += 1
|
|
648
|
+
|
|
649
|
+
def _insert_selected(self):
|
|
650
|
+
"""Insert the currently selected suggestion"""
|
|
651
|
+
if 0 <= self.selected_index < len(self.suggestions):
|
|
652
|
+
suggestion = self.suggestions[self.selected_index]
|
|
653
|
+
if not suggestion.is_error:
|
|
654
|
+
self.translation_selected.emit(suggestion.translation)
|
|
655
|
+
self.close()
|
|
656
|
+
|
|
657
|
+
def closeEvent(self, event):
|
|
658
|
+
"""Clean up worker on close and save window size and position"""
|
|
659
|
+
# Save window size and position for next time
|
|
660
|
+
settings = QSettings("Supervertaler", "MTQuickPopup")
|
|
661
|
+
settings.setValue("width", self.width())
|
|
662
|
+
settings.setValue("height", self.height())
|
|
663
|
+
settings.setValue("x", self.x())
|
|
664
|
+
settings.setValue("y", self.y())
|
|
665
|
+
|
|
666
|
+
# Clean up worker
|
|
667
|
+
if self.worker and self.worker.isRunning():
|
|
668
|
+
self.worker.quit()
|
|
669
|
+
self.worker.wait(1000)
|
|
670
|
+
super().closeEvent(event)
|