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.
Files changed (85) hide show
  1. Supervertaler.py +48473 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1911 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +351 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1176 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.163.dist-info/METADATA +906 -0
  81. supervertaler-1.9.163.dist-info/RECORD +85 -0
  82. supervertaler-1.9.163.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.163.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.163.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.163.dist-info/top_level.txt +2 -0
@@ -0,0 +1,842 @@
1
+ """
2
+ Glossary Entry Editor Dialog
3
+
4
+ Dialog for editing individual glossary entries with all metadata fields.
5
+ Can be opened from translation results panel (edit button or right-click menu).
6
+ """
7
+
8
+ from PyQt6.QtWidgets import (
9
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
10
+ QTextEdit, QSpinBox, QCheckBox, QPushButton, QGroupBox,
11
+ QMessageBox, QListWidget, QListWidgetItem, QMenu, QScrollArea,
12
+ QWidget, QToolButton, QApplication
13
+ )
14
+ from PyQt6.QtCore import Qt
15
+ from PyQt6.QtGui import QColor
16
+ from typing import Optional
17
+
18
+ class CheckmarkCheckBox(QCheckBox):
19
+ """Custom checkbox with green background and white checkmark when checked"""
20
+
21
+ def __init__(self, text="", parent=None):
22
+ super().__init__(text, parent)
23
+ self.setCheckable(True)
24
+ self.setEnabled(True)
25
+ self.setStyleSheet("""
26
+ QCheckBox {
27
+ font-size: 9pt;
28
+ spacing: 6px;
29
+ }
30
+ QCheckBox::indicator {
31
+ width: 16px;
32
+ height: 16px;
33
+ border: 2px solid #999;
34
+ border-radius: 3px;
35
+ background-color: white;
36
+ }
37
+ QCheckBox::indicator:checked {
38
+ background-color: #4CAF50;
39
+ border-color: #4CAF50;
40
+ }
41
+ QCheckBox::indicator:hover {
42
+ border-color: #666;
43
+ }
44
+ QCheckBox::indicator:checked:hover {
45
+ background-color: #45a049;
46
+ border-color: #45a049;
47
+ }
48
+ """)
49
+
50
+ def paintEvent(self, event):
51
+ """Override paint event to draw white checkmark when checked"""
52
+ super().paintEvent(event)
53
+
54
+ if self.isChecked():
55
+ from PyQt6.QtWidgets import QStyleOptionButton
56
+ from PyQt6.QtGui import QPainter, QPen, QColor
57
+ from PyQt6.QtCore import QPointF, Qt
58
+
59
+ opt = QStyleOptionButton()
60
+ self.initStyleOption(opt)
61
+ indicator_rect = self.style().subElementRect(
62
+ self.style().SubElement.SE_CheckBoxIndicator,
63
+ opt,
64
+ self
65
+ )
66
+
67
+ if indicator_rect.isValid():
68
+ painter = QPainter(self)
69
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
70
+ pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
71
+ painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
72
+ painter.setBrush(QColor(255, 255, 255))
73
+
74
+ x = indicator_rect.x()
75
+ y = indicator_rect.y()
76
+ w = indicator_rect.width()
77
+ h = indicator_rect.height()
78
+
79
+ padding = min(w, h) * 0.15
80
+ x += padding
81
+ y += padding
82
+ w -= padding * 2
83
+ h -= padding * 2
84
+
85
+ check_x1 = x + w * 0.10
86
+ check_y1 = y + h * 0.50
87
+ check_x2 = x + w * 0.35
88
+ check_y2 = y + h * 0.70
89
+ check_x3 = x + w * 0.90
90
+ check_y3 = y + h * 0.25
91
+
92
+ painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
93
+ painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
94
+
95
+ painter.end()
96
+
97
+
98
+
99
+ class TermbaseEntryEditor(QDialog):
100
+ """Dialog for editing a termbase entry"""
101
+
102
+ def __init__(self, parent=None, db_manager=None, termbase_id: Optional[int] = None, term_id: Optional[int] = None):
103
+ """
104
+ Initialize termbase entry editor
105
+
106
+ Args:
107
+ parent: Parent widget
108
+ db_manager: DatabaseManager instance
109
+ termbase_id: Termbase ID
110
+ term_id: Term ID to edit (if None, creates new term)
111
+ """
112
+ super().__init__(parent)
113
+ self.db_manager = db_manager
114
+ self.termbase_id = termbase_id
115
+ self.term_id = term_id
116
+ self.term_data = None
117
+
118
+ self.setWindowTitle("Edit Glossary Entry" if term_id else "New Glossary Entry")
119
+ self.setModal(True)
120
+ self.setMinimumWidth(550)
121
+
122
+ # Auto-resize to fit screen (max 85% of screen height)
123
+ screen = QApplication.primaryScreen().availableGeometry()
124
+ max_height = int(screen.height() * 0.85)
125
+ self.setMaximumHeight(max_height)
126
+
127
+ # Start with very compact size for laptops
128
+ self.resize(600, min(550, max_height))
129
+
130
+ self.setup_ui()
131
+
132
+ # Load existing term data if editing
133
+ if term_id and db_manager:
134
+ self.load_term_data()
135
+
136
+ def setup_ui(self):
137
+ """Setup the user interface"""
138
+ main_layout = QVBoxLayout(self)
139
+ main_layout.setContentsMargins(0, 0, 0, 0)
140
+
141
+ # Create scroll area for all content
142
+ scroll = QScrollArea()
143
+ scroll.setWidgetResizable(True)
144
+ scroll.setFrameShape(QScrollArea.Shape.NoFrame)
145
+
146
+ content_widget = QWidget()
147
+ layout = QVBoxLayout(content_widget)
148
+ layout.setSpacing(4)
149
+ layout.setContentsMargins(6, 6, 6, 6)
150
+
151
+ # Header
152
+ header = QLabel("📖 Glossary Entry Editor")
153
+ header.setStyleSheet("font-size: 12px; font-weight: bold; color: #333; padding: 4px;")
154
+ layout.addWidget(header)
155
+
156
+ # Terms group
157
+ terms_group = QGroupBox("Terms")
158
+ terms_layout = QVBoxLayout()
159
+ terms_layout.setSpacing(4)
160
+
161
+ # Source term
162
+ source_label = QLabel("Source Term:")
163
+ source_label.setStyleSheet("font-weight: bold;")
164
+ terms_layout.addWidget(source_label)
165
+
166
+ self.source_edit = QLineEdit()
167
+ self.source_edit.setPlaceholderText("Enter source language term...")
168
+ self.source_edit.setStyleSheet("padding: 6px; font-size: 11px;")
169
+ terms_layout.addWidget(self.source_edit)
170
+
171
+ # Target term
172
+ target_label = QLabel("Target Term:")
173
+ target_label.setStyleSheet("font-weight: bold;")
174
+ terms_layout.addWidget(target_label)
175
+
176
+ self.target_edit = QLineEdit()
177
+ self.target_edit.setPlaceholderText("Enter target language term...")
178
+ self.target_edit.setStyleSheet("padding: 6px; font-size: 11px;")
179
+ terms_layout.addWidget(self.target_edit)
180
+
181
+ terms_group.setLayout(terms_layout)
182
+ layout.addWidget(terms_group)
183
+
184
+ # Source Synonyms section (collapsible)
185
+ source_syn_group = QGroupBox()
186
+ source_syn_main_layout = QVBoxLayout()
187
+
188
+ # Header with collapse button
189
+ source_syn_header = QHBoxLayout()
190
+ self.source_syn_toggle = QToolButton()
191
+ self.source_syn_toggle.setText("▼")
192
+ self.source_syn_toggle.setStyleSheet("QToolButton { border: none; font-weight: bold; }")
193
+ self.source_syn_toggle.setFixedSize(20, 20)
194
+ self.source_syn_toggle.setCheckable(True)
195
+ self.source_syn_toggle.setChecked(False)
196
+ source_syn_header.addWidget(self.source_syn_toggle)
197
+
198
+ source_syn_label = QLabel("Source Synonyms (Optional)")
199
+ source_syn_label.setStyleSheet("font-weight: bold;")
200
+ source_syn_header.addWidget(source_syn_label)
201
+ source_syn_header.addStretch()
202
+ source_syn_main_layout.addLayout(source_syn_header)
203
+
204
+ # Collapsible content
205
+ self.source_syn_content = QWidget()
206
+ source_syn_layout = QVBoxLayout(self.source_syn_content)
207
+ source_syn_layout.setContentsMargins(0, 0, 0, 0)
208
+ self.source_syn_content.setVisible(False)
209
+
210
+ source_syn_info = QLabel("Alternative source terms. First item = preferred:")
211
+ source_syn_info.setStyleSheet("color: #666; font-size: 10px;")
212
+ source_syn_layout.addWidget(source_syn_info)
213
+
214
+ source_add_layout = QHBoxLayout()
215
+ self.source_synonym_edit = QLineEdit()
216
+ self.source_synonym_edit.setPlaceholderText("Enter source synonym...")
217
+ self.source_synonym_edit.setStyleSheet("padding: 4px; font-size: 10px;")
218
+ source_add_layout.addWidget(self.source_synonym_edit)
219
+
220
+ self.source_synonym_forbidden_check = CheckmarkCheckBox("Forbidden")
221
+ self.source_synonym_forbidden_check.setStyleSheet("font-size: 10px;")
222
+ source_add_layout.addWidget(self.source_synonym_forbidden_check)
223
+
224
+ source_add_btn = QPushButton("Add")
225
+ source_add_btn.setMaximumWidth(50)
226
+ source_add_btn.setStyleSheet("padding: 4px; font-size: 10px;")
227
+ source_add_btn.clicked.connect(self.add_source_synonym)
228
+ source_add_layout.addWidget(source_add_btn)
229
+ source_syn_layout.addLayout(source_add_layout)
230
+
231
+ self.source_synonym_edit.returnPressed.connect(self.add_source_synonym)
232
+
233
+ source_list_layout = QHBoxLayout()
234
+ self.source_synonym_list = QListWidget()
235
+ self.source_synonym_list.setMaximumHeight(80)
236
+ self.source_synonym_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
237
+ self.source_synonym_list.customContextMenuRequested.connect(self.show_source_synonym_context_menu)
238
+ source_list_layout.addWidget(self.source_synonym_list)
239
+
240
+ source_btn_col = QVBoxLayout()
241
+ source_up_btn = QPushButton("▲")
242
+ source_up_btn.setMaximumWidth(25)
243
+ source_up_btn.setToolTip("Move up")
244
+ source_up_btn.clicked.connect(lambda: self.move_synonym(self.source_synonym_list, -1))
245
+ source_btn_col.addWidget(source_up_btn)
246
+
247
+ source_down_btn = QPushButton("▼")
248
+ source_down_btn.setMaximumWidth(25)
249
+ source_down_btn.setToolTip("Move down")
250
+ source_down_btn.clicked.connect(lambda: self.move_synonym(self.source_synonym_list, 1))
251
+ source_btn_col.addWidget(source_down_btn)
252
+ source_btn_col.addStretch()
253
+
254
+ source_del_btn = QPushButton("✗")
255
+ source_del_btn.setMaximumWidth(25)
256
+ source_del_btn.setToolTip("Delete")
257
+ source_del_btn.clicked.connect(lambda: self.delete_synonym(self.source_synonym_list))
258
+ source_btn_col.addWidget(source_del_btn)
259
+
260
+ source_list_layout.addLayout(source_btn_col)
261
+ source_syn_layout.addLayout(source_list_layout)
262
+
263
+ # Add collapsible content to main layout
264
+ source_syn_main_layout.addWidget(self.source_syn_content)
265
+ source_syn_group.setLayout(source_syn_main_layout)
266
+
267
+ # Connect toggle button
268
+ self.source_syn_toggle.clicked.connect(lambda: self.toggle_section(self.source_syn_toggle, self.source_syn_content))
269
+
270
+ layout.addWidget(source_syn_group)
271
+
272
+ # Target Synonyms section (collapsible)
273
+ target_syn_group = QGroupBox()
274
+ target_syn_main_layout = QVBoxLayout()
275
+
276
+ # Header with collapse button
277
+ target_syn_header = QHBoxLayout()
278
+ self.target_syn_toggle = QToolButton()
279
+ self.target_syn_toggle.setText("▼")
280
+ self.target_syn_toggle.setStyleSheet("QToolButton { border: none; font-weight: bold; }")
281
+ self.target_syn_toggle.setFixedSize(20, 20)
282
+ self.target_syn_toggle.setCheckable(True)
283
+ self.target_syn_toggle.setChecked(False)
284
+ target_syn_header.addWidget(self.target_syn_toggle)
285
+
286
+ target_syn_label = QLabel("Target Synonyms (Optional)")
287
+ target_syn_label.setStyleSheet("font-weight: bold;")
288
+ target_syn_header.addWidget(target_syn_label)
289
+ target_syn_header.addStretch()
290
+ target_syn_main_layout.addLayout(target_syn_header)
291
+
292
+ # Collapsible content
293
+ self.target_syn_content = QWidget()
294
+ target_syn_layout = QVBoxLayout(self.target_syn_content)
295
+ target_syn_layout.setContentsMargins(0, 0, 0, 0)
296
+ self.target_syn_content.setVisible(False)
297
+
298
+ target_syn_info = QLabel("Alternative target terms. First item = preferred:")
299
+ target_syn_info.setStyleSheet("color: #666; font-size: 10px;")
300
+ target_syn_layout.addWidget(target_syn_info)
301
+
302
+ target_add_layout = QHBoxLayout()
303
+ self.target_synonym_edit = QLineEdit()
304
+ self.target_synonym_edit.setPlaceholderText("Enter target synonym...")
305
+ self.target_synonym_edit.setStyleSheet("padding: 4px; font-size: 10px;")
306
+ target_add_layout.addWidget(self.target_synonym_edit)
307
+
308
+ self.target_synonym_forbidden_check = CheckmarkCheckBox("Forbidden")
309
+ self.target_synonym_forbidden_check.setStyleSheet("font-size: 10px;")
310
+ target_add_layout.addWidget(self.target_synonym_forbidden_check)
311
+
312
+ target_add_btn = QPushButton("Add")
313
+ target_add_btn.setMaximumWidth(50)
314
+ target_add_btn.setStyleSheet("padding: 4px; font-size: 10px;")
315
+ target_add_btn.clicked.connect(self.add_target_synonym)
316
+ target_add_layout.addWidget(target_add_btn)
317
+ target_syn_layout.addLayout(target_add_layout)
318
+
319
+ self.target_synonym_edit.returnPressed.connect(self.add_target_synonym)
320
+
321
+ target_list_layout = QHBoxLayout()
322
+ self.target_synonym_list = QListWidget()
323
+ self.target_synonym_list.setMaximumHeight(80)
324
+ self.target_synonym_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
325
+ self.target_synonym_list.customContextMenuRequested.connect(self.show_target_synonym_context_menu)
326
+ target_list_layout.addWidget(self.target_synonym_list)
327
+
328
+ target_btn_col = QVBoxLayout()
329
+ target_up_btn = QPushButton("▲")
330
+ target_up_btn.setMaximumWidth(25)
331
+ target_up_btn.setToolTip("Move up")
332
+ target_up_btn.clicked.connect(lambda: self.move_synonym(self.target_synonym_list, -1))
333
+ target_btn_col.addWidget(target_up_btn)
334
+
335
+ target_down_btn = QPushButton("▼")
336
+ target_down_btn.setMaximumWidth(25)
337
+ target_down_btn.setToolTip("Move down")
338
+ target_down_btn.clicked.connect(lambda: self.move_synonym(self.target_synonym_list, 1))
339
+ target_btn_col.addWidget(target_down_btn)
340
+ target_btn_col.addStretch()
341
+
342
+ target_del_btn = QPushButton("✗")
343
+ target_del_btn.setMaximumWidth(25)
344
+ target_del_btn.setToolTip("Delete")
345
+ target_del_btn.clicked.connect(lambda: self.delete_synonym(self.target_synonym_list))
346
+ target_btn_col.addWidget(target_del_btn)
347
+
348
+ target_list_layout.addLayout(target_btn_col)
349
+ target_syn_layout.addLayout(target_list_layout)
350
+
351
+ # Add collapsible content to main layout
352
+ target_syn_main_layout.addWidget(self.target_syn_content)
353
+ target_syn_group.setLayout(target_syn_main_layout)
354
+
355
+ # Connect toggle button
356
+ self.target_syn_toggle.clicked.connect(lambda: self.toggle_section(self.target_syn_toggle, self.target_syn_content))
357
+
358
+ layout.addWidget(target_syn_group)
359
+
360
+ # Metadata group
361
+ metadata_group = QGroupBox("Metadata")
362
+ metadata_layout = QVBoxLayout()
363
+ metadata_layout.setSpacing(4)
364
+
365
+ # Priority
366
+ priority_layout = QHBoxLayout()
367
+ priority_label = QLabel("Priority (1=highest, 99=lowest):")
368
+ priority_label.setStyleSheet("font-weight: bold;")
369
+ priority_layout.addWidget(priority_label)
370
+
371
+ self.priority_spin = QSpinBox()
372
+ self.priority_spin.setMinimum(1)
373
+ self.priority_spin.setMaximum(99)
374
+ self.priority_spin.setValue(50)
375
+ self.priority_spin.setToolTip("Lower numbers = higher priority")
376
+ self.priority_spin.setStyleSheet("padding: 4px; font-size: 11px;")
377
+ priority_layout.addWidget(self.priority_spin)
378
+ priority_layout.addStretch()
379
+
380
+ metadata_layout.addLayout(priority_layout)
381
+
382
+ # Domain
383
+ domain_label = QLabel("Domain:")
384
+ domain_label.setStyleSheet("font-weight: bold;")
385
+ metadata_layout.addWidget(domain_label)
386
+
387
+ self.domain_edit = QLineEdit()
388
+ self.domain_edit.setPlaceholderText("e.g., Patents, Legal, Medical, IT...")
389
+ self.domain_edit.setStyleSheet("padding: 6px; font-size: 11px;")
390
+ metadata_layout.addWidget(self.domain_edit)
391
+
392
+ # Note
393
+ note_label = QLabel("Note:")
394
+ note_label.setStyleSheet("font-weight: bold;")
395
+ metadata_layout.addWidget(note_label)
396
+
397
+ self.note_edit = QTextEdit()
398
+ self.note_edit.setPlaceholderText("Usage notes, context, definition, URLs...")
399
+ self.note_edit.setMaximumHeight(45)
400
+ self.note_edit.setStyleSheet("padding: 3px; font-size: 10px;")
401
+ metadata_layout.addWidget(self.note_edit)
402
+
403
+ # Project
404
+ project_label = QLabel("Project:")
405
+ project_label.setStyleSheet("font-weight: bold;")
406
+ metadata_layout.addWidget(project_label)
407
+
408
+ self.project_edit = QLineEdit()
409
+ self.project_edit.setPlaceholderText("Optional project name...")
410
+ self.project_edit.setStyleSheet("padding: 6px; font-size: 11px;")
411
+ metadata_layout.addWidget(self.project_edit)
412
+
413
+ # Client
414
+ client_label = QLabel("Client:")
415
+ client_label.setStyleSheet("font-weight: bold;")
416
+ metadata_layout.addWidget(client_label)
417
+
418
+ self.client_edit = QLineEdit()
419
+ self.client_edit.setPlaceholderText("Optional client name...")
420
+ self.client_edit.setStyleSheet("padding: 6px; font-size: 11px;")
421
+ metadata_layout.addWidget(self.client_edit)
422
+
423
+ # Forbidden checkbox
424
+ self.forbidden_check = CheckmarkCheckBox("⚠️ Mark as FORBIDDEN term (do not use)")
425
+ self.forbidden_check.setStyleSheet("font-weight: bold; color: #d32f2f;")
426
+ metadata_layout.addWidget(self.forbidden_check)
427
+
428
+ metadata_group.setLayout(metadata_layout)
429
+ layout.addWidget(metadata_group)
430
+
431
+ # Buttons
432
+ buttons_layout = QHBoxLayout()
433
+
434
+ # Delete button (only show when editing existing term)
435
+ if self.term_id:
436
+ self.delete_btn = QPushButton("🗑️ Delete")
437
+ self.delete_btn.setStyleSheet("""
438
+ QPushButton {
439
+ padding: 8px 20px;
440
+ font-size: 11px;
441
+ font-weight: bold;
442
+ background-color: #f44336;
443
+ color: white;
444
+ border: none;
445
+ border-radius: 3px;
446
+ }
447
+ QPushButton:hover {
448
+ background-color: #d32f2f;
449
+ }
450
+ """)
451
+ self.delete_btn.clicked.connect(self.delete_term)
452
+ buttons_layout.addWidget(self.delete_btn)
453
+
454
+ buttons_layout.addStretch()
455
+
456
+ self.cancel_btn = QPushButton("Cancel")
457
+ self.cancel_btn.setStyleSheet("""
458
+ QPushButton {
459
+ padding: 8px 20px;
460
+ font-size: 11px;
461
+ background-color: #f5f5f5;
462
+ border: 1px solid #ccc;
463
+ border-radius: 3px;
464
+ }
465
+ QPushButton:hover {
466
+ background-color: #e0e0e0;
467
+ }
468
+ """)
469
+ self.cancel_btn.clicked.connect(self.reject)
470
+ buttons_layout.addWidget(self.cancel_btn)
471
+
472
+ self.save_btn = QPushButton("💾 Save")
473
+ self.save_btn.setStyleSheet("""
474
+ QPushButton {
475
+ padding: 8px 20px;
476
+ font-size: 11px;
477
+ font-weight: bold;
478
+ background-color: #4CAF50;
479
+ color: white;
480
+ border: none;
481
+ border-radius: 3px;
482
+ }
483
+ QPushButton:hover {
484
+ background-color: #45a049;
485
+ }
486
+ """)
487
+ self.save_btn.clicked.connect(self.save_term)
488
+ buttons_layout.addWidget(self.save_btn)
489
+
490
+ layout.addLayout(buttons_layout)
491
+
492
+ # Set the scroll area content
493
+ scroll.setWidget(content_widget)
494
+ main_layout.addWidget(scroll)
495
+
496
+ def toggle_section(self, toggle_btn, content_widget):
497
+ """Toggle visibility of a collapsible section"""
498
+ is_visible = content_widget.isVisible()
499
+ content_widget.setVisible(not is_visible)
500
+ toggle_btn.setText("▼" if is_visible else "▲")
501
+
502
+ def add_source_synonym(self):
503
+ """Add source synonym to list"""
504
+ text = self.source_synonym_edit.text().strip()
505
+ if text:
506
+ for i in range(self.source_synonym_list.count()):
507
+ if self.source_synonym_list.item(i).data(Qt.ItemDataRole.UserRole)['text'] == text:
508
+ QMessageBox.warning(self, "Duplicate", "Synonym already added")
509
+ return
510
+
511
+ forbidden = self.source_synonym_forbidden_check.isChecked()
512
+ display = f"{'🚫 ' if forbidden else ''}{text}"
513
+ item = QListWidgetItem(display)
514
+ item.setData(Qt.ItemDataRole.UserRole, {'text': text, 'forbidden': forbidden})
515
+ if forbidden:
516
+ item.setForeground(QColor('#d32f2f'))
517
+ self.source_synonym_list.addItem(item)
518
+ self.source_synonym_edit.clear()
519
+ self.source_synonym_forbidden_check.setChecked(False)
520
+
521
+ def add_target_synonym(self):
522
+ """Add target synonym to list"""
523
+ text = self.target_synonym_edit.text().strip()
524
+ if text:
525
+ for i in range(self.target_synonym_list.count()):
526
+ if self.target_synonym_list.item(i).data(Qt.ItemDataRole.UserRole)['text'] == text:
527
+ QMessageBox.warning(self, "Duplicate", "Synonym already added")
528
+ return
529
+
530
+ forbidden = self.target_synonym_forbidden_check.isChecked()
531
+ display = f"{'🚫 ' if forbidden else ''}{text}"
532
+ item = QListWidgetItem(display)
533
+ item.setData(Qt.ItemDataRole.UserRole, {'text': text, 'forbidden': forbidden})
534
+ if forbidden:
535
+ item.setForeground(QColor('#d32f2f'))
536
+ self.target_synonym_list.addItem(item)
537
+ self.target_synonym_edit.clear()
538
+ self.target_synonym_forbidden_check.setChecked(False)
539
+
540
+ def move_synonym(self, list_widget, direction):
541
+ """Move synonym up (-1) or down (1)"""
542
+ row = list_widget.currentRow()
543
+ if row < 0:
544
+ return
545
+ new_row = row + direction
546
+ if 0 <= new_row < list_widget.count():
547
+ item = list_widget.takeItem(row)
548
+ list_widget.insertItem(new_row, item)
549
+ list_widget.setCurrentRow(new_row)
550
+
551
+ def delete_synonym(self, list_widget):
552
+ """Delete selected synonym"""
553
+ row = list_widget.currentRow()
554
+ if row >= 0:
555
+ list_widget.takeItem(row)
556
+
557
+ def show_source_synonym_context_menu(self, position):
558
+ """Show context menu for source synonyms"""
559
+ self._show_synonym_context_menu(self.source_synonym_list, position)
560
+
561
+ def show_target_synonym_context_menu(self, position):
562
+ """Show context menu for target synonyms"""
563
+ self._show_synonym_context_menu(self.target_synonym_list, position)
564
+
565
+ def _show_synonym_context_menu(self, list_widget, position):
566
+ """Show context menu for synonym list"""
567
+ if list_widget.count() == 0:
568
+ return
569
+
570
+ item = list_widget.currentItem()
571
+ if not item:
572
+ return
573
+
574
+ menu = QMenu()
575
+ data = item.data(Qt.ItemDataRole.UserRole)
576
+ is_forbidden = data.get('forbidden', False)
577
+
578
+ toggle_action = menu.addAction("Mark as Allowed" if is_forbidden else "Mark as Forbidden")
579
+ menu.addSeparator()
580
+ delete_action = menu.addAction("Delete")
581
+
582
+ action = menu.exec(list_widget.mapToGlobal(position))
583
+
584
+ if action == toggle_action:
585
+ data['forbidden'] = not is_forbidden
586
+ text = data['text']
587
+ display = f"{'🚫 ' if data['forbidden'] else ''}{text}"
588
+ item.setText(display)
589
+ item.setData(Qt.ItemDataRole.UserRole, data)
590
+ item.setForeground(QColor('#d32f2f') if data['forbidden'] else QColor('#000000'))
591
+ elif action == delete_action:
592
+ list_widget.takeItem(list_widget.row(item))
593
+
594
+ def load_term_data(self):
595
+ """Load existing term data from database"""
596
+ if not self.db_manager or not self.term_id:
597
+ return
598
+
599
+ try:
600
+ cursor = self.db_manager.cursor
601
+ cursor.execute("""
602
+ SELECT source_term, target_term, priority, domain, definition, forbidden,
603
+ notes, project, client
604
+ FROM termbase_terms
605
+ WHERE id = ?
606
+ """, (self.term_id,))
607
+
608
+ row = cursor.fetchone()
609
+ if row:
610
+ self.term_data = {
611
+ 'source_term': row[0],
612
+ 'target_term': row[1],
613
+ 'priority': row[2] or 50,
614
+ 'domain': row[3] or '',
615
+ 'definition': row[4] or '', # Legacy field
616
+ 'forbidden': row[5] or False,
617
+ 'note': row[6] or '',
618
+ 'project': row[7] or '',
619
+ 'client': row[8] or ''
620
+ }
621
+
622
+ # Populate fields
623
+ self.source_edit.setText(self.term_data['source_term'])
624
+ self.target_edit.setText(self.term_data['target_term'])
625
+ self.priority_spin.setValue(self.term_data['priority'])
626
+ self.domain_edit.setText(self.term_data['domain'])
627
+ # Use note field if available, otherwise fall back to definition (legacy)
628
+ note_text = self.term_data['note'] or self.term_data['definition']
629
+ self.note_edit.setPlainText(note_text)
630
+ self.project_edit.setText(self.term_data['project'])
631
+ self.client_edit.setText(self.term_data['client'])
632
+ self.forbidden_check.setChecked(self.term_data['forbidden'])
633
+
634
+ # Load synonyms
635
+ self.load_synonyms()
636
+
637
+ except Exception as e:
638
+ QMessageBox.critical(self, "Error", f"Failed to load term data: {e}")
639
+
640
+ def load_synonyms(self):
641
+ """Load synonyms for current term"""
642
+ if not self.db_manager or not self.term_id:
643
+ return
644
+
645
+ try:
646
+ cursor = self.db_manager.cursor
647
+
648
+ # Check if forbidden column exists (backward compatibility)
649
+ cursor.execute("PRAGMA table_info(termbase_synonyms)")
650
+ columns = [row[1] for row in cursor.fetchall()]
651
+ has_forbidden = 'forbidden' in columns
652
+ has_display_order = 'display_order' in columns
653
+
654
+ # Load source synonyms
655
+ if has_forbidden and has_display_order:
656
+ cursor.execute("""
657
+ SELECT synonym_text, forbidden FROM termbase_synonyms
658
+ WHERE term_id = ? AND language = 'source'
659
+ ORDER BY display_order ASC
660
+ """, (self.term_id,))
661
+ else:
662
+ cursor.execute("""
663
+ SELECT synonym_text FROM termbase_synonyms
664
+ WHERE term_id = ? AND language = 'source'
665
+ ORDER BY created_date ASC
666
+ """, (self.term_id,))
667
+
668
+ for row in cursor.fetchall():
669
+ text = row[0]
670
+ forbidden = bool(row[1]) if has_forbidden and len(row) > 1 else False
671
+ display = f"{'🚫 ' if forbidden else ''}{text}"
672
+ item = QListWidgetItem(display)
673
+ item.setData(Qt.ItemDataRole.UserRole, {'text': text, 'forbidden': forbidden})
674
+ if forbidden:
675
+ item.setForeground(QColor('#d32f2f'))
676
+ self.source_synonym_list.addItem(item)
677
+
678
+ # Load target synonyms
679
+ if has_forbidden and has_display_order:
680
+ cursor.execute("""
681
+ SELECT synonym_text, forbidden FROM termbase_synonyms
682
+ WHERE term_id = ? AND language = 'target'
683
+ ORDER BY display_order ASC
684
+ """, (self.term_id,))
685
+ else:
686
+ cursor.execute("""
687
+ SELECT synonym_text FROM termbase_synonyms
688
+ WHERE term_id = ? AND language = 'target'
689
+ ORDER BY created_date ASC
690
+ """, (self.term_id,))
691
+
692
+ for row in cursor.fetchall():
693
+ text = row[0]
694
+ forbidden = bool(row[1]) if has_forbidden and len(row) > 1 else False
695
+ display = f"{'🚫 ' if forbidden else ''}{text}"
696
+ item = QListWidgetItem(display)
697
+ item.setData(Qt.ItemDataRole.UserRole, {'text': text, 'forbidden': forbidden})
698
+ if forbidden:
699
+ item.setForeground(QColor('#d32f2f'))
700
+ self.target_synonym_list.addItem(item)
701
+
702
+ except Exception as e:
703
+ # Silently fail for backward compatibility
704
+ print(f"Warning: Could not load synonyms: {e}")
705
+
706
+ def delete_term(self):
707
+ """Delete this term from database"""
708
+ if not self.db_manager or not self.term_id:
709
+ return
710
+
711
+ # Confirm deletion
712
+ reply = QMessageBox.question(
713
+ self,
714
+ "Confirm Deletion",
715
+ f"Delete this glossary entry?\n\nSource: {self.source_edit.text()}\nTarget: {self.target_edit.text()}\n\nThis action cannot be undone.",
716
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
717
+ QMessageBox.StandardButton.No
718
+ )
719
+
720
+ if reply == QMessageBox.StandardButton.Yes:
721
+ try:
722
+ cursor = self.db_manager.cursor
723
+ cursor.execute("DELETE FROM termbase_terms WHERE id = ?", (self.term_id,))
724
+ self.db_manager.connection.commit()
725
+ QMessageBox.information(self, "Success", "Glossary entry deleted")
726
+ self.accept() # Close dialog with success
727
+ except Exception as e:
728
+ QMessageBox.critical(self, "Error", f"Failed to delete entry: {e}")
729
+
730
+ def save_term(self):
731
+ """Save term to database"""
732
+ # Validate inputs
733
+ source_term = self.source_edit.text().strip()
734
+ target_term = self.target_edit.text().strip()
735
+
736
+ if not source_term or not target_term:
737
+ QMessageBox.warning(
738
+ self,
739
+ "Validation Error",
740
+ "Both source and target terms are required."
741
+ )
742
+ return
743
+
744
+ if not self.db_manager:
745
+ QMessageBox.critical(
746
+ self,
747
+ "Error",
748
+ "No database connection available."
749
+ )
750
+ return
751
+
752
+ try:
753
+ cursor = self.db_manager.cursor
754
+
755
+ # Gather data
756
+ priority = self.priority_spin.value()
757
+ domain = self.domain_edit.text().strip()
758
+ note = self.note_edit.toPlainText().strip()
759
+ project = self.project_edit.text().strip()
760
+ client = self.client_edit.text().strip()
761
+ forbidden = self.forbidden_check.isChecked()
762
+
763
+ if self.term_id:
764
+ # Update existing term
765
+ cursor.execute("""
766
+ UPDATE termbase_terms
767
+ SET source_term = ?, target_term = ?, priority = ?,
768
+ domain = ?, notes = ?, project = ?, client = ?, forbidden = ?
769
+ WHERE id = ?
770
+ """, (source_term, target_term, priority, domain, note, project, client, forbidden, self.term_id))
771
+ else:
772
+ # Insert new term
773
+ cursor.execute("""
774
+ INSERT INTO termbase_terms
775
+ (termbase_id, source_term, target_term, priority, domain, notes, project, client, forbidden)
776
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
777
+ """, (self.termbase_id, source_term, target_term, priority, domain, note, project, client, forbidden))
778
+
779
+ self.db_manager.connection.commit()
780
+
781
+ # Save synonyms (get the term_id if this was a new term)
782
+ if not self.term_id:
783
+ self.term_id = cursor.lastrowid
784
+
785
+ self.save_synonyms()
786
+
787
+ # Success
788
+ self.accept()
789
+
790
+ except Exception as e:
791
+ QMessageBox.critical(
792
+ self,
793
+ "Error",
794
+ f"Failed to save term: {e}"
795
+ )
796
+
797
+ def save_synonyms(self):
798
+ """Save synonyms to database"""
799
+ if not self.db_manager or not self.term_id:
800
+ return
801
+
802
+ try:
803
+ cursor = self.db_manager.cursor
804
+
805
+ # Delete existing synonyms for this term
806
+ cursor.execute("DELETE FROM termbase_synonyms WHERE term_id = ?", (self.term_id,))
807
+
808
+ # Save source synonyms
809
+ for i in range(self.source_synonym_list.count()):
810
+ item = self.source_synonym_list.item(i)
811
+ data = item.data(Qt.ItemDataRole.UserRole)
812
+ cursor.execute("""
813
+ INSERT INTO termbase_synonyms (term_id, synonym_text, language, display_order, forbidden)
814
+ VALUES (?, ?, 'source', ?, ?)
815
+ """, (self.term_id, data['text'], i, 1 if data['forbidden'] else 0))
816
+
817
+ # Save target synonyms
818
+ for i in range(self.target_synonym_list.count()):
819
+ item = self.target_synonym_list.item(i)
820
+ data = item.data(Qt.ItemDataRole.UserRole)
821
+ cursor.execute("""
822
+ INSERT INTO termbase_synonyms (term_id, synonym_text, language, display_order, forbidden)
823
+ VALUES (?, ?, 'target', ?, ?)
824
+ """, (self.term_id, data['text'], i, 1 if data['forbidden'] else 0))
825
+
826
+ self.db_manager.connection.commit()
827
+
828
+ except Exception as e:
829
+ QMessageBox.warning(self, "Warning", f"Failed to save synonyms: {e}")
830
+
831
+ def get_term_data(self) -> Optional[dict]:
832
+ """Get the current term data from the form fields"""
833
+ return {
834
+ 'source_term': self.source_edit.text().strip(),
835
+ 'target_term': self.target_edit.text().strip(),
836
+ 'priority': self.priority_spin.value(),
837
+ 'domain': self.domain_edit.text().strip(),
838
+ 'note': self.note_edit.toPlainText().strip(),
839
+ 'project': self.project_edit.text().strip(),
840
+ 'client': self.client_edit.text().strip(),
841
+ 'forbidden': self.forbidden_check.isChecked()
842
+ }