supervertaler 1.9.163__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 +48473 -0
- modules/__init__.py +10 -0
- modules/ai_actions.py +964 -0
- modules/ai_attachment_manager.py +343 -0
- modules/ai_file_viewer_dialog.py +210 -0
- modules/autofingers_engine.py +466 -0
- modules/cafetran_docx_handler.py +379 -0
- modules/config_manager.py +469 -0
- modules/database_manager.py +1911 -0
- modules/database_migrations.py +417 -0
- modules/dejavurtf_handler.py +779 -0
- modules/document_analyzer.py +427 -0
- modules/docx_handler.py +689 -0
- modules/encoding_repair.py +319 -0
- modules/encoding_repair_Qt.py +393 -0
- modules/encoding_repair_ui.py +481 -0
- modules/feature_manager.py +350 -0
- modules/figure_context_manager.py +340 -0
- modules/file_dialog_helper.py +148 -0
- modules/find_replace.py +164 -0
- modules/find_replace_qt.py +457 -0
- modules/glossary_manager.py +433 -0
- modules/image_extractor.py +188 -0
- modules/keyboard_shortcuts_widget.py +571 -0
- modules/llm_clients.py +1211 -0
- modules/llm_leaderboard.py +737 -0
- modules/llm_superbench_ui.py +1401 -0
- modules/local_llm_setup.py +1104 -0
- modules/model_update_dialog.py +381 -0
- modules/model_version_checker.py +373 -0
- modules/mqxliff_handler.py +638 -0
- modules/non_translatables_manager.py +743 -0
- modules/pdf_rescue_Qt.py +1822 -0
- modules/pdf_rescue_tkinter.py +909 -0
- modules/phrase_docx_handler.py +516 -0
- modules/project_home_panel.py +209 -0
- modules/prompt_assistant.py +357 -0
- modules/prompt_library.py +689 -0
- modules/prompt_library_migration.py +447 -0
- modules/quick_access_sidebar.py +282 -0
- modules/ribbon_widget.py +597 -0
- modules/sdlppx_handler.py +874 -0
- modules/setup_wizard.py +353 -0
- modules/shortcut_manager.py +932 -0
- modules/simple_segmenter.py +128 -0
- modules/spellcheck_manager.py +727 -0
- modules/statuses.py +207 -0
- modules/style_guide_manager.py +315 -0
- modules/superbench_ui.py +1319 -0
- modules/superbrowser.py +329 -0
- modules/supercleaner.py +600 -0
- modules/supercleaner_ui.py +444 -0
- modules/superdocs.py +19 -0
- modules/superdocs_viewer_qt.py +382 -0
- modules/superlookup.py +252 -0
- modules/tag_cleaner.py +260 -0
- modules/tag_manager.py +351 -0
- modules/term_extractor.py +270 -0
- modules/termbase_entry_editor.py +842 -0
- modules/termbase_import_export.py +488 -0
- modules/termbase_manager.py +1060 -0
- modules/termview_widget.py +1176 -0
- modules/theme_manager.py +499 -0
- modules/tm_editor_dialog.py +99 -0
- modules/tm_manager_qt.py +1280 -0
- modules/tm_metadata_manager.py +545 -0
- modules/tmx_editor.py +1461 -0
- modules/tmx_editor_qt.py +2784 -0
- modules/tmx_generator.py +284 -0
- modules/tracked_changes.py +900 -0
- modules/trados_docx_handler.py +430 -0
- modules/translation_memory.py +715 -0
- modules/translation_results_panel.py +2134 -0
- modules/translation_services.py +282 -0
- modules/unified_prompt_library.py +659 -0
- modules/unified_prompt_manager_qt.py +3951 -0
- modules/voice_commands.py +920 -0
- modules/voice_dictation.py +477 -0
- modules/voice_dictation_lite.py +249 -0
- supervertaler-1.9.163.dist-info/METADATA +906 -0
- supervertaler-1.9.163.dist-info/RECORD +85 -0
- supervertaler-1.9.163.dist-info/WHEEL +5 -0
- supervertaler-1.9.163.dist-info/entry_points.txt +2 -0
- supervertaler-1.9.163.dist-info/licenses/LICENSE +21 -0
- supervertaler-1.9.163.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,2134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Translation Results Panel
|
|
3
|
+
Compact memoQ-style right-side panel for displaying translation matches
|
|
4
|
+
Supports stacked match sections, drag/drop, and compare boxes with diff highlighting
|
|
5
|
+
|
|
6
|
+
Keyboard Shortcuts:
|
|
7
|
+
- ↑/↓ arrows: Navigate through matches (cycle through sections)
|
|
8
|
+
- Spacebar/Enter: Insert currently selected match into target cell
|
|
9
|
+
- Ctrl+1-9: Insert specific match directly (by number, global across all sections)
|
|
10
|
+
- Escape: Deselect match (when focus on panel)
|
|
11
|
+
|
|
12
|
+
Compare boxes: Vertical stacked with resizable splitter
|
|
13
|
+
Text display: Supports long segments with text wrapping
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from PyQt6.QtWidgets import (
|
|
17
|
+
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
18
|
+
QFrame, QScrollArea, QTextEdit, QSplitter, QTabWidget
|
|
19
|
+
)
|
|
20
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QMimeData
|
|
21
|
+
from PyQt6.QtGui import QDrag, QCursor, QFont, QColor, QTextCharFormat, QTextCursor
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import List, Optional, Dict, Any
|
|
24
|
+
import difflib
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TranslationMatch:
|
|
29
|
+
"""Represents a single translation match"""
|
|
30
|
+
source: str
|
|
31
|
+
target: str
|
|
32
|
+
relevance: int # 0-100
|
|
33
|
+
metadata: Dict[str, Any] # Context, domain, timestamp, etc.
|
|
34
|
+
match_type: str # "NT", "MT", "TM", "Termbase", "LLM"
|
|
35
|
+
compare_source: Optional[str] = None # For TM compare boxes
|
|
36
|
+
provider_code: Optional[str] = None # Provider code: "GT", "AT", "MMT", "CL", "GPT", "GEM", etc.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CompactMatchItem(QFrame):
|
|
40
|
+
"""Compact match display (like memoQ) with source and target in separate columns"""
|
|
41
|
+
|
|
42
|
+
match_selected = pyqtSignal(TranslationMatch)
|
|
43
|
+
|
|
44
|
+
# Class variables (can be changed globally)
|
|
45
|
+
font_size_pt = 9
|
|
46
|
+
show_tags = False # When False, HTML/XML tags are hidden
|
|
47
|
+
tag_highlight_color = '#7f0001' # Default memoQ dark red for tag highlighting
|
|
48
|
+
badge_text_color = '#333333' # Dark gray for badge text (readable without being harsh)
|
|
49
|
+
theme_manager = None # Class-level theme manager reference
|
|
50
|
+
|
|
51
|
+
def __init__(self, match: TranslationMatch, match_number: int = 0, parent=None):
|
|
52
|
+
super().__init__(parent)
|
|
53
|
+
self.match = match
|
|
54
|
+
self.match_number = match_number
|
|
55
|
+
self.is_selected = False
|
|
56
|
+
self.num_label_ref = None # Initialize FIRST before update_styling()
|
|
57
|
+
self.source_label = None
|
|
58
|
+
self.target_label = None
|
|
59
|
+
|
|
60
|
+
self.setFrameStyle(QFrame.Shape.NoFrame) # No frame border
|
|
61
|
+
self.setMinimumHeight(20) # Minimum height (can expand)
|
|
62
|
+
self.setMaximumHeight(100) # Allow up to 100px if text wraps
|
|
63
|
+
|
|
64
|
+
# Vertical layout with 2 rows: number+relevance on left, then source and target on right
|
|
65
|
+
main_layout = QHBoxLayout(self)
|
|
66
|
+
main_layout.setContentsMargins(2, 1, 2, 1) # Minimal padding
|
|
67
|
+
main_layout.setSpacing(3)
|
|
68
|
+
|
|
69
|
+
# Left side: Match number box (small colored box)
|
|
70
|
+
if match_number > 0:
|
|
71
|
+
num_label = QLabel(f"{match_number}")
|
|
72
|
+
num_label.setStyleSheet("""
|
|
73
|
+
QLabel {
|
|
74
|
+
font-weight: bold;
|
|
75
|
+
font-size: 9px;
|
|
76
|
+
padding: 1px;
|
|
77
|
+
border-radius: 2px;
|
|
78
|
+
margin: 0px;
|
|
79
|
+
}
|
|
80
|
+
""")
|
|
81
|
+
num_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
82
|
+
num_label.setFixedWidth(22)
|
|
83
|
+
num_label.setFixedHeight(18)
|
|
84
|
+
|
|
85
|
+
# Add tooltip based on match type
|
|
86
|
+
match_type_tooltips = {
|
|
87
|
+
"LLM": "LLM Translation (AI-generated)",
|
|
88
|
+
"TM": "Translation Memory (Previously approved)",
|
|
89
|
+
"Termbase": "Termbase",
|
|
90
|
+
"MT": "Machine Translation",
|
|
91
|
+
"NT": "New Translation",
|
|
92
|
+
"NonTrans": "🚫 Non-Translatable (do not translate)"
|
|
93
|
+
}
|
|
94
|
+
tooltip_text = match_type_tooltips.get(match.match_type, "Translation Match")
|
|
95
|
+
num_label.setToolTip(tooltip_text)
|
|
96
|
+
|
|
97
|
+
self.num_label_ref = num_label # Set BEFORE calling update_styling()
|
|
98
|
+
main_layout.addWidget(num_label, 0, Qt.AlignmentFlag.AlignTop)
|
|
99
|
+
|
|
100
|
+
# Get theme color for secondary text
|
|
101
|
+
secondary_text_color = "#666"
|
|
102
|
+
if CompactMatchItem.theme_manager:
|
|
103
|
+
secondary_text_color = CompactMatchItem.theme_manager.current_theme.text_disabled
|
|
104
|
+
|
|
105
|
+
# Middle: Relevance % (vertical)
|
|
106
|
+
rel_label = QLabel(f"{match.relevance}%")
|
|
107
|
+
rel_label.setStyleSheet(f"font-size: 7px; color: {secondary_text_color}; padding: 0px; margin: 0px;")
|
|
108
|
+
rel_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
109
|
+
rel_label.setFixedWidth(32)
|
|
110
|
+
rel_label.setFixedHeight(18)
|
|
111
|
+
main_layout.addWidget(rel_label, 0, Qt.AlignmentFlag.AlignTop)
|
|
112
|
+
|
|
113
|
+
# Right side: Source and Target in a horizontal layout (like spreadsheet columns)
|
|
114
|
+
content_layout = QHBoxLayout()
|
|
115
|
+
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
116
|
+
content_layout.setSpacing(6)
|
|
117
|
+
|
|
118
|
+
# Get theme colors for text
|
|
119
|
+
if CompactMatchItem.theme_manager:
|
|
120
|
+
theme = CompactMatchItem.theme_manager.current_theme
|
|
121
|
+
source_color = theme.text
|
|
122
|
+
# Use slightly dimmer text for target, but not as dim as text_disabled
|
|
123
|
+
# For dark themes, use a color between text and text_disabled for better readability
|
|
124
|
+
is_dark = theme.name == "Dark"
|
|
125
|
+
target_color = "#B0B0B0" if is_dark else "#555"
|
|
126
|
+
else:
|
|
127
|
+
source_color = "#333"
|
|
128
|
+
target_color = "#555"
|
|
129
|
+
|
|
130
|
+
# Source column - NO truncation, allow wrapping
|
|
131
|
+
self.source_label = QLabel(self._format_text(match.source))
|
|
132
|
+
self.source_label.setWordWrap(True) # Allow wrapping
|
|
133
|
+
# Always use RichText when tags are shown (for highlighting), otherwise RichText for rendering
|
|
134
|
+
self.source_label.setTextFormat(Qt.TextFormat.RichText)
|
|
135
|
+
self.source_label.setStyleSheet(f"font-size: {self.font_size_pt}px; color: {source_color}; padding: 0px; margin: 0px;")
|
|
136
|
+
self.source_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
|
137
|
+
self.source_label.setMinimumWidth(150) # Much wider minimum
|
|
138
|
+
content_layout.addWidget(self.source_label, 1)
|
|
139
|
+
|
|
140
|
+
# Target column - NO truncation, allow wrapping
|
|
141
|
+
self.target_label = QLabel(self._format_text(match.target))
|
|
142
|
+
self.target_label.setWordWrap(True) # Allow wrapping
|
|
143
|
+
# Always use RichText when tags are shown (for highlighting), otherwise RichText for rendering
|
|
144
|
+
self.target_label.setTextFormat(Qt.TextFormat.RichText)
|
|
145
|
+
self.target_label.setStyleSheet(f"font-size: {self.font_size_pt}px; color: {target_color}; padding: 0px; margin: 0px;")
|
|
146
|
+
self.target_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
|
147
|
+
self.target_label.setMinimumWidth(150) # Much wider minimum
|
|
148
|
+
content_layout.addWidget(self.target_label, 1)
|
|
149
|
+
|
|
150
|
+
# Provider code column (tiny, after target text) - always reserve space for alignment
|
|
151
|
+
# Determine provider code text and styling
|
|
152
|
+
provider_code_text = match.provider_code if match.provider_code else ""
|
|
153
|
+
|
|
154
|
+
# Determine if this is a project termbase or project TM
|
|
155
|
+
# For termbases: explicit flag OR ranking #1 = project termbase
|
|
156
|
+
is_project_tb_flag = match.match_type == 'Termbase' and match.metadata.get('is_project_termbase', False)
|
|
157
|
+
is_ranking_1 = match.match_type == 'Termbase' and match.metadata.get('ranking') == 1
|
|
158
|
+
is_project_tb = is_project_tb_flag or is_ranking_1
|
|
159
|
+
is_project_tm = match.match_type == 'TM' and match.metadata.get('is_project_tm', False)
|
|
160
|
+
|
|
161
|
+
provider_label = QLabel(provider_code_text)
|
|
162
|
+
|
|
163
|
+
# Use bold font for project termbases/TMs, normal font for background resources
|
|
164
|
+
font_weight = "bold" if (is_project_tb or is_project_tm) else "normal"
|
|
165
|
+
# Use theme color for text
|
|
166
|
+
provider_text_color = secondary_text_color # Reuse the secondary text color from above
|
|
167
|
+
provider_label.setStyleSheet(f"font-size: 7px; color: {provider_text_color}; padding: 0px; margin: 0px; font-weight: {font_weight};")
|
|
168
|
+
provider_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
169
|
+
provider_label.setFixedWidth(28) # Tiny column, just wide enough for "GPT", "MMT", etc.
|
|
170
|
+
provider_label.setFixedHeight(18)
|
|
171
|
+
# Add tooltip with full provider name (only if code exists)
|
|
172
|
+
if match.provider_code:
|
|
173
|
+
provider_tooltips = {
|
|
174
|
+
"GT": "Google Translate",
|
|
175
|
+
"AT": "Amazon Translate",
|
|
176
|
+
"MMT": "ModernMT",
|
|
177
|
+
"DL": "DeepL",
|
|
178
|
+
"MS": "Microsoft Translator",
|
|
179
|
+
"MM": "MyMemory",
|
|
180
|
+
"CL": "Claude",
|
|
181
|
+
"GPT": "OpenAI",
|
|
182
|
+
"GEM": "Gemini"
|
|
183
|
+
}
|
|
184
|
+
# Add any custom termbase codes to tooltips (they'll show termbase name from metadata)
|
|
185
|
+
if match.match_type == 'Termbase' and match.metadata.get('termbase_name'):
|
|
186
|
+
provider_tooltips[match.provider_code] = match.metadata.get('termbase_name', match.provider_code)
|
|
187
|
+
full_name = provider_tooltips.get(match.provider_code, match.provider_code)
|
|
188
|
+
provider_label.setToolTip(full_name)
|
|
189
|
+
content_layout.addWidget(provider_label, 0, Qt.AlignmentFlag.AlignTop)
|
|
190
|
+
|
|
191
|
+
main_layout.addLayout(content_layout, 1) # Expand to fill remaining space
|
|
192
|
+
|
|
193
|
+
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
194
|
+
|
|
195
|
+
# NOW call update_styling() after num_label_ref is set
|
|
196
|
+
self.update_styling()
|
|
197
|
+
|
|
198
|
+
def _format_text(self, text: str) -> str:
|
|
199
|
+
"""Format text based on show_tags setting"""
|
|
200
|
+
if self.show_tags:
|
|
201
|
+
# Show tags with text color
|
|
202
|
+
import re
|
|
203
|
+
# Escape HTML entities first to prevent double-escaping
|
|
204
|
+
text = text.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
205
|
+
# Now color the escaped tags
|
|
206
|
+
tag_pattern = re.compile(r'</?[a-zA-Z][a-zA-Z0-9]*/?>')
|
|
207
|
+
text = tag_pattern.sub(lambda m: f'<span style="color: {self.tag_highlight_color};">{m.group()}</span>', text)
|
|
208
|
+
return text
|
|
209
|
+
else:
|
|
210
|
+
# Let QLabel interpret as HTML (tags will be rendered/hidden)
|
|
211
|
+
return text
|
|
212
|
+
|
|
213
|
+
def update_tag_color(self, color: str):
|
|
214
|
+
"""Update tag highlight color for this item"""
|
|
215
|
+
self.tag_highlight_color = color
|
|
216
|
+
# Refresh text if tags are shown
|
|
217
|
+
if self.show_tags and self.source_label and self.target_label:
|
|
218
|
+
self.source_label.setText(self._format_text(self.match.source))
|
|
219
|
+
self.target_label.setText(self._format_text(self.match.target))
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def set_font_size(cls, size: int):
|
|
223
|
+
"""Set the font size for all match items"""
|
|
224
|
+
cls.font_size_pt = size
|
|
225
|
+
|
|
226
|
+
def update_font_size(self):
|
|
227
|
+
"""Update font size for this item"""
|
|
228
|
+
if self.source_label:
|
|
229
|
+
self.source_label.setStyleSheet(f"font-size: {self.font_size_pt}px; color: #333; padding: 0px; margin: 0px;")
|
|
230
|
+
if self.target_label:
|
|
231
|
+
self.target_label.setStyleSheet(f"font-size: {self.font_size_pt}px; color: #555; padding: 0px; margin: 0px;")
|
|
232
|
+
|
|
233
|
+
def mousePressEvent(self, event):
|
|
234
|
+
"""Emit signal when clicked"""
|
|
235
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
236
|
+
self.match_selected.emit(self.match)
|
|
237
|
+
self.select()
|
|
238
|
+
elif event.button() == Qt.MouseButton.RightButton:
|
|
239
|
+
self._show_context_menu(event.globalPosition().toPoint())
|
|
240
|
+
|
|
241
|
+
def _show_context_menu(self, pos):
|
|
242
|
+
"""Show context menu for this match item"""
|
|
243
|
+
# Only show edit option for termbase matches
|
|
244
|
+
if self.match.match_type != "Termbase":
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
from PyQt6.QtWidgets import QMenu
|
|
248
|
+
from PyQt6.QtGui import QAction
|
|
249
|
+
|
|
250
|
+
menu = QMenu()
|
|
251
|
+
|
|
252
|
+
# Edit entry action
|
|
253
|
+
edit_action = QAction("✏️ Edit Glossary Entry", menu)
|
|
254
|
+
edit_action.triggered.connect(self._edit_termbase_entry)
|
|
255
|
+
menu.addAction(edit_action)
|
|
256
|
+
|
|
257
|
+
# Delete entry action
|
|
258
|
+
delete_action = QAction("🗑️ Delete Glossary Entry", menu)
|
|
259
|
+
delete_action.triggered.connect(self._delete_termbase_entry)
|
|
260
|
+
menu.addAction(delete_action)
|
|
261
|
+
|
|
262
|
+
menu.exec(pos)
|
|
263
|
+
|
|
264
|
+
def _edit_termbase_entry(self):
|
|
265
|
+
"""Open termbase entry editor for this match"""
|
|
266
|
+
if self.match.match_type != "Termbase":
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Get term_id and termbase_id from metadata
|
|
270
|
+
term_id = self.match.metadata.get('term_id')
|
|
271
|
+
termbase_id = self.match.metadata.get('termbase_id')
|
|
272
|
+
|
|
273
|
+
if term_id and termbase_id:
|
|
274
|
+
from modules.termbase_entry_editor import TermbaseEntryEditor
|
|
275
|
+
|
|
276
|
+
# Get parent window (main application)
|
|
277
|
+
parent_window = self.window()
|
|
278
|
+
|
|
279
|
+
dialog = TermbaseEntryEditor(
|
|
280
|
+
parent=parent_window,
|
|
281
|
+
db_manager=getattr(parent_window, 'db_manager', None),
|
|
282
|
+
termbase_id=termbase_id,
|
|
283
|
+
term_id=term_id
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if dialog.exec():
|
|
287
|
+
# Entry was edited, refresh if needed
|
|
288
|
+
# Signal could be emitted here to refresh the translation results panel
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
def _delete_termbase_entry(self):
|
|
292
|
+
"""Delete this termbase entry"""
|
|
293
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
294
|
+
|
|
295
|
+
if self.match.match_type != "Termbase":
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# Get term_id and termbase_id from metadata
|
|
299
|
+
term_id = self.match.metadata.get('term_id')
|
|
300
|
+
termbase_id = self.match.metadata.get('termbase_id')
|
|
301
|
+
source_term = self.match.source
|
|
302
|
+
target_term = self.match.target
|
|
303
|
+
|
|
304
|
+
if term_id and termbase_id:
|
|
305
|
+
# Confirm deletion
|
|
306
|
+
parent_window = self.window()
|
|
307
|
+
reply = QMessageBox.question(
|
|
308
|
+
parent_window,
|
|
309
|
+
"Confirm Deletion",
|
|
310
|
+
f"Delete glossary entry?\n\nSource: {source_term}\nTarget: {target_term}\n\nThis action cannot be undone.",
|
|
311
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
312
|
+
QMessageBox.StandardButton.No
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
316
|
+
db_manager = getattr(parent_window, 'db_manager', None)
|
|
317
|
+
if db_manager:
|
|
318
|
+
try:
|
|
319
|
+
# Log database path for debugging
|
|
320
|
+
if hasattr(parent_window, 'log'):
|
|
321
|
+
db_path = getattr(db_manager, 'db_path', 'unknown')
|
|
322
|
+
parent_window.log(f"🗑️ Deleting term ID {term_id} from database: {db_path}")
|
|
323
|
+
|
|
324
|
+
cursor = db_manager.cursor
|
|
325
|
+
# First verify the term exists
|
|
326
|
+
cursor.execute("SELECT source_term, target_term FROM termbase_terms WHERE id = ?", (term_id,))
|
|
327
|
+
existing = cursor.fetchone()
|
|
328
|
+
if hasattr(parent_window, 'log'):
|
|
329
|
+
if existing:
|
|
330
|
+
parent_window.log(f" Found term to delete: {existing[0]} → {existing[1]}")
|
|
331
|
+
else:
|
|
332
|
+
parent_window.log(f" ⚠️ Term ID {term_id} not found in database!")
|
|
333
|
+
|
|
334
|
+
# Delete the term
|
|
335
|
+
cursor.execute("DELETE FROM termbase_terms WHERE id = ?", (term_id,))
|
|
336
|
+
rows_deleted = cursor.rowcount
|
|
337
|
+
db_manager.connection.commit()
|
|
338
|
+
|
|
339
|
+
if hasattr(parent_window, 'log'):
|
|
340
|
+
parent_window.log(f" ✅ Deleted {rows_deleted} row(s) from database")
|
|
341
|
+
|
|
342
|
+
# Clear termbase cache to force refresh
|
|
343
|
+
if hasattr(parent_window, 'termbase_cache'):
|
|
344
|
+
with parent_window.termbase_cache_lock:
|
|
345
|
+
parent_window.termbase_cache.clear()
|
|
346
|
+
if hasattr(parent_window, 'log'):
|
|
347
|
+
parent_window.log(f" ✅ Cleared termbase cache")
|
|
348
|
+
|
|
349
|
+
# Reset the last selected row to force re-highlighting when returning to this segment
|
|
350
|
+
if hasattr(parent_window, '_last_selected_row'):
|
|
351
|
+
parent_window._last_selected_row = None
|
|
352
|
+
|
|
353
|
+
# Trigger re-highlighting of source text to remove deleted term
|
|
354
|
+
if hasattr(parent_window, 'table') and hasattr(parent_window, 'find_termbase_matches_in_source'):
|
|
355
|
+
current_row = parent_window.table.currentRow()
|
|
356
|
+
if current_row >= 0:
|
|
357
|
+
# Get source text widget
|
|
358
|
+
source_widget = parent_window.table.cellWidget(current_row, 2)
|
|
359
|
+
if source_widget and hasattr(source_widget, 'toPlainText'):
|
|
360
|
+
source_text = source_widget.toPlainText()
|
|
361
|
+
# Re-find matches and re-highlight
|
|
362
|
+
termbase_matches = parent_window.find_termbase_matches_in_source(source_text)
|
|
363
|
+
if hasattr(source_widget, 'highlight_termbase_matches'):
|
|
364
|
+
source_widget.highlight_termbase_matches(termbase_matches)
|
|
365
|
+
# Update the widget's stored matches to reflect the deletion
|
|
366
|
+
if hasattr(source_widget, 'termbase_matches'):
|
|
367
|
+
source_widget.termbase_matches = termbase_matches
|
|
368
|
+
|
|
369
|
+
QMessageBox.information(parent_window, "Success", "Glossary entry deleted")
|
|
370
|
+
# Hide this match card since it's been deleted
|
|
371
|
+
self.hide()
|
|
372
|
+
except Exception as e:
|
|
373
|
+
QMessageBox.critical(parent_window, "Error", f"Failed to delete entry: {e}")
|
|
374
|
+
|
|
375
|
+
def select(self):
|
|
376
|
+
"""Select this match"""
|
|
377
|
+
self.is_selected = True
|
|
378
|
+
self.update_styling()
|
|
379
|
+
|
|
380
|
+
def deselect(self):
|
|
381
|
+
"""Deselect this match"""
|
|
382
|
+
self.is_selected = False
|
|
383
|
+
self.update_styling()
|
|
384
|
+
|
|
385
|
+
def update_styling(self):
|
|
386
|
+
"""Update visual styling based on selection state and match type"""
|
|
387
|
+
# Color code by match type: LLM=purple, TM=red, Termbase=green, MT=orange, NT=gray, NonTrans=yellow
|
|
388
|
+
base_color_map = {
|
|
389
|
+
"LLM": "#9c27b0", # Purple for LLM translations
|
|
390
|
+
"TM": "#ff6b6b", # Red for Translation Memory
|
|
391
|
+
"Termbase": "#4CAF50", # Green for all termbase matches (Material Design Green 500)
|
|
392
|
+
"MT": "#ff9800", # Orange for Machine Translation
|
|
393
|
+
"NT": "#adb5bd", # Gray for New Translation
|
|
394
|
+
"NonTrans": "#E6C200" # Pastel yellow for Non-Translatables
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
base_color = base_color_map.get(self.match.match_type, "#adb5bd")
|
|
398
|
+
|
|
399
|
+
# Special styling for Non-Translatables
|
|
400
|
+
if self.match.match_type == "NonTrans":
|
|
401
|
+
type_color = "#FFFDD0" # Pastel yellow background
|
|
402
|
+
# For termbase matches, apply ranking-based green shading
|
|
403
|
+
elif self.match.match_type == "Termbase":
|
|
404
|
+
is_forbidden = self.match.metadata.get('forbidden', False)
|
|
405
|
+
is_project_termbase_flag = self.match.metadata.get('is_project_termbase', False)
|
|
406
|
+
termbase_ranking = self.match.metadata.get('ranking', None)
|
|
407
|
+
|
|
408
|
+
# EFFECTIVE project termbase = explicit flag OR ranking #1
|
|
409
|
+
is_effective_project = is_project_termbase_flag or (termbase_ranking == 1)
|
|
410
|
+
is_project_termbase = is_effective_project # For later use in background styling
|
|
411
|
+
|
|
412
|
+
if is_forbidden:
|
|
413
|
+
type_color = "#000000" # Forbidden terms: black
|
|
414
|
+
else:
|
|
415
|
+
# Use ranking to determine soft pastel green shade
|
|
416
|
+
# All shades are subtle to stay in the background
|
|
417
|
+
if termbase_ranking is not None:
|
|
418
|
+
# Map ranking to soft pastel green shades:
|
|
419
|
+
# Ranking #1: Soft medium green (Green 200)
|
|
420
|
+
# Ranking #2: Soft light green (Green 100)
|
|
421
|
+
# Ranking #3: Very soft light green (Light Green 100)
|
|
422
|
+
# Ranking #4+: Extremely soft pastel green (Green 50)
|
|
423
|
+
ranking_colors = {
|
|
424
|
+
1: "#A5D6A7", # Soft medium green (Green 200)
|
|
425
|
+
2: "#C8E6C9", # Soft light green (Green 100)
|
|
426
|
+
3: "#DCEDC8", # Very soft light green (Light Green 100)
|
|
427
|
+
}
|
|
428
|
+
type_color = ranking_colors.get(termbase_ranking, "#E8F5E9") # Green 50 for 4+
|
|
429
|
+
else:
|
|
430
|
+
# No ranking - use soft light green
|
|
431
|
+
type_color = "#C8E6C9" # Green 100 (fallback)
|
|
432
|
+
else:
|
|
433
|
+
type_color = base_color
|
|
434
|
+
|
|
435
|
+
# Update styling only for the number label, not the entire item
|
|
436
|
+
if hasattr(self, 'num_label_ref') and self.num_label_ref:
|
|
437
|
+
if self.is_selected:
|
|
438
|
+
# Selected: darker shade of type color with black text and outline
|
|
439
|
+
darker_color = self._darken_color(type_color)
|
|
440
|
+
self.num_label_ref.setStyleSheet(f"""
|
|
441
|
+
QLabel {{
|
|
442
|
+
background-color: {darker_color};
|
|
443
|
+
color: black;
|
|
444
|
+
font-weight: bold;
|
|
445
|
+
font-size: 10px;
|
|
446
|
+
min-width: 22px;
|
|
447
|
+
padding: 2px;
|
|
448
|
+
border-radius: 2px;
|
|
449
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
450
|
+
}}
|
|
451
|
+
""")
|
|
452
|
+
# Add background to the entire item only when selected
|
|
453
|
+
self.setStyleSheet(f"""
|
|
454
|
+
CompactMatchItem {{
|
|
455
|
+
background-color: {self._lighten_color(type_color, 0.95)};
|
|
456
|
+
border: 1px solid {type_color};
|
|
457
|
+
}}
|
|
458
|
+
""")
|
|
459
|
+
else:
|
|
460
|
+
# Unselected: number badge colored with customizable text color and subtle outline
|
|
461
|
+
self.num_label_ref.setStyleSheet(f"""
|
|
462
|
+
QLabel {{
|
|
463
|
+
background-color: {type_color};
|
|
464
|
+
color: {CompactMatchItem.badge_text_color};
|
|
465
|
+
font-weight: bold;
|
|
466
|
+
font-size: 10px;
|
|
467
|
+
min-width: 22px;
|
|
468
|
+
padding: 1px;
|
|
469
|
+
border-radius: 2px;
|
|
470
|
+
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
471
|
+
}}
|
|
472
|
+
""")
|
|
473
|
+
# Use theme colors for background if available
|
|
474
|
+
if CompactMatchItem.theme_manager:
|
|
475
|
+
theme = CompactMatchItem.theme_manager.current_theme
|
|
476
|
+
bg_color = theme.base
|
|
477
|
+
hover_color = theme.alternate_bg
|
|
478
|
+
else:
|
|
479
|
+
bg_color = "white"
|
|
480
|
+
hover_color = "#f5f5f5"
|
|
481
|
+
|
|
482
|
+
self.setStyleSheet(f"""
|
|
483
|
+
CompactMatchItem {{
|
|
484
|
+
background-color: {bg_color};
|
|
485
|
+
border: none;
|
|
486
|
+
}}
|
|
487
|
+
CompactMatchItem:hover {{
|
|
488
|
+
background-color: {hover_color};
|
|
489
|
+
}}
|
|
490
|
+
""")
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def _lighten_color(hex_color: str, factor: float) -> str:
|
|
494
|
+
"""Lighten a hex color"""
|
|
495
|
+
hex_color = hex_color.lstrip('#')
|
|
496
|
+
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
497
|
+
r = int(r + (255 - r) * (1 - factor))
|
|
498
|
+
g = int(g + (255 - g) * (1 - factor))
|
|
499
|
+
b = int(b + (255 - b) * (1 - factor))
|
|
500
|
+
return f'#{r:02x}{g:02x}{b:02x}'
|
|
501
|
+
|
|
502
|
+
@staticmethod
|
|
503
|
+
def _darken_color(hex_color: str, factor: float = 0.7) -> str:
|
|
504
|
+
"""Darken a hex color"""
|
|
505
|
+
hex_color = hex_color.lstrip('#')
|
|
506
|
+
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
507
|
+
r = int(r * factor)
|
|
508
|
+
g = int(g * factor)
|
|
509
|
+
b = int(b * factor)
|
|
510
|
+
return f'#{r:02x}{g:02x}{b:02x}'
|
|
511
|
+
|
|
512
|
+
def mouseMoveEvent(self, event):
|
|
513
|
+
"""Support drag/drop"""
|
|
514
|
+
if event.buttons() == Qt.MouseButton.LeftButton:
|
|
515
|
+
drag = QDrag(self)
|
|
516
|
+
mime_data = QMimeData()
|
|
517
|
+
mime_data.setText(self.match.target)
|
|
518
|
+
mime_data.setData("application/x-match", str(self.match.target).encode())
|
|
519
|
+
drag.setMimeData(mime_data)
|
|
520
|
+
drag.exec(Qt.DropAction.CopyAction)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class MatchSection(QWidget):
|
|
524
|
+
"""Stacked section for a match type (NT/MT/TM/Termbases)"""
|
|
525
|
+
|
|
526
|
+
match_selected = pyqtSignal(TranslationMatch)
|
|
527
|
+
|
|
528
|
+
def __init__(self, title: str, matches: List[TranslationMatch], parent=None, global_number_start: int = 1):
|
|
529
|
+
super().__init__(parent)
|
|
530
|
+
self.title = title
|
|
531
|
+
self.matches = matches
|
|
532
|
+
self.is_expanded = True
|
|
533
|
+
self.match_items = []
|
|
534
|
+
self.selected_index = -1
|
|
535
|
+
self.global_number_start = global_number_start # For global numbering across sections
|
|
536
|
+
|
|
537
|
+
layout = QVBoxLayout(self)
|
|
538
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
539
|
+
layout.setSpacing(2)
|
|
540
|
+
|
|
541
|
+
# Section header (collapsible)
|
|
542
|
+
header = self._create_header()
|
|
543
|
+
layout.addWidget(header)
|
|
544
|
+
|
|
545
|
+
# Matches container
|
|
546
|
+
self.matches_container = QWidget()
|
|
547
|
+
self.matches_layout = QVBoxLayout(self.matches_container)
|
|
548
|
+
self.matches_layout.setContentsMargins(0, 0, 0, 0) # No margins
|
|
549
|
+
self.matches_layout.setSpacing(0) # No spacing between matches
|
|
550
|
+
|
|
551
|
+
# Populate with matches
|
|
552
|
+
self._populate_matches()
|
|
553
|
+
|
|
554
|
+
self.scroll_area = QScrollArea()
|
|
555
|
+
self.scroll_area.setWidget(self.matches_container)
|
|
556
|
+
self.scroll_area.setWidgetResizable(True)
|
|
557
|
+
self.scroll_area.setStyleSheet("QScrollArea { border: none; background-color: white; }")
|
|
558
|
+
layout.addWidget(self.scroll_area)
|
|
559
|
+
|
|
560
|
+
def _create_header(self) -> QWidget:
|
|
561
|
+
"""Create collapsible header"""
|
|
562
|
+
header = QWidget()
|
|
563
|
+
header_layout = QHBoxLayout(header)
|
|
564
|
+
header_layout.setContentsMargins(4, 2, 4, 2)
|
|
565
|
+
|
|
566
|
+
# Toggle button
|
|
567
|
+
self.toggle_btn = QPushButton("▼" if self.is_expanded else "▶")
|
|
568
|
+
self.toggle_btn.setMaximumWidth(20)
|
|
569
|
+
self.toggle_btn.setMaximumHeight(20)
|
|
570
|
+
self.toggle_btn.setFlat(True)
|
|
571
|
+
self.toggle_btn.clicked.connect(self._toggle_section)
|
|
572
|
+
header_layout.addWidget(self.toggle_btn)
|
|
573
|
+
|
|
574
|
+
# Title + match count
|
|
575
|
+
title_text = f"{self.title}"
|
|
576
|
+
if self.matches:
|
|
577
|
+
title_text += f" ({len(self.matches)})"
|
|
578
|
+
|
|
579
|
+
title_label = QLabel(title_text)
|
|
580
|
+
title_label.setStyleSheet("font-weight: bold; font-size: 10px; color: #333;")
|
|
581
|
+
header_layout.addWidget(title_label)
|
|
582
|
+
|
|
583
|
+
header_layout.addStretch()
|
|
584
|
+
|
|
585
|
+
header.setStyleSheet("""
|
|
586
|
+
background-color: #f0f0f0;
|
|
587
|
+
border-bottom: 1px solid #ddd;
|
|
588
|
+
padding: 2px;
|
|
589
|
+
""")
|
|
590
|
+
|
|
591
|
+
return header
|
|
592
|
+
|
|
593
|
+
def _populate_matches(self):
|
|
594
|
+
"""Populate section with matches using global numbering"""
|
|
595
|
+
for local_idx, match in enumerate(self.matches):
|
|
596
|
+
global_number = self.global_number_start + local_idx
|
|
597
|
+
item = CompactMatchItem(match, match_number=global_number)
|
|
598
|
+
item.match_selected.connect(lambda m, i=local_idx: self._on_match_selected(m, i))
|
|
599
|
+
self.matches_layout.addWidget(item)
|
|
600
|
+
self.match_items.append(item)
|
|
601
|
+
|
|
602
|
+
self.matches_layout.addStretch()
|
|
603
|
+
|
|
604
|
+
def _toggle_section(self):
|
|
605
|
+
"""Toggle section expansion"""
|
|
606
|
+
self.is_expanded = not self.is_expanded
|
|
607
|
+
self.toggle_btn.setText("▼" if self.is_expanded else "▶")
|
|
608
|
+
self.scroll_area.setVisible(self.is_expanded)
|
|
609
|
+
|
|
610
|
+
def _on_match_selected(self, match: TranslationMatch, index: int):
|
|
611
|
+
"""Handle match selection"""
|
|
612
|
+
# Deselect previous
|
|
613
|
+
if 0 <= self.selected_index < len(self.match_items):
|
|
614
|
+
self.match_items[self.selected_index].deselect()
|
|
615
|
+
|
|
616
|
+
# Select new
|
|
617
|
+
self.selected_index = index
|
|
618
|
+
if 0 <= index < len(self.match_items):
|
|
619
|
+
self.match_items[index].select()
|
|
620
|
+
|
|
621
|
+
self.match_selected.emit(match)
|
|
622
|
+
|
|
623
|
+
def select_by_number(self, number: int):
|
|
624
|
+
"""Select match by number (1-based)"""
|
|
625
|
+
if 1 <= number <= len(self.match_items):
|
|
626
|
+
self._on_match_selected(self.matches[number-1], number-1)
|
|
627
|
+
# Scroll to visible
|
|
628
|
+
self.scroll_area.ensureWidgetVisible(self.match_items[number-1])
|
|
629
|
+
|
|
630
|
+
def navigate(self, direction: int):
|
|
631
|
+
"""Navigate matches: direction=1 for next, -1 for previous"""
|
|
632
|
+
new_index = self.selected_index + direction
|
|
633
|
+
if 0 <= new_index < len(self.match_items):
|
|
634
|
+
self._on_match_selected(self.matches[new_index], new_index)
|
|
635
|
+
self.scroll_area.ensureWidgetVisible(self.match_items[new_index])
|
|
636
|
+
return True
|
|
637
|
+
return False
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class TranslationResultsPanel(QWidget):
|
|
641
|
+
"""
|
|
642
|
+
Main translation results panel (right side of editor)
|
|
643
|
+
Compact memoQ-style design with stacked match sections
|
|
644
|
+
|
|
645
|
+
Features:
|
|
646
|
+
- Keyboard navigation: Up/Down arrows to cycle through matches
|
|
647
|
+
- Insert selected match: Press Enter
|
|
648
|
+
- Quick insert by number: Ctrl+1 through Ctrl+9 (1-based index)
|
|
649
|
+
- Vertical compare boxes with resizable splitter
|
|
650
|
+
- Match numbering display
|
|
651
|
+
- Zoom controls for both match list and compare boxes
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
match_selected = pyqtSignal(TranslationMatch)
|
|
655
|
+
match_inserted = pyqtSignal(str) # Emitted when user wants to insert match into target
|
|
656
|
+
|
|
657
|
+
# Class variables for font sizes
|
|
658
|
+
compare_box_font_size = 9
|
|
659
|
+
|
|
660
|
+
def __init__(self, parent=None, parent_app=None):
|
|
661
|
+
super().__init__(parent)
|
|
662
|
+
self.parent_app = parent_app # Reference to main app for settings access
|
|
663
|
+
self.theme_manager = parent_app.theme_manager if parent_app and hasattr(parent_app, 'theme_manager') else None
|
|
664
|
+
self.matches_by_type: Dict[str, List[TranslationMatch]] = {}
|
|
665
|
+
self.current_selection: Optional[TranslationMatch] = None
|
|
666
|
+
self.all_matches: List[TranslationMatch] = []
|
|
667
|
+
self.match_sections: Dict[str, MatchSection] = {}
|
|
668
|
+
self.match_items: List[CompactMatchItem] = []
|
|
669
|
+
self.selected_index = -1
|
|
670
|
+
self.compare_text_edits = [] # Track compare boxes for font size updates
|
|
671
|
+
self.setup_ui()
|
|
672
|
+
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # Ensure widget receives keyboard events
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def setup_ui(self):
|
|
676
|
+
"""Setup the UI"""
|
|
677
|
+
layout = QVBoxLayout(self)
|
|
678
|
+
layout.setContentsMargins(4, 4, 4, 4)
|
|
679
|
+
layout.setSpacing(2)
|
|
680
|
+
|
|
681
|
+
# Set class-level theme_manager for CompactMatchItem
|
|
682
|
+
CompactMatchItem.theme_manager = self.theme_manager
|
|
683
|
+
|
|
684
|
+
# Header with segment info
|
|
685
|
+
# Get theme colors
|
|
686
|
+
if self.theme_manager:
|
|
687
|
+
theme = self.theme_manager.current_theme
|
|
688
|
+
bg_color = theme.base
|
|
689
|
+
border_color = theme.border
|
|
690
|
+
separator_color = theme.separator
|
|
691
|
+
title_color = theme.text_disabled
|
|
692
|
+
frame_bg = theme.alternate_bg
|
|
693
|
+
else:
|
|
694
|
+
bg_color = "white"
|
|
695
|
+
border_color = "#ddd"
|
|
696
|
+
separator_color = "#e0e0e0"
|
|
697
|
+
title_color = "#666"
|
|
698
|
+
frame_bg = "#f5f5f5"
|
|
699
|
+
|
|
700
|
+
self.segment_label = QLabel("No segment selected")
|
|
701
|
+
self.segment_label.setStyleSheet(f"font-weight: bold; font-size: 10px; color: {title_color};")
|
|
702
|
+
layout.addWidget(self.segment_label)
|
|
703
|
+
|
|
704
|
+
# Use splitter for resizable sections (matches vs compare boxes)
|
|
705
|
+
self.main_splitter = QSplitter(Qt.Orientation.Vertical)
|
|
706
|
+
|
|
707
|
+
self.main_splitter.setStyleSheet(f"QSplitter::handle {{ background-color: {separator_color}; }}")
|
|
708
|
+
|
|
709
|
+
# Matches scroll area
|
|
710
|
+
self.matches_scroll = QScrollArea()
|
|
711
|
+
self.matches_scroll.setWidgetResizable(True)
|
|
712
|
+
self.matches_scroll.setStyleSheet(f"""
|
|
713
|
+
QScrollArea {{
|
|
714
|
+
border: 1px solid {border_color};
|
|
715
|
+
background-color: {bg_color};
|
|
716
|
+
border-radius: 3px;
|
|
717
|
+
}}
|
|
718
|
+
""")
|
|
719
|
+
|
|
720
|
+
self.matches_container = QWidget()
|
|
721
|
+
self.matches_container.setStyleSheet(f"background-color: {bg_color};")
|
|
722
|
+
self.main_layout = QVBoxLayout(self.matches_container)
|
|
723
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
724
|
+
self.main_layout.setSpacing(2)
|
|
725
|
+
|
|
726
|
+
self.matches_scroll.setWidget(self.matches_container)
|
|
727
|
+
self.main_splitter.addWidget(self.matches_scroll)
|
|
728
|
+
|
|
729
|
+
# Compare box (shown when TM match selected) - VERTICAL STACKED LAYOUT
|
|
730
|
+
self.compare_frame = self._create_compare_box()
|
|
731
|
+
self.main_splitter.addWidget(self.compare_frame)
|
|
732
|
+
self.compare_frame.hide() # Hidden by default
|
|
733
|
+
|
|
734
|
+
# Termbase data viewer (shown when termbase match selected)
|
|
735
|
+
self.termbase_frame = self._create_termbase_viewer()
|
|
736
|
+
self.main_splitter.addWidget(self.termbase_frame)
|
|
737
|
+
self.termbase_frame.hide() # Hidden by default
|
|
738
|
+
|
|
739
|
+
# Tabbed widget for TM Info and Notes (always visible, compact)
|
|
740
|
+
self.info_tabs = QTabWidget()
|
|
741
|
+
self.info_tabs.setStyleSheet(f"""
|
|
742
|
+
QTabWidget::pane {{
|
|
743
|
+
border: 1px solid {border_color};
|
|
744
|
+
background-color: {bg_color};
|
|
745
|
+
border-radius: 3px;
|
|
746
|
+
}}
|
|
747
|
+
QTabBar::tab {{
|
|
748
|
+
background-color: {frame_bg};
|
|
749
|
+
border: 1px solid {border_color};
|
|
750
|
+
border-bottom: none;
|
|
751
|
+
padding: 3px 8px;
|
|
752
|
+
font-size: 9px;
|
|
753
|
+
min-width: 60px;
|
|
754
|
+
}}
|
|
755
|
+
QTabBar::tab:selected {{
|
|
756
|
+
background-color: {bg_color};
|
|
757
|
+
font-weight: bold;
|
|
758
|
+
}}
|
|
759
|
+
""")
|
|
760
|
+
self.info_tabs.setMaximumHeight(100)
|
|
761
|
+
|
|
762
|
+
# TM Info tab
|
|
763
|
+
self.tm_info_frame = self._create_tm_info_panel()
|
|
764
|
+
self.info_tabs.addTab(self.tm_info_frame, "💾 TM Info")
|
|
765
|
+
|
|
766
|
+
# Notes tab
|
|
767
|
+
self.notes_widget = QWidget()
|
|
768
|
+
notes_layout = QVBoxLayout(self.notes_widget)
|
|
769
|
+
notes_layout.setContentsMargins(4, 4, 4, 4)
|
|
770
|
+
notes_layout.setSpacing(2)
|
|
771
|
+
|
|
772
|
+
self.notes_edit = QTextEdit()
|
|
773
|
+
self.notes_edit.setPlaceholderText("Add notes about this segment, context, or translation concerns...")
|
|
774
|
+
self.notes_edit.setStyleSheet(f"font-size: 9px; padding: 4px; background-color: {bg_color}; color: {theme.text if self.theme_manager else '#333'}; border: none;")
|
|
775
|
+
notes_layout.addWidget(self.notes_edit)
|
|
776
|
+
|
|
777
|
+
self.info_tabs.addTab(self.notes_widget, "📝 Segment note")
|
|
778
|
+
|
|
779
|
+
self.main_splitter.addWidget(self.info_tabs)
|
|
780
|
+
|
|
781
|
+
# Set splitter proportions for all 4 widgets:
|
|
782
|
+
# [matches_scroll, compare_frame, termbase_frame, info_tabs]
|
|
783
|
+
# Compare/Termbase are hidden by default, give them reasonable starting sizes
|
|
784
|
+
self.main_splitter.setSizes([300, 200, 150, 100])
|
|
785
|
+
self.main_splitter.setCollapsible(0, False) # matches_scroll
|
|
786
|
+
self.main_splitter.setCollapsible(1, False) # compare_frame - don't allow collapsing
|
|
787
|
+
self.main_splitter.setCollapsible(2, True) # termbase_frame
|
|
788
|
+
self.main_splitter.setCollapsible(3, False) # info_tabs
|
|
789
|
+
|
|
790
|
+
layout.addWidget(self.main_splitter)
|
|
791
|
+
|
|
792
|
+
def apply_theme(self):
|
|
793
|
+
"""Refresh all theme-dependent colors when theme changes"""
|
|
794
|
+
if not self.theme_manager:
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
theme = self.theme_manager.current_theme
|
|
798
|
+
|
|
799
|
+
bg_color = theme.base
|
|
800
|
+
border_color = theme.border
|
|
801
|
+
separator_color = theme.separator
|
|
802
|
+
frame_bg = theme.alternate_bg
|
|
803
|
+
title_color = theme.text_disabled
|
|
804
|
+
text_color = theme.text
|
|
805
|
+
|
|
806
|
+
# Update class-level theme_manager for CompactMatchItem
|
|
807
|
+
CompactMatchItem.theme_manager = self.theme_manager
|
|
808
|
+
|
|
809
|
+
# Update main scroll area
|
|
810
|
+
if hasattr(self, 'matches_scroll'):
|
|
811
|
+
self.matches_scroll.setStyleSheet(f"""
|
|
812
|
+
QScrollArea {{
|
|
813
|
+
border: 1px solid {border_color};
|
|
814
|
+
background-color: {bg_color};
|
|
815
|
+
border-radius: 3px;
|
|
816
|
+
}}
|
|
817
|
+
""")
|
|
818
|
+
|
|
819
|
+
# Update matches container background
|
|
820
|
+
if hasattr(self, 'matches_container'):
|
|
821
|
+
self.matches_container.setStyleSheet(f"background-color: {bg_color};")
|
|
822
|
+
|
|
823
|
+
# Update compare frame
|
|
824
|
+
if hasattr(self, 'compare_frame') and self.compare_frame:
|
|
825
|
+
self.compare_frame.setStyleSheet(f"""
|
|
826
|
+
QFrame {{
|
|
827
|
+
background-color: {frame_bg};
|
|
828
|
+
border: 1px solid {border_color};
|
|
829
|
+
border-radius: 3px;
|
|
830
|
+
padding: 4px;
|
|
831
|
+
}}
|
|
832
|
+
""")
|
|
833
|
+
|
|
834
|
+
# Update compare text boxes backgrounds using QPalette (more reliable than stylesheet)
|
|
835
|
+
box_colors = [theme.panel_info, theme.panel_warning, theme.panel_neutral]
|
|
836
|
+
for i, text_edit in enumerate(self.compare_text_edits):
|
|
837
|
+
if text_edit and i < len(box_colors):
|
|
838
|
+
bg_color = box_colors[i]
|
|
839
|
+
|
|
840
|
+
# Clear existing stylesheet first, then set new one
|
|
841
|
+
text_edit.setStyleSheet("")
|
|
842
|
+
new_style = f"""
|
|
843
|
+
QTextEdit {{
|
|
844
|
+
font-size: {self.compare_box_font_size}px;
|
|
845
|
+
padding: 3px;
|
|
846
|
+
background-color: {bg_color};
|
|
847
|
+
border: 1px solid {border_color};
|
|
848
|
+
border-radius: 2px;
|
|
849
|
+
color: {text_color};
|
|
850
|
+
}}
|
|
851
|
+
"""
|
|
852
|
+
text_edit.setStyleSheet(new_style)
|
|
853
|
+
|
|
854
|
+
# Also set palette for reliability
|
|
855
|
+
palette = text_edit.palette()
|
|
856
|
+
palette.setColor(palette.ColorRole.Base, QColor(bg_color))
|
|
857
|
+
palette.setColor(palette.ColorRole.Text, QColor(text_color))
|
|
858
|
+
text_edit.setPalette(palette)
|
|
859
|
+
text_edit.setAutoFillBackground(True)
|
|
860
|
+
|
|
861
|
+
# Force update
|
|
862
|
+
text_edit.style().unpolish(text_edit)
|
|
863
|
+
text_edit.style().polish(text_edit)
|
|
864
|
+
text_edit.update()
|
|
865
|
+
|
|
866
|
+
# Update segment label
|
|
867
|
+
if hasattr(self, 'segment_label'):
|
|
868
|
+
self.segment_label.setStyleSheet(f"font-weight: bold; font-size: 10px; color: {title_color};")
|
|
869
|
+
|
|
870
|
+
# Update notes section
|
|
871
|
+
if hasattr(self, 'notes_edit'):
|
|
872
|
+
self.notes_edit.setStyleSheet(f"font-size: 9px; padding: 4px; background-color: {bg_color}; color: {text_color}; border: none;")
|
|
873
|
+
|
|
874
|
+
# Update info_tabs (TM Info + Notes tabs)
|
|
875
|
+
if hasattr(self, 'info_tabs'):
|
|
876
|
+
self.info_tabs.setStyleSheet(f"""
|
|
877
|
+
QTabWidget::pane {{
|
|
878
|
+
border: 1px solid {border_color};
|
|
879
|
+
background-color: {bg_color};
|
|
880
|
+
border-radius: 3px;
|
|
881
|
+
}}
|
|
882
|
+
QTabBar::tab {{
|
|
883
|
+
background-color: {frame_bg};
|
|
884
|
+
border: 1px solid {border_color};
|
|
885
|
+
border-bottom: none;
|
|
886
|
+
padding: 3px 8px;
|
|
887
|
+
font-size: 9px;
|
|
888
|
+
min-width: 60px;
|
|
889
|
+
}}
|
|
890
|
+
QTabBar::tab:selected {{
|
|
891
|
+
background-color: {bg_color};
|
|
892
|
+
font-weight: bold;
|
|
893
|
+
}}
|
|
894
|
+
""")
|
|
895
|
+
|
|
896
|
+
# Update TM Info panel (now inside tab)
|
|
897
|
+
if hasattr(self, 'tm_info_frame') and self.tm_info_frame:
|
|
898
|
+
self.tm_info_frame.setStyleSheet(f"""
|
|
899
|
+
QFrame {{
|
|
900
|
+
background-color: {bg_color};
|
|
901
|
+
border: none;
|
|
902
|
+
padding: 2px;
|
|
903
|
+
}}
|
|
904
|
+
""")
|
|
905
|
+
|
|
906
|
+
if hasattr(self, 'tm_info_title'):
|
|
907
|
+
self.tm_info_title.setStyleSheet(f"font-weight: bold; font-size: 9px; color: {title_color}; margin-bottom: 2px;")
|
|
908
|
+
|
|
909
|
+
if hasattr(self, 'tm_name_label'):
|
|
910
|
+
self.tm_name_label.setStyleSheet(f"font-size: 9px; color: {text_color}; font-weight: bold;")
|
|
911
|
+
|
|
912
|
+
if hasattr(self, 'tm_languages_label'):
|
|
913
|
+
self.tm_languages_label.setStyleSheet(f"font-size: 8px; color: {title_color};")
|
|
914
|
+
|
|
915
|
+
if hasattr(self, 'tm_stats_label'):
|
|
916
|
+
self.tm_stats_label.setStyleSheet(f"font-size: 8px; color: {title_color};")
|
|
917
|
+
|
|
918
|
+
if hasattr(self, 'tm_description_label'):
|
|
919
|
+
self.tm_description_label.setStyleSheet(f"""
|
|
920
|
+
QLabel {{
|
|
921
|
+
font-size: 8px;
|
|
922
|
+
color: {title_color};
|
|
923
|
+
background-color: {bg_color};
|
|
924
|
+
padding: 3px;
|
|
925
|
+
border: 1px solid {border_color};
|
|
926
|
+
border-radius: 2px;
|
|
927
|
+
}}
|
|
928
|
+
""")
|
|
929
|
+
|
|
930
|
+
# Update Termbase viewer panel
|
|
931
|
+
source_bg = theme.panel_info
|
|
932
|
+
target_bg = theme.panel_neutral
|
|
933
|
+
metadata_bg = theme.panel_warning
|
|
934
|
+
|
|
935
|
+
if hasattr(self, 'termbase_frame') and self.termbase_frame:
|
|
936
|
+
self.termbase_frame.setStyleSheet(f"""
|
|
937
|
+
QFrame {{
|
|
938
|
+
background-color: {frame_bg};
|
|
939
|
+
border: 1px solid {border_color};
|
|
940
|
+
border-radius: 3px;
|
|
941
|
+
padding: 4px;
|
|
942
|
+
}}
|
|
943
|
+
""")
|
|
944
|
+
|
|
945
|
+
if hasattr(self, 'termbase_title'):
|
|
946
|
+
self.termbase_title.setStyleSheet(f"font-weight: bold; font-size: 9px; color: {title_color};")
|
|
947
|
+
|
|
948
|
+
if hasattr(self, 'termbase_source_label'):
|
|
949
|
+
self.termbase_source_label.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {title_color};")
|
|
950
|
+
|
|
951
|
+
if hasattr(self, 'termbase_source'):
|
|
952
|
+
self.termbase_source.setStyleSheet(f"""
|
|
953
|
+
QLabel {{
|
|
954
|
+
background-color: {source_bg};
|
|
955
|
+
border: 1px solid {border_color};
|
|
956
|
+
border-radius: 2px;
|
|
957
|
+
font-size: 10px;
|
|
958
|
+
padding: 6px;
|
|
959
|
+
margin: 0px;
|
|
960
|
+
color: {text_color};
|
|
961
|
+
}}
|
|
962
|
+
""")
|
|
963
|
+
|
|
964
|
+
if hasattr(self, 'termbase_target_label'):
|
|
965
|
+
self.termbase_target_label.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {title_color};")
|
|
966
|
+
|
|
967
|
+
if hasattr(self, 'termbase_target'):
|
|
968
|
+
self.termbase_target.setStyleSheet(f"""
|
|
969
|
+
QLabel {{
|
|
970
|
+
background-color: {target_bg};
|
|
971
|
+
border: 1px solid {border_color};
|
|
972
|
+
border-radius: 2px;
|
|
973
|
+
font-size: 10px;
|
|
974
|
+
padding: 6px;
|
|
975
|
+
margin: 0px;
|
|
976
|
+
color: {text_color};
|
|
977
|
+
}}
|
|
978
|
+
""")
|
|
979
|
+
|
|
980
|
+
if hasattr(self, 'termbase_metadata_label'):
|
|
981
|
+
self.termbase_metadata_label.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {title_color};")
|
|
982
|
+
|
|
983
|
+
if hasattr(self, 'termbase_metadata'):
|
|
984
|
+
self.termbase_metadata.setStyleSheet(f"""
|
|
985
|
+
QTextBrowser {{
|
|
986
|
+
background-color: {metadata_bg};
|
|
987
|
+
border: 1px solid {border_color};
|
|
988
|
+
border-radius: 2px;
|
|
989
|
+
font-size: {self.compare_box_font_size}px;
|
|
990
|
+
padding: 4px;
|
|
991
|
+
margin: 0px;
|
|
992
|
+
color: {text_color};
|
|
993
|
+
}}
|
|
994
|
+
""")
|
|
995
|
+
|
|
996
|
+
def _apply_compare_box_theme(self):
|
|
997
|
+
"""Apply theme colors to compare boxes - called when boxes become visible"""
|
|
998
|
+
if not self.theme_manager:
|
|
999
|
+
return
|
|
1000
|
+
|
|
1001
|
+
theme = self.theme_manager.current_theme
|
|
1002
|
+
border_color = theme.border
|
|
1003
|
+
text_color = theme.text
|
|
1004
|
+
box_colors = [theme.panel_info, theme.panel_warning, theme.panel_neutral]
|
|
1005
|
+
|
|
1006
|
+
for i, text_edit in enumerate(self.compare_text_edits[:3]): # Only the 3 compare boxes
|
|
1007
|
+
if text_edit:
|
|
1008
|
+
bg_color = box_colors[i]
|
|
1009
|
+
# Clear and set stylesheet
|
|
1010
|
+
text_edit.setStyleSheet("")
|
|
1011
|
+
text_edit.setStyleSheet(f"""
|
|
1012
|
+
QTextEdit {{
|
|
1013
|
+
font-size: {self.compare_box_font_size}px;
|
|
1014
|
+
padding: 3px;
|
|
1015
|
+
background-color: {bg_color};
|
|
1016
|
+
border: 1px solid {border_color};
|
|
1017
|
+
border-radius: 2px;
|
|
1018
|
+
color: {text_color};
|
|
1019
|
+
}}
|
|
1020
|
+
""")
|
|
1021
|
+
# Also set palette for reliability
|
|
1022
|
+
palette = text_edit.palette()
|
|
1023
|
+
palette.setColor(palette.ColorRole.Base, QColor(bg_color))
|
|
1024
|
+
palette.setColor(palette.ColorRole.Text, QColor(text_color))
|
|
1025
|
+
text_edit.setPalette(palette)
|
|
1026
|
+
text_edit.setAutoFillBackground(True)
|
|
1027
|
+
# Force visual update
|
|
1028
|
+
text_edit.style().unpolish(text_edit)
|
|
1029
|
+
text_edit.style().polish(text_edit)
|
|
1030
|
+
text_edit.update()
|
|
1031
|
+
|
|
1032
|
+
def _create_compare_box(self) -> QFrame:
|
|
1033
|
+
"""Create compare box frame with VERTICAL stacked layout - all boxes resize together"""
|
|
1034
|
+
# Get theme colors - try to get from parent_app if self.theme_manager is not set yet
|
|
1035
|
+
theme_manager = self.theme_manager
|
|
1036
|
+
if not theme_manager and hasattr(self, 'parent_app') and self.parent_app:
|
|
1037
|
+
theme_manager = getattr(self.parent_app, 'theme_manager', None)
|
|
1038
|
+
|
|
1039
|
+
if theme_manager:
|
|
1040
|
+
theme = theme_manager.current_theme
|
|
1041
|
+
frame_bg = theme.alternate_bg
|
|
1042
|
+
border_color = theme.border
|
|
1043
|
+
title_color = theme.text_disabled
|
|
1044
|
+
box1_bg = theme.panel_info
|
|
1045
|
+
box2_bg = theme.panel_warning
|
|
1046
|
+
box3_bg = theme.panel_neutral
|
|
1047
|
+
else:
|
|
1048
|
+
frame_bg = "#fafafa"
|
|
1049
|
+
border_color = "#ddd"
|
|
1050
|
+
title_color = "#666"
|
|
1051
|
+
box1_bg = "#e3f2fd"
|
|
1052
|
+
box2_bg = "#fff3cd"
|
|
1053
|
+
box3_bg = "#d4edda"
|
|
1054
|
+
|
|
1055
|
+
frame = QFrame()
|
|
1056
|
+
frame.setStyleSheet(f"""
|
|
1057
|
+
QFrame {{
|
|
1058
|
+
background-color: {frame_bg};
|
|
1059
|
+
border: 1px solid {border_color};
|
|
1060
|
+
border-radius: 3px;
|
|
1061
|
+
padding: 4px;
|
|
1062
|
+
}}
|
|
1063
|
+
""")
|
|
1064
|
+
|
|
1065
|
+
layout = QVBoxLayout(frame)
|
|
1066
|
+
layout.setContentsMargins(4, 4, 4, 4)
|
|
1067
|
+
layout.setSpacing(2)
|
|
1068
|
+
|
|
1069
|
+
# Title
|
|
1070
|
+
title = QLabel("📊 Compare Box")
|
|
1071
|
+
title.setStyleSheet(f"font-weight: bold; font-size: 9px; color: {title_color};")
|
|
1072
|
+
layout.addWidget(title)
|
|
1073
|
+
|
|
1074
|
+
# Box 1: Current Source
|
|
1075
|
+
box1 = self._create_compare_text_box("Current Source:", box1_bg)
|
|
1076
|
+
self.compare_current = box1[1]
|
|
1077
|
+
self.compare_current_label = box1[2]
|
|
1078
|
+
layout.addWidget(box1[0], 1) # stretch factor 1
|
|
1079
|
+
|
|
1080
|
+
# Box 2: TM Source (with diff highlighting capability)
|
|
1081
|
+
box2 = self._create_compare_text_box("TM Source:", box2_bg)
|
|
1082
|
+
self.compare_tm_source = box2[1]
|
|
1083
|
+
self.compare_source_label = box2[2]
|
|
1084
|
+
self.compare_source_container = box2[0]
|
|
1085
|
+
layout.addWidget(box2[0], 1) # stretch factor 1
|
|
1086
|
+
|
|
1087
|
+
# Box 3: TM Target
|
|
1088
|
+
box3 = self._create_compare_text_box("TM Target:", box3_bg)
|
|
1089
|
+
self.compare_tm_target = box3[1]
|
|
1090
|
+
self.compare_target_label = box3[2]
|
|
1091
|
+
layout.addWidget(box3[0], 1) # stretch factor 1
|
|
1092
|
+
|
|
1093
|
+
return frame
|
|
1094
|
+
|
|
1095
|
+
def _create_compare_text_box(self, label: str, bg_color: str) -> tuple:
|
|
1096
|
+
"""Create a single compare text box"""
|
|
1097
|
+
# Get theme colors
|
|
1098
|
+
if self.theme_manager:
|
|
1099
|
+
theme = self.theme_manager.current_theme
|
|
1100
|
+
label_color = theme.text_disabled
|
|
1101
|
+
border_color = theme.border
|
|
1102
|
+
text_color = theme.text
|
|
1103
|
+
else:
|
|
1104
|
+
label_color = "#666"
|
|
1105
|
+
border_color = "#ccc"
|
|
1106
|
+
text_color = "#333"
|
|
1107
|
+
|
|
1108
|
+
container = QWidget()
|
|
1109
|
+
layout = QVBoxLayout(container)
|
|
1110
|
+
layout.setContentsMargins(2, 2, 2, 2)
|
|
1111
|
+
layout.setSpacing(2)
|
|
1112
|
+
|
|
1113
|
+
label_widget = QLabel(label)
|
|
1114
|
+
label_widget.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {label_color};")
|
|
1115
|
+
layout.addWidget(label_widget)
|
|
1116
|
+
|
|
1117
|
+
text_edit = QTextEdit()
|
|
1118
|
+
text_edit.setReadOnly(True)
|
|
1119
|
+
text_edit.setStyleSheet(f"""
|
|
1120
|
+
QTextEdit {{
|
|
1121
|
+
background-color: {bg_color};
|
|
1122
|
+
border: 1px solid {border_color};
|
|
1123
|
+
border-radius: 2px;
|
|
1124
|
+
font-size: {self.compare_box_font_size}px;
|
|
1125
|
+
padding: 4px;
|
|
1126
|
+
margin: 0px;
|
|
1127
|
+
color: {text_color};
|
|
1128
|
+
}}
|
|
1129
|
+
""")
|
|
1130
|
+
layout.addWidget(text_edit)
|
|
1131
|
+
|
|
1132
|
+
# Track this text edit for font size updates
|
|
1133
|
+
self.compare_text_edits.append(text_edit)
|
|
1134
|
+
|
|
1135
|
+
return (container, text_edit, label_widget)
|
|
1136
|
+
|
|
1137
|
+
def _create_termbase_viewer(self) -> QFrame:
|
|
1138
|
+
"""Create termbase data viewer frame"""
|
|
1139
|
+
# Get theme colors
|
|
1140
|
+
if self.theme_manager:
|
|
1141
|
+
theme = self.theme_manager.current_theme
|
|
1142
|
+
frame_bg = theme.alternate_bg
|
|
1143
|
+
border_color = theme.border
|
|
1144
|
+
title_color = theme.text_disabled
|
|
1145
|
+
text_color = theme.text
|
|
1146
|
+
source_bg = theme.panel_info
|
|
1147
|
+
target_bg = theme.panel_neutral
|
|
1148
|
+
metadata_bg = theme.panel_warning
|
|
1149
|
+
else:
|
|
1150
|
+
frame_bg = "#fafafa"
|
|
1151
|
+
border_color = "#ddd"
|
|
1152
|
+
title_color = "#666"
|
|
1153
|
+
text_color = "#333"
|
|
1154
|
+
source_bg = "#e3f2fd"
|
|
1155
|
+
target_bg = "#d4edda"
|
|
1156
|
+
metadata_bg = "#fff3cd"
|
|
1157
|
+
|
|
1158
|
+
frame = QFrame()
|
|
1159
|
+
frame.setStyleSheet(f"""
|
|
1160
|
+
QFrame {{
|
|
1161
|
+
background-color: {frame_bg};
|
|
1162
|
+
border: 1px solid {border_color};
|
|
1163
|
+
border-radius: 3px;
|
|
1164
|
+
padding: 4px;
|
|
1165
|
+
}}
|
|
1166
|
+
""")
|
|
1167
|
+
|
|
1168
|
+
layout = QVBoxLayout(frame)
|
|
1169
|
+
layout.setContentsMargins(4, 4, 4, 4)
|
|
1170
|
+
layout.setSpacing(4)
|
|
1171
|
+
|
|
1172
|
+
# Title with termbase name (will be updated dynamically)
|
|
1173
|
+
header_layout = QHBoxLayout()
|
|
1174
|
+
self.termbase_title = QLabel("📖 Term Info")
|
|
1175
|
+
self.termbase_title.setStyleSheet(f"font-weight: bold; font-size: 9px; color: {title_color};")
|
|
1176
|
+
header_layout.addWidget(self.termbase_title)
|
|
1177
|
+
header_layout.addStretch()
|
|
1178
|
+
|
|
1179
|
+
# Refresh button
|
|
1180
|
+
self.termbase_refresh_btn = QPushButton("🔄 Refresh data")
|
|
1181
|
+
self.termbase_refresh_btn.setStyleSheet("""
|
|
1182
|
+
QPushButton {
|
|
1183
|
+
font-size: 8px;
|
|
1184
|
+
padding: 2px 6px;
|
|
1185
|
+
background-color: #2196F3;
|
|
1186
|
+
color: white;
|
|
1187
|
+
border: none;
|
|
1188
|
+
border-radius: 2px;
|
|
1189
|
+
}
|
|
1190
|
+
QPushButton:hover {
|
|
1191
|
+
background-color: #0b7dda;
|
|
1192
|
+
}
|
|
1193
|
+
""")
|
|
1194
|
+
self.termbase_refresh_btn.setFixedHeight(20)
|
|
1195
|
+
self.termbase_refresh_btn.setToolTip("Refresh entry from database")
|
|
1196
|
+
self.termbase_refresh_btn.clicked.connect(self._on_refresh_termbase_entry)
|
|
1197
|
+
header_layout.addWidget(self.termbase_refresh_btn)
|
|
1198
|
+
|
|
1199
|
+
# Edit button
|
|
1200
|
+
self.termbase_edit_btn = QPushButton("✏️ Edit")
|
|
1201
|
+
self.termbase_edit_btn.setStyleSheet("""
|
|
1202
|
+
QPushButton {
|
|
1203
|
+
font-size: 8px;
|
|
1204
|
+
padding: 2px 6px;
|
|
1205
|
+
background-color: #4CAF50;
|
|
1206
|
+
color: white;
|
|
1207
|
+
border: none;
|
|
1208
|
+
border-radius: 2px;
|
|
1209
|
+
}
|
|
1210
|
+
QPushButton:hover {
|
|
1211
|
+
background-color: #45a049;
|
|
1212
|
+
}
|
|
1213
|
+
""")
|
|
1214
|
+
self.termbase_edit_btn.setFixedHeight(20)
|
|
1215
|
+
self.termbase_edit_btn.clicked.connect(self._on_edit_termbase_entry)
|
|
1216
|
+
header_layout.addWidget(self.termbase_edit_btn)
|
|
1217
|
+
|
|
1218
|
+
layout.addLayout(header_layout)
|
|
1219
|
+
|
|
1220
|
+
# Source and Target terms
|
|
1221
|
+
terms_container = QWidget()
|
|
1222
|
+
terms_layout = QVBoxLayout(terms_container)
|
|
1223
|
+
terms_layout.setContentsMargins(2, 2, 2, 2)
|
|
1224
|
+
terms_layout.setSpacing(3)
|
|
1225
|
+
|
|
1226
|
+
# Source term
|
|
1227
|
+
self.termbase_source_label = QLabel("Source Term:")
|
|
1228
|
+
self.termbase_source_label.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {title_color};")
|
|
1229
|
+
terms_layout.addWidget(self.termbase_source_label)
|
|
1230
|
+
|
|
1231
|
+
self.termbase_source = QLabel()
|
|
1232
|
+
self.termbase_source.setStyleSheet(f"""
|
|
1233
|
+
QLabel {{
|
|
1234
|
+
background-color: {source_bg};
|
|
1235
|
+
border: 1px solid {border_color};
|
|
1236
|
+
border-radius: 2px;
|
|
1237
|
+
font-size: 10px;
|
|
1238
|
+
padding: 6px;
|
|
1239
|
+
margin: 0px;
|
|
1240
|
+
color: {text_color};
|
|
1241
|
+
}}
|
|
1242
|
+
""")
|
|
1243
|
+
self.termbase_source.setWordWrap(True)
|
|
1244
|
+
terms_layout.addWidget(self.termbase_source)
|
|
1245
|
+
|
|
1246
|
+
# Target term
|
|
1247
|
+
self.termbase_target_label = QLabel("Target Term:")
|
|
1248
|
+
self.termbase_target_label.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {title_color};")
|
|
1249
|
+
terms_layout.addWidget(self.termbase_target_label)
|
|
1250
|
+
|
|
1251
|
+
self.termbase_target = QLabel()
|
|
1252
|
+
self.termbase_target.setStyleSheet(f"""
|
|
1253
|
+
QLabel {{
|
|
1254
|
+
background-color: {target_bg};
|
|
1255
|
+
border: 1px solid {border_color};
|
|
1256
|
+
border-radius: 2px;
|
|
1257
|
+
font-size: 10px;
|
|
1258
|
+
padding: 6px;
|
|
1259
|
+
margin: 0px;
|
|
1260
|
+
color: {text_color};
|
|
1261
|
+
}}
|
|
1262
|
+
""")
|
|
1263
|
+
self.termbase_target.setWordWrap(True)
|
|
1264
|
+
terms_layout.addWidget(self.termbase_target)
|
|
1265
|
+
|
|
1266
|
+
layout.addWidget(terms_container)
|
|
1267
|
+
|
|
1268
|
+
# Metadata area
|
|
1269
|
+
self.termbase_metadata_label = QLabel("Metadata:")
|
|
1270
|
+
self.termbase_metadata_label.setStyleSheet(f"font-weight: bold; font-size: 8px; color: {title_color};")
|
|
1271
|
+
layout.addWidget(self.termbase_metadata_label)
|
|
1272
|
+
|
|
1273
|
+
from PyQt6.QtWidgets import QTextBrowser
|
|
1274
|
+
self.termbase_metadata = QTextBrowser()
|
|
1275
|
+
self.termbase_metadata.setReadOnly(True)
|
|
1276
|
+
self.termbase_metadata.setMaximumHeight(80)
|
|
1277
|
+
self.termbase_metadata.setStyleSheet(f"""
|
|
1278
|
+
QTextBrowser {{
|
|
1279
|
+
background-color: {metadata_bg};
|
|
1280
|
+
border: 1px solid {border_color};
|
|
1281
|
+
border-radius: 2px;
|
|
1282
|
+
font-size: {self.compare_box_font_size}px;
|
|
1283
|
+
padding: 4px;
|
|
1284
|
+
margin: 0px;
|
|
1285
|
+
color: {text_color};
|
|
1286
|
+
}}
|
|
1287
|
+
""")
|
|
1288
|
+
# Enable clickable links
|
|
1289
|
+
self.termbase_metadata.setOpenExternalLinks(True)
|
|
1290
|
+
layout.addWidget(self.termbase_metadata)
|
|
1291
|
+
|
|
1292
|
+
# Track metadata text edit for font size updates
|
|
1293
|
+
self.compare_text_edits.append(self.termbase_metadata)
|
|
1294
|
+
|
|
1295
|
+
return frame
|
|
1296
|
+
|
|
1297
|
+
def _create_tm_info_panel(self) -> QFrame:
|
|
1298
|
+
"""Create TM metadata info panel (memoQ-style) - shown in TM Info tab"""
|
|
1299
|
+
# Get theme colors
|
|
1300
|
+
if self.theme_manager:
|
|
1301
|
+
theme = self.theme_manager.current_theme
|
|
1302
|
+
frame_bg = theme.base
|
|
1303
|
+
border_color = theme.border
|
|
1304
|
+
title_color = theme.text_disabled
|
|
1305
|
+
text_color = theme.text
|
|
1306
|
+
desc_bg = theme.alternate_bg
|
|
1307
|
+
else:
|
|
1308
|
+
frame_bg = "#fff"
|
|
1309
|
+
border_color = "#ddd"
|
|
1310
|
+
title_color = "#666"
|
|
1311
|
+
text_color = "#333"
|
|
1312
|
+
desc_bg = "#f5f5f5"
|
|
1313
|
+
|
|
1314
|
+
frame = QFrame()
|
|
1315
|
+
frame.setStyleSheet(f"""
|
|
1316
|
+
QFrame {{
|
|
1317
|
+
background-color: {frame_bg};
|
|
1318
|
+
border: none;
|
|
1319
|
+
padding: 2px;
|
|
1320
|
+
}}
|
|
1321
|
+
""")
|
|
1322
|
+
|
|
1323
|
+
layout = QVBoxLayout(frame)
|
|
1324
|
+
layout.setContentsMargins(4, 4, 4, 4)
|
|
1325
|
+
layout.setSpacing(2)
|
|
1326
|
+
|
|
1327
|
+
# Info grid (compact layout - no title needed, it's in the tab)
|
|
1328
|
+
info_container = QWidget()
|
|
1329
|
+
info_layout = QVBoxLayout(info_container)
|
|
1330
|
+
info_layout.setContentsMargins(0, 0, 0, 0)
|
|
1331
|
+
info_layout.setSpacing(2)
|
|
1332
|
+
|
|
1333
|
+
# TM Name
|
|
1334
|
+
self.tm_name_label = QLabel()
|
|
1335
|
+
self.tm_name_label.setStyleSheet(f"font-size: 9px; color: {text_color}; font-weight: bold;")
|
|
1336
|
+
self.tm_name_label.setWordWrap(True)
|
|
1337
|
+
info_layout.addWidget(self.tm_name_label)
|
|
1338
|
+
|
|
1339
|
+
# Languages (smaller)
|
|
1340
|
+
self.tm_languages_label = QLabel()
|
|
1341
|
+
self.tm_languages_label.setStyleSheet(f"font-size: 8px; color: {title_color};")
|
|
1342
|
+
info_layout.addWidget(self.tm_languages_label)
|
|
1343
|
+
|
|
1344
|
+
# Entry count and modified date in single line
|
|
1345
|
+
self.tm_stats_label = QLabel()
|
|
1346
|
+
self.tm_stats_label.setStyleSheet(f"font-size: 8px; color: {title_color};")
|
|
1347
|
+
self.tm_stats_label.setWordWrap(True)
|
|
1348
|
+
info_layout.addWidget(self.tm_stats_label)
|
|
1349
|
+
|
|
1350
|
+
# Description (if available)
|
|
1351
|
+
self.tm_description_label = QLabel()
|
|
1352
|
+
self.tm_description_label.setStyleSheet(f"""
|
|
1353
|
+
QLabel {{
|
|
1354
|
+
font-size: 8px;
|
|
1355
|
+
color: {title_color};
|
|
1356
|
+
background-color: {desc_bg};
|
|
1357
|
+
padding: 3px;
|
|
1358
|
+
border: 1px solid {border_color};
|
|
1359
|
+
border-radius: 2px;
|
|
1360
|
+
}}
|
|
1361
|
+
""")
|
|
1362
|
+
self.tm_description_label.setWordWrap(True)
|
|
1363
|
+
self.tm_description_label.hide() # Hidden if no description
|
|
1364
|
+
info_layout.addWidget(self.tm_description_label)
|
|
1365
|
+
|
|
1366
|
+
layout.addWidget(info_container)
|
|
1367
|
+
|
|
1368
|
+
return frame
|
|
1369
|
+
|
|
1370
|
+
def _on_edit_termbase_entry(self):
|
|
1371
|
+
"""Handle edit button click - open termbase entry editor dialog"""
|
|
1372
|
+
if not self.current_selection or self.current_selection.match_type != "Termbase":
|
|
1373
|
+
return
|
|
1374
|
+
|
|
1375
|
+
# Get term_id from metadata if available
|
|
1376
|
+
term_id = self.current_selection.metadata.get('term_id')
|
|
1377
|
+
termbase_id = self.current_selection.metadata.get('termbase_id')
|
|
1378
|
+
|
|
1379
|
+
if term_id and termbase_id:
|
|
1380
|
+
# Import and show editor dialog
|
|
1381
|
+
from modules.termbase_entry_editor import TermbaseEntryEditor
|
|
1382
|
+
|
|
1383
|
+
# Get parent window (main application)
|
|
1384
|
+
parent_window = self.window()
|
|
1385
|
+
|
|
1386
|
+
dialog = TermbaseEntryEditor(
|
|
1387
|
+
parent=parent_window,
|
|
1388
|
+
db_manager=getattr(parent_window, 'db_manager', None),
|
|
1389
|
+
termbase_id=termbase_id,
|
|
1390
|
+
term_id=term_id
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
if dialog.exec():
|
|
1394
|
+
# Entry was edited, refresh the display
|
|
1395
|
+
# Get updated term data and refresh the termbase viewer
|
|
1396
|
+
self._refresh_termbase_viewer()
|
|
1397
|
+
|
|
1398
|
+
def _on_refresh_termbase_entry(self):
|
|
1399
|
+
"""Handle refresh button click - reload entry from database"""
|
|
1400
|
+
if not self.current_selection or self.current_selection.match_type != "Termbase":
|
|
1401
|
+
return
|
|
1402
|
+
|
|
1403
|
+
# Get term_id from metadata
|
|
1404
|
+
term_id = self.current_selection.metadata.get('term_id')
|
|
1405
|
+
if not term_id:
|
|
1406
|
+
return
|
|
1407
|
+
|
|
1408
|
+
# Get parent window and database manager
|
|
1409
|
+
parent_window = self.window()
|
|
1410
|
+
db_manager = getattr(parent_window, 'db_manager', None)
|
|
1411
|
+
|
|
1412
|
+
if not db_manager:
|
|
1413
|
+
return
|
|
1414
|
+
|
|
1415
|
+
try:
|
|
1416
|
+
# Fetch fresh data from database
|
|
1417
|
+
cursor = db_manager.cursor
|
|
1418
|
+
cursor.execute("""
|
|
1419
|
+
SELECT source_term, target_term, priority, domain, notes,
|
|
1420
|
+
project, client, forbidden, termbase_id
|
|
1421
|
+
FROM termbase_terms
|
|
1422
|
+
WHERE id = ?
|
|
1423
|
+
""", (term_id,))
|
|
1424
|
+
|
|
1425
|
+
row = cursor.fetchone()
|
|
1426
|
+
if row:
|
|
1427
|
+
# Update the current selection metadata with fresh data
|
|
1428
|
+
self.current_selection.source = row[0]
|
|
1429
|
+
self.current_selection.target = row[1]
|
|
1430
|
+
self.current_selection.metadata['priority'] = row[2] or 99
|
|
1431
|
+
self.current_selection.metadata['domain'] = row[3] or ''
|
|
1432
|
+
self.current_selection.metadata['notes'] = row[4] or ''
|
|
1433
|
+
self.current_selection.metadata['project'] = row[5] or ''
|
|
1434
|
+
self.current_selection.metadata['client'] = row[6] or ''
|
|
1435
|
+
self.current_selection.metadata['forbidden'] = row[7] or False
|
|
1436
|
+
self.current_selection.metadata['termbase_id'] = row[8]
|
|
1437
|
+
|
|
1438
|
+
# Re-display with updated data
|
|
1439
|
+
self._display_termbase_data(self.current_selection)
|
|
1440
|
+
|
|
1441
|
+
except Exception as e:
|
|
1442
|
+
print(f"Error refreshing termbase entry: {e}")
|
|
1443
|
+
|
|
1444
|
+
def _refresh_termbase_viewer(self):
|
|
1445
|
+
"""Refresh termbase viewer with latest data from database"""
|
|
1446
|
+
if not self.current_selection or self.current_selection.match_type != "Termbase":
|
|
1447
|
+
return
|
|
1448
|
+
|
|
1449
|
+
# Use the refresh handler to fetch and display fresh data
|
|
1450
|
+
self._on_refresh_termbase_entry()
|
|
1451
|
+
|
|
1452
|
+
def _display_termbase_data(self, match: TranslationMatch):
|
|
1453
|
+
"""Display termbase entry data in the viewer"""
|
|
1454
|
+
# Keep consistent "Term Info" title
|
|
1455
|
+
self.termbase_title.setText("📖 Term Info")
|
|
1456
|
+
|
|
1457
|
+
# Display source and target terms
|
|
1458
|
+
self.termbase_source.setText(match.source)
|
|
1459
|
+
|
|
1460
|
+
# Include synonyms in target display if available
|
|
1461
|
+
target_synonyms = match.metadata.get('target_synonyms', [])
|
|
1462
|
+
if target_synonyms:
|
|
1463
|
+
# Show main term with synonyms
|
|
1464
|
+
synonyms_text = ", ".join(target_synonyms)
|
|
1465
|
+
self.termbase_target.setText(f"{match.target} | {synonyms_text}")
|
|
1466
|
+
else:
|
|
1467
|
+
self.termbase_target.setText(match.target)
|
|
1468
|
+
|
|
1469
|
+
# Build metadata text
|
|
1470
|
+
metadata_parts = []
|
|
1471
|
+
|
|
1472
|
+
# Termbase name
|
|
1473
|
+
termbase_name = match.metadata.get('termbase_name', 'Unknown')
|
|
1474
|
+
metadata_parts.append(f"<b>Termbase:</b> {termbase_name}")
|
|
1475
|
+
|
|
1476
|
+
# Priority
|
|
1477
|
+
priority = match.metadata.get('priority', 50)
|
|
1478
|
+
metadata_parts.append(f"<b>Priority:</b> {priority}")
|
|
1479
|
+
|
|
1480
|
+
# Domain
|
|
1481
|
+
domain = match.metadata.get('domain', '')
|
|
1482
|
+
if domain:
|
|
1483
|
+
metadata_parts.append(f"<b>Domain:</b> {domain}")
|
|
1484
|
+
|
|
1485
|
+
# Notes
|
|
1486
|
+
notes = match.metadata.get('notes', '')
|
|
1487
|
+
if notes:
|
|
1488
|
+
# Truncate long notes for display
|
|
1489
|
+
if len(notes) > 200:
|
|
1490
|
+
notes = notes[:200] + "..."
|
|
1491
|
+
# Convert URLs to clickable links
|
|
1492
|
+
import re
|
|
1493
|
+
url_pattern = r'(https?://[^\s]+)'
|
|
1494
|
+
notes = re.sub(url_pattern, r'<a href="\1">\1</a>', notes)
|
|
1495
|
+
metadata_parts.append(f"<b>Notes:</b> {notes}")
|
|
1496
|
+
|
|
1497
|
+
# Project
|
|
1498
|
+
project = match.metadata.get('project', '')
|
|
1499
|
+
if project:
|
|
1500
|
+
metadata_parts.append(f"<b>Project:</b> {project}")
|
|
1501
|
+
|
|
1502
|
+
# Client
|
|
1503
|
+
client = match.metadata.get('client', '')
|
|
1504
|
+
if client:
|
|
1505
|
+
metadata_parts.append(f"<b>Client:</b> {client}")
|
|
1506
|
+
|
|
1507
|
+
# Forbidden status
|
|
1508
|
+
forbidden = match.metadata.get('forbidden', False)
|
|
1509
|
+
if forbidden:
|
|
1510
|
+
metadata_parts.append("<b><span style='color: red;'>⚠️ FORBIDDEN TERM</span></b>")
|
|
1511
|
+
|
|
1512
|
+
# Term ID (for debugging)
|
|
1513
|
+
term_id = match.metadata.get('term_id', '')
|
|
1514
|
+
if term_id:
|
|
1515
|
+
metadata_parts.append(f"<span style='color: #888; font-size: 7px;'>Term ID: {term_id}</span>")
|
|
1516
|
+
|
|
1517
|
+
metadata_html = "<br>".join(metadata_parts) if metadata_parts else "<i>No metadata</i>"
|
|
1518
|
+
self.termbase_metadata.setHtml(metadata_html)
|
|
1519
|
+
|
|
1520
|
+
def _display_tm_metadata(self, match: TranslationMatch):
|
|
1521
|
+
"""Display TM metadata in the info panel (memoQ-style)"""
|
|
1522
|
+
# Get TM metadata from match
|
|
1523
|
+
tm_name = match.metadata.get('tm_name', 'Unknown TM')
|
|
1524
|
+
tm_id = match.metadata.get('tm_id', '')
|
|
1525
|
+
source_lang = match.metadata.get('source_lang', '')
|
|
1526
|
+
target_lang = match.metadata.get('target_lang', '')
|
|
1527
|
+
entry_count = match.metadata.get('entry_count', 0)
|
|
1528
|
+
modified_date = match.metadata.get('modified_date', '')
|
|
1529
|
+
description = match.metadata.get('description', '')
|
|
1530
|
+
|
|
1531
|
+
# Update TM name
|
|
1532
|
+
self.tm_name_label.setText(f"📝 {tm_name}")
|
|
1533
|
+
|
|
1534
|
+
# Update languages
|
|
1535
|
+
if source_lang and target_lang:
|
|
1536
|
+
self.tm_languages_label.setText(f"🌐 {source_lang} → {target_lang}")
|
|
1537
|
+
else:
|
|
1538
|
+
self.tm_languages_label.setText("")
|
|
1539
|
+
|
|
1540
|
+
# Update stats (entry count + modified date)
|
|
1541
|
+
stats_parts = []
|
|
1542
|
+
if entry_count:
|
|
1543
|
+
stats_parts.append(f"📊 {entry_count:,} entries")
|
|
1544
|
+
if modified_date:
|
|
1545
|
+
# Format date nicely if it's ISO format
|
|
1546
|
+
try:
|
|
1547
|
+
from datetime import datetime
|
|
1548
|
+
dt = datetime.fromisoformat(modified_date)
|
|
1549
|
+
formatted_date = dt.strftime("%Y-%m-%d %H:%M")
|
|
1550
|
+
stats_parts.append(f"🕒 Modified: {formatted_date}")
|
|
1551
|
+
except:
|
|
1552
|
+
stats_parts.append(f"🕒 {modified_date}")
|
|
1553
|
+
|
|
1554
|
+
self.tm_stats_label.setText(" • ".join(stats_parts) if stats_parts else "")
|
|
1555
|
+
|
|
1556
|
+
# Update description (show/hide based on content)
|
|
1557
|
+
if description and description.strip():
|
|
1558
|
+
self.tm_description_label.setText(f"💬 {description}")
|
|
1559
|
+
self.tm_description_label.show()
|
|
1560
|
+
else:
|
|
1561
|
+
self.tm_description_label.hide()
|
|
1562
|
+
|
|
1563
|
+
def add_matches(self, new_matches_dict: Dict[str, List[TranslationMatch]]):
|
|
1564
|
+
"""
|
|
1565
|
+
Add new matches to existing matches (for progressive loading)
|
|
1566
|
+
Merges new matches with existing ones and re-renders the display
|
|
1567
|
+
Includes deduplication to prevent showing identical matches
|
|
1568
|
+
|
|
1569
|
+
Args:
|
|
1570
|
+
new_matches_dict: Dict with keys like "NT", "MT", "TM", "Termbases"
|
|
1571
|
+
"""
|
|
1572
|
+
# Merge new matches with existing matches_by_type
|
|
1573
|
+
if not hasattr(self, 'matches_by_type') or not self.matches_by_type:
|
|
1574
|
+
# No existing matches, just set them
|
|
1575
|
+
self.set_matches(new_matches_dict)
|
|
1576
|
+
return
|
|
1577
|
+
|
|
1578
|
+
# Merge: Update existing match types with new matches (with deduplication)
|
|
1579
|
+
for match_type, new_matches in new_matches_dict.items():
|
|
1580
|
+
if new_matches: # Only merge non-empty lists
|
|
1581
|
+
if match_type in self.matches_by_type:
|
|
1582
|
+
# Deduplicate: Only add matches that don't already exist
|
|
1583
|
+
existing_targets = {match.target for match in self.matches_by_type[match_type]}
|
|
1584
|
+
unique_new_matches = [m for m in new_matches if m.target not in existing_targets]
|
|
1585
|
+
if unique_new_matches:
|
|
1586
|
+
self.matches_by_type[match_type].extend(unique_new_matches)
|
|
1587
|
+
else:
|
|
1588
|
+
# New match type, add it
|
|
1589
|
+
self.matches_by_type[match_type] = new_matches
|
|
1590
|
+
|
|
1591
|
+
# Re-render with merged matches
|
|
1592
|
+
self.set_matches(self.matches_by_type)
|
|
1593
|
+
|
|
1594
|
+
def _sort_termbase_matches(self, matches: List[TranslationMatch]) -> List[TranslationMatch]:
|
|
1595
|
+
"""
|
|
1596
|
+
Sort termbase matches based on user preference.
|
|
1597
|
+
|
|
1598
|
+
Args:
|
|
1599
|
+
matches: List of termbase matches
|
|
1600
|
+
|
|
1601
|
+
Returns:
|
|
1602
|
+
Sorted list of matches
|
|
1603
|
+
"""
|
|
1604
|
+
if not self.parent_app:
|
|
1605
|
+
return matches # No sorting if no parent app
|
|
1606
|
+
|
|
1607
|
+
sort_order = getattr(self.parent_app, 'termbase_display_order', 'appearance')
|
|
1608
|
+
|
|
1609
|
+
if sort_order == 'alphabetical':
|
|
1610
|
+
# Sort alphabetically by source term (case-insensitive)
|
|
1611
|
+
return sorted(matches, key=lambda m: m.source.lower())
|
|
1612
|
+
elif sort_order == 'length':
|
|
1613
|
+
# Sort by source term length (longest first)
|
|
1614
|
+
return sorted(matches, key=lambda m: len(m.source), reverse=True)
|
|
1615
|
+
elif sort_order == 'appearance':
|
|
1616
|
+
# Sort by position in source text (if available in metadata)
|
|
1617
|
+
# If position not available, keep original order
|
|
1618
|
+
def get_position(match):
|
|
1619
|
+
pos = match.metadata.get('position_in_source', -1)
|
|
1620
|
+
# If no position, put at end
|
|
1621
|
+
return pos if pos >= 0 else 999999
|
|
1622
|
+
return sorted(matches, key=get_position)
|
|
1623
|
+
else:
|
|
1624
|
+
# Default: keep original order
|
|
1625
|
+
return matches
|
|
1626
|
+
|
|
1627
|
+
def _filter_shorter_matches(self, matches: List[TranslationMatch]) -> List[TranslationMatch]:
|
|
1628
|
+
"""
|
|
1629
|
+
Filter out shorter termbase matches that are substrings of longer matches.
|
|
1630
|
+
|
|
1631
|
+
Args:
|
|
1632
|
+
matches: List of termbase matches
|
|
1633
|
+
|
|
1634
|
+
Returns:
|
|
1635
|
+
Filtered list with shorter substring matches removed
|
|
1636
|
+
"""
|
|
1637
|
+
if not self.parent_app:
|
|
1638
|
+
return matches # No filtering if no parent app
|
|
1639
|
+
|
|
1640
|
+
hide_shorter = getattr(self.parent_app, 'termbase_hide_shorter_matches', False)
|
|
1641
|
+
|
|
1642
|
+
if not hide_shorter:
|
|
1643
|
+
return matches
|
|
1644
|
+
|
|
1645
|
+
# Create a list to track which matches to keep
|
|
1646
|
+
filtered_matches = []
|
|
1647
|
+
|
|
1648
|
+
for i, match in enumerate(matches):
|
|
1649
|
+
# Check if this match's source is a substring of any other match's source
|
|
1650
|
+
is_substring = False
|
|
1651
|
+
source_lower = match.source.lower()
|
|
1652
|
+
|
|
1653
|
+
for j, other_match in enumerate(matches):
|
|
1654
|
+
if i == j:
|
|
1655
|
+
continue
|
|
1656
|
+
other_source_lower = other_match.source.lower()
|
|
1657
|
+
|
|
1658
|
+
# Check if current match is a substring of the other match
|
|
1659
|
+
# and is shorter than the other match
|
|
1660
|
+
if (source_lower in other_source_lower and
|
|
1661
|
+
len(source_lower) < len(other_source_lower)):
|
|
1662
|
+
is_substring = True
|
|
1663
|
+
break
|
|
1664
|
+
|
|
1665
|
+
# Keep the match if it's not a substring of a longer match
|
|
1666
|
+
if not is_substring:
|
|
1667
|
+
filtered_matches.append(match)
|
|
1668
|
+
|
|
1669
|
+
return filtered_matches
|
|
1670
|
+
|
|
1671
|
+
def set_matches(self, matches_dict: Dict[str, List[TranslationMatch]]):
|
|
1672
|
+
"""
|
|
1673
|
+
Set matches from different sources in unified flat list with GLOBAL consecutive numbering
|
|
1674
|
+
(memoQ-style: single grid, color coding only identifies match type)
|
|
1675
|
+
|
|
1676
|
+
Args:
|
|
1677
|
+
matches_dict: Dict with keys like "NT", "MT", "TM", "Termbases"
|
|
1678
|
+
"""
|
|
1679
|
+
print(f"🎯 TranslationResultsPanel.set_matches() called with matches_dict keys: {list(matches_dict.keys())}")
|
|
1680
|
+
for match_type, matches in matches_dict.items():
|
|
1681
|
+
print(f" {match_type}: {len(matches)} matches")
|
|
1682
|
+
if match_type == "Termbases" and matches:
|
|
1683
|
+
for i, match in enumerate(matches[:2]): # Show first 2 for debugging
|
|
1684
|
+
print(f" [{i}] {match.source} → {match.target}")
|
|
1685
|
+
|
|
1686
|
+
# Ensure CompactMatchItem has current theme_manager
|
|
1687
|
+
if self.theme_manager:
|
|
1688
|
+
CompactMatchItem.theme_manager = self.theme_manager
|
|
1689
|
+
|
|
1690
|
+
# Store current matches for delayed search access
|
|
1691
|
+
self._current_matches = matches_dict.copy()
|
|
1692
|
+
self.matches_by_type = matches_dict
|
|
1693
|
+
self.all_matches = []
|
|
1694
|
+
self.match_items = [] # Track all match items for navigation
|
|
1695
|
+
self.selected_index = -1
|
|
1696
|
+
|
|
1697
|
+
# Clear existing matches
|
|
1698
|
+
while self.main_layout.count() > 0:
|
|
1699
|
+
item = self.main_layout.takeAt(0)
|
|
1700
|
+
if item and item.widget():
|
|
1701
|
+
item.widget().deleteLater()
|
|
1702
|
+
|
|
1703
|
+
# Apply match limits per type (configurable, defaults provided)
|
|
1704
|
+
match_limits = getattr(self, 'match_limits', {
|
|
1705
|
+
"LLM": 3,
|
|
1706
|
+
"NT": 5,
|
|
1707
|
+
"MT": 3,
|
|
1708
|
+
"TM": 5,
|
|
1709
|
+
"Termbases": 10,
|
|
1710
|
+
"NonTrans": 20 # Non-translatables (show more since they're important)
|
|
1711
|
+
})
|
|
1712
|
+
|
|
1713
|
+
# Build flat list of all matches with global numbering
|
|
1714
|
+
global_number = 1
|
|
1715
|
+
order = ["LLM", "NonTrans", "NT", "MT", "TM", "Termbases"] # LLM first, NonTrans early (important for translator)
|
|
1716
|
+
|
|
1717
|
+
for match_type in order:
|
|
1718
|
+
if match_type in matches_dict and matches_dict[match_type]:
|
|
1719
|
+
# Get matches for this type
|
|
1720
|
+
type_matches = matches_dict[match_type]
|
|
1721
|
+
|
|
1722
|
+
# Apply sorting and filtering for termbase matches
|
|
1723
|
+
if match_type == "Termbases":
|
|
1724
|
+
# First filter out shorter substring matches (if enabled)
|
|
1725
|
+
type_matches = self._filter_shorter_matches(type_matches)
|
|
1726
|
+
# Then sort according to user preference
|
|
1727
|
+
type_matches = self._sort_termbase_matches(type_matches)
|
|
1728
|
+
|
|
1729
|
+
# Apply limit for this match type
|
|
1730
|
+
limit = match_limits.get(match_type, 5)
|
|
1731
|
+
limited_matches = type_matches[:limit]
|
|
1732
|
+
|
|
1733
|
+
for match in limited_matches:
|
|
1734
|
+
self.all_matches.append(match)
|
|
1735
|
+
|
|
1736
|
+
# Create match item with global number
|
|
1737
|
+
item = CompactMatchItem(match, match_number=global_number)
|
|
1738
|
+
item.match_selected.connect(lambda m, idx=len(self.match_items): self._on_match_item_selected(m, idx))
|
|
1739
|
+
self.main_layout.addWidget(item)
|
|
1740
|
+
self.match_items.append(item)
|
|
1741
|
+
|
|
1742
|
+
global_number += 1
|
|
1743
|
+
|
|
1744
|
+
self.main_layout.addStretch()
|
|
1745
|
+
|
|
1746
|
+
def _on_match_item_selected(self, match: TranslationMatch, index: int):
|
|
1747
|
+
"""Handle match item selection"""
|
|
1748
|
+
# Deselect previous
|
|
1749
|
+
if 0 <= self.selected_index < len(self.match_items):
|
|
1750
|
+
self.match_items[self.selected_index].deselect()
|
|
1751
|
+
|
|
1752
|
+
# Select new
|
|
1753
|
+
self.selected_index = index
|
|
1754
|
+
if 0 <= index < len(self.match_items):
|
|
1755
|
+
self.match_items[index].select()
|
|
1756
|
+
|
|
1757
|
+
self._on_match_selected(match)
|
|
1758
|
+
|
|
1759
|
+
def _on_match_selected(self, match: TranslationMatch):
|
|
1760
|
+
"""Handle match selection"""
|
|
1761
|
+
print(f"🔍 DEBUG: _on_match_selected called with match_type='{match.match_type}'")
|
|
1762
|
+
self.current_selection = match
|
|
1763
|
+
self.match_selected.emit(match)
|
|
1764
|
+
|
|
1765
|
+
# Show appropriate viewer based on match type
|
|
1766
|
+
if match.match_type == "TM" and match.compare_source:
|
|
1767
|
+
# Show TM compare box
|
|
1768
|
+
print("📊 DEBUG: Showing TM compare box")
|
|
1769
|
+
self.compare_frame.show()
|
|
1770
|
+
self.termbase_frame.hide()
|
|
1771
|
+
|
|
1772
|
+
# Switch to TM Info tab
|
|
1773
|
+
if hasattr(self, 'info_tabs'):
|
|
1774
|
+
self.info_tabs.setCurrentIndex(0) # TM Info tab
|
|
1775
|
+
|
|
1776
|
+
# Ensure compare box has reasonable size in splitter
|
|
1777
|
+
if hasattr(self, 'main_splitter'):
|
|
1778
|
+
sizes = self.main_splitter.sizes()
|
|
1779
|
+
# If compare_frame (index 1) has 0 or very small size, redistribute
|
|
1780
|
+
if len(sizes) >= 4 and sizes[1] < 100:
|
|
1781
|
+
# Give compare box 200px, take from matches
|
|
1782
|
+
total = sum(sizes)
|
|
1783
|
+
sizes[1] = 200 # compare_frame
|
|
1784
|
+
sizes[0] = max(100, total - 200 - sizes[2] - sizes[3]) # matches_scroll
|
|
1785
|
+
self.main_splitter.setSizes(sizes)
|
|
1786
|
+
|
|
1787
|
+
# Apply theme colors to compare boxes (needed because they might not apply when hidden)
|
|
1788
|
+
self._apply_compare_box_theme()
|
|
1789
|
+
|
|
1790
|
+
# Update labels for TM
|
|
1791
|
+
self.compare_source_label.setText("TM Source:")
|
|
1792
|
+
self.compare_target_label.setText("TM Target:")
|
|
1793
|
+
self.compare_source_container.show() # Show TM source box
|
|
1794
|
+
|
|
1795
|
+
# Get current source text for diff comparison
|
|
1796
|
+
current_source = self.compare_current.toPlainText()
|
|
1797
|
+
tm_source = match.compare_source
|
|
1798
|
+
|
|
1799
|
+
# Apply diff highlighting between current source and TM source
|
|
1800
|
+
self._apply_diff_highlighting(current_source, tm_source)
|
|
1801
|
+
|
|
1802
|
+
# Set TM target (no diff highlighting needed)
|
|
1803
|
+
self.compare_tm_target.setText(match.target)
|
|
1804
|
+
|
|
1805
|
+
# Populate TM metadata panel
|
|
1806
|
+
self._display_tm_metadata(match)
|
|
1807
|
+
|
|
1808
|
+
elif match.match_type in ("MT", "LLM") and match.compare_source:
|
|
1809
|
+
# Show MT/LLM compare box (simplified - just current source and translation)
|
|
1810
|
+
print(f"🤖 DEBUG: Showing {match.match_type} compare box")
|
|
1811
|
+
self.compare_frame.show()
|
|
1812
|
+
self.termbase_frame.hide()
|
|
1813
|
+
|
|
1814
|
+
# Ensure compare box has reasonable size in splitter
|
|
1815
|
+
if hasattr(self, 'main_splitter'):
|
|
1816
|
+
sizes = self.main_splitter.sizes()
|
|
1817
|
+
# If compare_frame (index 1) has 0 or very small size, redistribute
|
|
1818
|
+
if len(sizes) >= 4 and sizes[1] < 100:
|
|
1819
|
+
total = sum(sizes)
|
|
1820
|
+
sizes[1] = 200 # compare_frame
|
|
1821
|
+
sizes[0] = max(100, total - 200 - sizes[2] - sizes[3]) # matches_scroll
|
|
1822
|
+
self.main_splitter.setSizes(sizes)
|
|
1823
|
+
|
|
1824
|
+
# Apply theme colors to compare boxes (needed because they might not apply when hidden)
|
|
1825
|
+
self._apply_compare_box_theme()
|
|
1826
|
+
|
|
1827
|
+
# Update labels for MT/LLM
|
|
1828
|
+
provider_name = match.metadata.get('provider', match.match_type)
|
|
1829
|
+
self.compare_source_container.hide() # Hide source box for MT/LLM (source = current)
|
|
1830
|
+
self.compare_target_label.setText(f"{match.match_type} Translation ({provider_name}):")
|
|
1831
|
+
|
|
1832
|
+
# Set target text
|
|
1833
|
+
self.compare_tm_target.setText(match.target)
|
|
1834
|
+
|
|
1835
|
+
elif match.match_type == "Termbase":
|
|
1836
|
+
# Show termbase data viewer
|
|
1837
|
+
print("📖 DEBUG: Showing termbase viewer!")
|
|
1838
|
+
self.compare_frame.hide()
|
|
1839
|
+
self.termbase_frame.show()
|
|
1840
|
+
self._display_termbase_data(match)
|
|
1841
|
+
else:
|
|
1842
|
+
# Hide all viewers
|
|
1843
|
+
print(f"❌ DEBUG: Match type '{match.match_type}' - hiding all viewers")
|
|
1844
|
+
self.compare_frame.hide()
|
|
1845
|
+
self.termbase_frame.hide()
|
|
1846
|
+
|
|
1847
|
+
def set_segment_info(self, segment_num: int, source_text: str):
|
|
1848
|
+
"""Update segment info display"""
|
|
1849
|
+
self.segment_label.setText(f"Segment {segment_num}: {source_text[:50]}...")
|
|
1850
|
+
self.compare_current.setText(source_text)
|
|
1851
|
+
|
|
1852
|
+
def _apply_diff_highlighting(self, current_source: str, tm_source: str):
|
|
1853
|
+
"""
|
|
1854
|
+
Apply diff highlighting between current source and TM source.
|
|
1855
|
+
Shows differences memoQ-style in the TM Source box:
|
|
1856
|
+
- Red strikethrough for text in TM that's not in current segment (will need to be removed)
|
|
1857
|
+
- Red underline for text in current that's not in TM (translator needs to add this)
|
|
1858
|
+
|
|
1859
|
+
The Current Source box shows the plain text without highlighting.
|
|
1860
|
+
This helps translators quickly see what changed between the fuzzy match and the current segment.
|
|
1861
|
+
"""
|
|
1862
|
+
# Use difflib's SequenceMatcher to find differences at word level
|
|
1863
|
+
current_words = current_source.split()
|
|
1864
|
+
tm_words = tm_source.split()
|
|
1865
|
+
|
|
1866
|
+
# Get opcodes that describe how to transform tm_source into current_source
|
|
1867
|
+
matcher = difflib.SequenceMatcher(None, tm_words, current_words)
|
|
1868
|
+
opcodes = matcher.get_opcodes()
|
|
1869
|
+
|
|
1870
|
+
# Define formatting styles
|
|
1871
|
+
# Red strikethrough for deletions (text in TM but not in current)
|
|
1872
|
+
delete_format = QTextCharFormat()
|
|
1873
|
+
delete_format.setForeground(QColor("#CC0000")) # Red text
|
|
1874
|
+
delete_format.setFontStrikeOut(True)
|
|
1875
|
+
|
|
1876
|
+
# Red underline for additions (text in current but not in TM)
|
|
1877
|
+
insert_format = QTextCharFormat()
|
|
1878
|
+
insert_format.setForeground(QColor("#CC0000")) # Red text
|
|
1879
|
+
insert_format.setFontUnderline(True)
|
|
1880
|
+
|
|
1881
|
+
# Normal format (for unchanged text)
|
|
1882
|
+
normal_format = QTextCharFormat()
|
|
1883
|
+
if self.theme_manager:
|
|
1884
|
+
normal_format.setForeground(QColor(self.theme_manager.current_theme.text))
|
|
1885
|
+
else:
|
|
1886
|
+
normal_format.setForeground(QColor("#333333"))
|
|
1887
|
+
|
|
1888
|
+
# Current Source box: just show plain text (already set by set_segment_info, but reset to ensure no formatting)
|
|
1889
|
+
self.compare_current.setText(current_source)
|
|
1890
|
+
|
|
1891
|
+
# TM Source box: show with diff highlighting
|
|
1892
|
+
# Red strikethrough = text in TM but not in current (needs to be removed/changed)
|
|
1893
|
+
# Red underline = text in current but not in TM (needs to be added to translation)
|
|
1894
|
+
self.compare_tm_source.clear()
|
|
1895
|
+
tm_cursor = self.compare_tm_source.textCursor()
|
|
1896
|
+
|
|
1897
|
+
first_word = True
|
|
1898
|
+
for tag, i1, i2, j1, j2 in opcodes:
|
|
1899
|
+
if tag == 'equal':
|
|
1900
|
+
# Unchanged words - show in normal format
|
|
1901
|
+
text = ' '.join(tm_words[i1:i2])
|
|
1902
|
+
if not first_word:
|
|
1903
|
+
tm_cursor.insertText(' ', normal_format)
|
|
1904
|
+
tm_cursor.insertText(text, normal_format)
|
|
1905
|
+
first_word = False
|
|
1906
|
+
elif tag == 'replace':
|
|
1907
|
+
# Words were replaced
|
|
1908
|
+
# Show what's in TM (being replaced) as strikethrough
|
|
1909
|
+
# Show what's in current (replacing it) as underlined
|
|
1910
|
+
old_text = ' '.join(tm_words[i1:i2])
|
|
1911
|
+
new_text = ' '.join(current_words[j1:j2])
|
|
1912
|
+
if not first_word:
|
|
1913
|
+
tm_cursor.insertText(' ', normal_format)
|
|
1914
|
+
tm_cursor.insertText(old_text, delete_format)
|
|
1915
|
+
tm_cursor.insertText(' ', normal_format)
|
|
1916
|
+
tm_cursor.insertText(new_text, insert_format)
|
|
1917
|
+
first_word = False
|
|
1918
|
+
elif tag == 'delete':
|
|
1919
|
+
# Words in TM but not in current - strikethrough (will be removed)
|
|
1920
|
+
text = ' '.join(tm_words[i1:i2])
|
|
1921
|
+
if not first_word:
|
|
1922
|
+
tm_cursor.insertText(' ', normal_format)
|
|
1923
|
+
tm_cursor.insertText(text, delete_format)
|
|
1924
|
+
first_word = False
|
|
1925
|
+
elif tag == 'insert':
|
|
1926
|
+
# Words in current but not in TM - underlined (needs to be added)
|
|
1927
|
+
text = ' '.join(current_words[j1:j2])
|
|
1928
|
+
if not first_word:
|
|
1929
|
+
tm_cursor.insertText(' ', normal_format)
|
|
1930
|
+
tm_cursor.insertText(text, insert_format)
|
|
1931
|
+
first_word = False
|
|
1932
|
+
|
|
1933
|
+
def clear(self):
|
|
1934
|
+
"""Clear all matches (but NOT notes - those are managed separately)"""
|
|
1935
|
+
self.matches_by_type = {}
|
|
1936
|
+
self.current_selection = None
|
|
1937
|
+
self.all_matches = []
|
|
1938
|
+
self.compare_frame.hide()
|
|
1939
|
+
self.termbase_frame.hide()
|
|
1940
|
+
# NOTE: Do NOT clear notes_edit here - notes are loaded/saved separately
|
|
1941
|
+
# and clear() is called multiple times during segment navigation
|
|
1942
|
+
|
|
1943
|
+
while self.main_layout.count() > 0:
|
|
1944
|
+
item = self.main_layout.takeAt(0)
|
|
1945
|
+
if item and item.widget():
|
|
1946
|
+
item.widget().deleteLater()
|
|
1947
|
+
|
|
1948
|
+
def get_selected_match(self) -> Optional[TranslationMatch]:
|
|
1949
|
+
"""Get currently selected match"""
|
|
1950
|
+
return self.current_selection
|
|
1951
|
+
|
|
1952
|
+
def set_font_size(self, size: int):
|
|
1953
|
+
"""Set font size for all match items (for zoom control)"""
|
|
1954
|
+
CompactMatchItem.set_font_size(size)
|
|
1955
|
+
# Update all currently displayed items
|
|
1956
|
+
for item in self.match_items:
|
|
1957
|
+
item.update_font_size()
|
|
1958
|
+
item.adjustSize()
|
|
1959
|
+
self.matches_scroll.update()
|
|
1960
|
+
|
|
1961
|
+
def set_compare_box_font_size(self, size: int):
|
|
1962
|
+
"""Set font size for compare boxes"""
|
|
1963
|
+
TranslationResultsPanel.compare_box_font_size = size
|
|
1964
|
+
for text_edit in self.compare_text_edits:
|
|
1965
|
+
text_edit.setStyleSheet(f"""
|
|
1966
|
+
QTextEdit {{
|
|
1967
|
+
background-color: {self._get_box_color(text_edit)};
|
|
1968
|
+
border: 1px solid #ccc;
|
|
1969
|
+
border-radius: 2px;
|
|
1970
|
+
font-size: {size}px;
|
|
1971
|
+
padding: 4px;
|
|
1972
|
+
margin: 0px;
|
|
1973
|
+
}}
|
|
1974
|
+
""")
|
|
1975
|
+
|
|
1976
|
+
def set_show_tags(self, show: bool):
|
|
1977
|
+
"""Set whether to show HTML/XML tags in matches"""
|
|
1978
|
+
CompactMatchItem.show_tags = show
|
|
1979
|
+
# Refresh all match items
|
|
1980
|
+
for item in self.match_items:
|
|
1981
|
+
if hasattr(item, 'source_label') and hasattr(item, 'target_label'):
|
|
1982
|
+
# Always use RichText (needed for both tag rendering and highlighting)
|
|
1983
|
+
item.source_label.setTextFormat(Qt.TextFormat.RichText)
|
|
1984
|
+
item.target_label.setTextFormat(Qt.TextFormat.RichText)
|
|
1985
|
+
# Refresh text
|
|
1986
|
+
item.source_label.setText(item._format_text(item.match.source))
|
|
1987
|
+
item.target_label.setText(item._format_text(item.match.target))
|
|
1988
|
+
|
|
1989
|
+
def set_tag_color(self, color: str):
|
|
1990
|
+
"""Set tag highlight color for all match items"""
|
|
1991
|
+
CompactMatchItem.tag_highlight_color = color
|
|
1992
|
+
# Refresh all match items to apply new color
|
|
1993
|
+
for item in self.match_items:
|
|
1994
|
+
if hasattr(item, 'update_tag_color'):
|
|
1995
|
+
item.update_tag_color(color)
|
|
1996
|
+
|
|
1997
|
+
def _get_box_color(self, text_edit) -> str:
|
|
1998
|
+
"""Get background color for a compare box (mapping hack)"""
|
|
1999
|
+
# This is a workaround - in production, store colors with the widgets
|
|
2000
|
+
colors = ["#e3f2fd", "#fff3cd", "#d4edda"]
|
|
2001
|
+
if text_edit in self.compare_text_edits:
|
|
2002
|
+
return colors[self.compare_text_edits.index(text_edit) % len(colors)]
|
|
2003
|
+
return "#fafafa"
|
|
2004
|
+
|
|
2005
|
+
def zoom_in(self):
|
|
2006
|
+
"""Increase font size for both match list and compare boxes"""
|
|
2007
|
+
new_size = CompactMatchItem.font_size_pt + 1
|
|
2008
|
+
if new_size <= 16: # Max 16pt
|
|
2009
|
+
self.set_font_size(new_size)
|
|
2010
|
+
# Also increase compare boxes
|
|
2011
|
+
compare_size = TranslationResultsPanel.compare_box_font_size + 1
|
|
2012
|
+
if compare_size <= 14:
|
|
2013
|
+
self.set_compare_box_font_size(compare_size)
|
|
2014
|
+
return new_size
|
|
2015
|
+
return CompactMatchItem.font_size_pt
|
|
2016
|
+
|
|
2017
|
+
def zoom_out(self):
|
|
2018
|
+
"""Decrease font size for both match list and compare boxes"""
|
|
2019
|
+
new_size = CompactMatchItem.font_size_pt - 1
|
|
2020
|
+
if new_size >= 7: # Min 7pt
|
|
2021
|
+
self.set_font_size(new_size)
|
|
2022
|
+
# Also decrease compare boxes
|
|
2023
|
+
compare_size = TranslationResultsPanel.compare_box_font_size - 1
|
|
2024
|
+
if compare_size >= 7:
|
|
2025
|
+
self.set_compare_box_font_size(compare_size)
|
|
2026
|
+
return new_size
|
|
2027
|
+
return CompactMatchItem.font_size_pt
|
|
2028
|
+
|
|
2029
|
+
def reset_zoom(self):
|
|
2030
|
+
"""Reset font size to defaults"""
|
|
2031
|
+
self.set_font_size(9)
|
|
2032
|
+
self.set_compare_box_font_size(9)
|
|
2033
|
+
return 9
|
|
2034
|
+
|
|
2035
|
+
def select_previous_match(self):
|
|
2036
|
+
"""Navigate to previous match (Ctrl+Up from main window)"""
|
|
2037
|
+
if self.selected_index > 0:
|
|
2038
|
+
new_index = self.selected_index - 1
|
|
2039
|
+
self._on_match_item_selected(self.all_matches[new_index], new_index)
|
|
2040
|
+
# Scroll to make it visible
|
|
2041
|
+
if 0 <= new_index < len(self.match_items):
|
|
2042
|
+
self.matches_scroll.ensureWidgetVisible(self.match_items[new_index])
|
|
2043
|
+
elif self.selected_index == -1 and self.all_matches:
|
|
2044
|
+
# No selection, select last match
|
|
2045
|
+
new_index = len(self.all_matches) - 1
|
|
2046
|
+
self._on_match_item_selected(self.all_matches[new_index], new_index)
|
|
2047
|
+
if 0 <= new_index < len(self.match_items):
|
|
2048
|
+
self.matches_scroll.ensureWidgetVisible(self.match_items[new_index])
|
|
2049
|
+
|
|
2050
|
+
def select_next_match(self):
|
|
2051
|
+
"""Navigate to next match (Ctrl+Down from main window)"""
|
|
2052
|
+
if self.selected_index < len(self.all_matches) - 1:
|
|
2053
|
+
new_index = self.selected_index + 1
|
|
2054
|
+
self._on_match_item_selected(self.all_matches[new_index], new_index)
|
|
2055
|
+
# Scroll to make it visible
|
|
2056
|
+
if 0 <= new_index < len(self.match_items):
|
|
2057
|
+
self.matches_scroll.ensureWidgetVisible(self.match_items[new_index])
|
|
2058
|
+
elif self.selected_index == -1 and self.all_matches:
|
|
2059
|
+
# No selection, select first match
|
|
2060
|
+
new_index = 0
|
|
2061
|
+
self._on_match_item_selected(self.all_matches[new_index], new_index)
|
|
2062
|
+
if 0 <= new_index < len(self.match_items):
|
|
2063
|
+
self.matches_scroll.ensureWidgetVisible(self.match_items[new_index])
|
|
2064
|
+
|
|
2065
|
+
def insert_match_by_number(self, match_number: int):
|
|
2066
|
+
"""Insert match by its number (1-based index) - for Ctrl+1-9 shortcuts"""
|
|
2067
|
+
if 0 < match_number <= len(self.all_matches):
|
|
2068
|
+
match = self.all_matches[match_number - 1]
|
|
2069
|
+
# Select it visually
|
|
2070
|
+
self._on_match_item_selected(match, match_number - 1)
|
|
2071
|
+
# Scroll to it
|
|
2072
|
+
if 0 <= match_number - 1 < len(self.match_items):
|
|
2073
|
+
self.matches_scroll.ensureWidgetVisible(self.match_items[match_number - 1])
|
|
2074
|
+
# Emit insert signal
|
|
2075
|
+
self.match_inserted.emit(match.target)
|
|
2076
|
+
return True
|
|
2077
|
+
return False
|
|
2078
|
+
|
|
2079
|
+
def insert_selected_match(self):
|
|
2080
|
+
"""Insert currently selected match (Ctrl+Space)"""
|
|
2081
|
+
if self.current_selection:
|
|
2082
|
+
self.match_inserted.emit(self.current_selection.target)
|
|
2083
|
+
return True
|
|
2084
|
+
return False
|
|
2085
|
+
|
|
2086
|
+
def keyPressEvent(self, event):
|
|
2087
|
+
"""
|
|
2088
|
+
Handle keyboard events for navigation and insertion
|
|
2089
|
+
|
|
2090
|
+
Shortcuts:
|
|
2091
|
+
- Up/Down arrows: Navigate matches (plain arrows, no Ctrl)
|
|
2092
|
+
- Spacebar: Insert selected match into target
|
|
2093
|
+
- Return/Enter: Insert selected match into target
|
|
2094
|
+
- Ctrl+Space: Insert selected match (alternative)
|
|
2095
|
+
- Ctrl+1 to Ctrl+9: Insert specific match by number (global)
|
|
2096
|
+
|
|
2097
|
+
Note: Ctrl+Up/Down are handled at main window level for grid navigation
|
|
2098
|
+
"""
|
|
2099
|
+
# Ctrl+Space: Insert currently selected match
|
|
2100
|
+
if (event.modifiers() & Qt.KeyboardModifier.ControlModifier and
|
|
2101
|
+
event.key() == Qt.Key.Key_Space):
|
|
2102
|
+
if self.insert_selected_match():
|
|
2103
|
+
event.accept()
|
|
2104
|
+
return
|
|
2105
|
+
|
|
2106
|
+
# Ctrl+1 through Ctrl+9: Insert match by number
|
|
2107
|
+
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
2108
|
+
if event.key() >= Qt.Key.Key_1 and event.key() <= Qt.Key.Key_9:
|
|
2109
|
+
match_num = event.key() - Qt.Key.Key_0 # Convert key to number
|
|
2110
|
+
if self.insert_match_by_number(match_num):
|
|
2111
|
+
event.accept()
|
|
2112
|
+
return
|
|
2113
|
+
|
|
2114
|
+
# Up/Down arrows: Navigate matches (plain arrows only, NOT Ctrl+Up/Down)
|
|
2115
|
+
if event.key() == Qt.Key.Key_Up:
|
|
2116
|
+
if not (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
2117
|
+
self.select_previous_match()
|
|
2118
|
+
event.accept()
|
|
2119
|
+
return
|
|
2120
|
+
elif event.key() == Qt.Key.Key_Down:
|
|
2121
|
+
if not (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
2122
|
+
self.select_next_match()
|
|
2123
|
+
event.accept()
|
|
2124
|
+
return
|
|
2125
|
+
|
|
2126
|
+
# Spacebar or Return/Enter: Insert selected match
|
|
2127
|
+
elif event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Space):
|
|
2128
|
+
if self.current_selection:
|
|
2129
|
+
self.match_inserted.emit(self.current_selection.target)
|
|
2130
|
+
event.accept()
|
|
2131
|
+
return
|
|
2132
|
+
|
|
2133
|
+
super().keyPressEvent(event)
|
|
2134
|
+
|