supervertaler 1.9.153__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of supervertaler might be problematic. Click here for more details.

Files changed (85) hide show
  1. Supervertaler.py +47886 -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 +1878 -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 +333 -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 +1172 -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.153.dist-info/METADATA +896 -0
  81. supervertaler-1.9.153.dist-info/RECORD +85 -0
  82. supervertaler-1.9.153.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.153.dist-info/top_level.txt +2 -0
@@ -0,0 +1,2784 @@
1
+ """
2
+ TMX Editor Module - PyQt6 Edition
3
+
4
+ Professional Translation Memory Editor for Qt version of Supervertaler.
5
+ Based on the tkinter version, converted to PyQt6.
6
+
7
+ Key Features:
8
+ - Dual-language grid editor (source/target columns)
9
+ - Fast filtering by language, content, status
10
+ - In-place editing with validation
11
+ - TMX file validation and repair
12
+ - Header metadata editing
13
+ - Large file support with pagination
14
+ - Import/Export multiple formats
15
+ - Multi-language support (view any language pair)
16
+
17
+ Reuses core logic from tmx_editor.py (TmxFile, TmxParser, data classes)
18
+ """
19
+
20
+ from PyQt6.QtWidgets import (
21
+ QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
22
+ QPushButton, QLabel, QLineEdit, QTextEdit, QComboBox, QFileDialog,
23
+ QMessageBox, QDialog, QDialogButtonBox, QFormLayout, QListWidget,
24
+ QFrame, QMenu, QGroupBox, QHeaderView, QCheckBox, QStyledItemDelegate,
25
+ QAbstractItemView, QStyleOptionViewItem, QStyle, QStyleOptionButton,
26
+ QRadioButton, QProgressDialog, QApplication
27
+ )
28
+ from PyQt6.QtCore import Qt, pyqtSignal, QRect, QPointF
29
+ from PyQt6.QtGui import QColor, QKeySequence, QShortcut, QContextMenuEvent, QPainter, QFontMetrics, QPen, QPolygonF
30
+ import os
31
+ import json
32
+ import re
33
+ from datetime import datetime
34
+ from typing import List, Dict, Optional
35
+
36
+ # Import core logic from tkinter version (framework-agnostic)
37
+ from modules.tmx_editor import (
38
+ TmxFile, TmxParser, TmxTranslationUnit, TmxSegment, TmxHeader
39
+ )
40
+
41
+
42
+ class CheckmarkCheckBox(QCheckBox):
43
+ """Custom checkbox with green background and white checkmark when checked - same as AutoFingers"""
44
+
45
+ def __init__(self, text="", parent=None):
46
+ super().__init__(text, parent)
47
+ self.setStyleSheet("""
48
+ QCheckBox {
49
+ font-size: 9pt;
50
+ spacing: 6px;
51
+ }
52
+ QCheckBox::indicator {
53
+ width: 18px;
54
+ height: 18px;
55
+ border: 2px solid #999;
56
+ border-radius: 3px;
57
+ background-color: white;
58
+ }
59
+ QCheckBox::indicator:checked {
60
+ background-color: #4CAF50;
61
+ border-color: #4CAF50;
62
+ }
63
+ QCheckBox::indicator:hover {
64
+ border-color: #666;
65
+ }
66
+ QCheckBox::indicator:checked:hover {
67
+ background-color: #45a049;
68
+ border-color: #45a049;
69
+ }
70
+ """)
71
+
72
+ def paintEvent(self, event):
73
+ """Override paint event to draw white checkmark when checked"""
74
+ super().paintEvent(event)
75
+
76
+ if self.isChecked():
77
+ # Get the indicator rectangle using QStyle
78
+ opt = QStyleOptionButton()
79
+ self.initStyleOption(opt)
80
+ indicator_rect = self.style().subElementRect(
81
+ self.style().SubElement.SE_CheckBoxIndicator,
82
+ opt,
83
+ self
84
+ )
85
+
86
+ if indicator_rect.isValid():
87
+ # Draw white checkmark
88
+ painter = QPainter(self)
89
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
90
+ pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
91
+ painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
92
+ painter.setBrush(QColor(255, 255, 255))
93
+
94
+ # Draw checkmark (✓ shape) - coordinates relative to indicator
95
+ x = indicator_rect.x()
96
+ y = indicator_rect.y()
97
+ w = indicator_rect.width()
98
+ h = indicator_rect.height()
99
+
100
+ # Add padding (15% on all sides)
101
+ padding = min(w, h) * 0.15
102
+ x += padding
103
+ y += padding
104
+ w -= padding * 2
105
+ h -= padding * 2
106
+
107
+ # Checkmark path: bottom-left to middle, then middle to top-right
108
+ check_x1 = x + w * 0.10
109
+ check_y1 = y + h * 0.50
110
+ check_x2 = x + w * 0.35
111
+ check_y2 = y + h * 0.70
112
+ check_x3 = x + w * 0.90
113
+ check_y3 = y + h * 0.25
114
+
115
+ checkmark = QPolygonF([
116
+ QPointF(check_x1, check_y1),
117
+ QPointF(check_x2, check_y2),
118
+ QPointF(check_x3, check_y3)
119
+ ])
120
+ painter.drawPolyline(checkmark)
121
+ painter.end()
122
+
123
+
124
+ class CheckmarkRadioButton(QRadioButton):
125
+ """Custom radio button with green background when checked"""
126
+
127
+ def __init__(self, text="", parent=None):
128
+ super().__init__(text, parent)
129
+ self.setStyleSheet("""
130
+ QRadioButton {
131
+ font-size: 9pt;
132
+ spacing: 6px;
133
+ }
134
+ QRadioButton::indicator {
135
+ width: 16px;
136
+ height: 16px;
137
+ border: 2px solid #999;
138
+ border-radius: 9px;
139
+ background-color: white;
140
+ }
141
+ QRadioButton::indicator:checked {
142
+ background-color: #4CAF50;
143
+ border-color: #4CAF50;
144
+ }
145
+ QRadioButton::indicator:hover {
146
+ border-color: #666;
147
+ }
148
+ QRadioButton::indicator:checked:hover {
149
+ background-color: #45a049;
150
+ border-color: #45a049;
151
+ }
152
+ """)
153
+
154
+ def paintEvent(self, event):
155
+ """Override paint event to draw white dot when checked"""
156
+ super().paintEvent(event)
157
+
158
+ if self.isChecked():
159
+ from PyQt6.QtWidgets import QStyleOptionButton
160
+ from PyQt6.QtGui import QBrush
161
+
162
+ opt = QStyleOptionButton()
163
+ self.initStyleOption(opt)
164
+ indicator_rect = self.style().subElementRect(
165
+ self.style().SubElement.SE_RadioButtonIndicator,
166
+ opt,
167
+ self
168
+ )
169
+
170
+ if indicator_rect.isValid():
171
+ painter = QPainter(self)
172
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
173
+ painter.setBrush(QBrush(QColor("white")))
174
+ painter.setPen(Qt.PenStyle.NoPen)
175
+
176
+ # Draw white dot in center
177
+ center = indicator_rect.center()
178
+ center_pt = QPointF(center.x(), center.y())
179
+
180
+ # Dot size: 25% of width radius (50% diameter)
181
+ radius = indicator_rect.width() * 0.25
182
+
183
+ painter.drawEllipse(center_pt, radius, radius)
184
+ painter.end()
185
+
186
+
187
+ class HighlightDelegate(QStyledItemDelegate):
188
+ """Custom delegate to highlight filter text in table cells"""
189
+
190
+ def __init__(self, parent=None, column: int = 1):
191
+ super().__init__(parent)
192
+ self.column = column # Column index (1 = Source, 2 = Target)
193
+ self.highlight_text = ""
194
+ self.ignore_case = True
195
+
196
+ def set_highlight(self, text: str, ignore_case: bool = True):
197
+ """Set the text to highlight"""
198
+ self.highlight_text = text
199
+ self.ignore_case = ignore_case
200
+
201
+ def paint(self, painter, option, index):
202
+ """Paint the cell with highlighted text"""
203
+ text = index.data(Qt.ItemDataRole.DisplayRole) or ""
204
+
205
+ if not self.highlight_text or not text:
206
+ # No highlighting needed, use default painting
207
+ super().paint(painter, option, index)
208
+ return
209
+
210
+ # Get the rect and text
211
+ rect = option.rect
212
+ painter.save()
213
+
214
+ # Draw background if selected (check if Selected flag is set)
215
+ is_selected = bool(option.state & QStyle.StateFlag.State_Selected)
216
+ if is_selected:
217
+ painter.fillRect(rect, option.palette.highlight())
218
+ painter.setPen(option.palette.highlightedText().color())
219
+ else:
220
+ painter.setPen(option.palette.text().color())
221
+
222
+ # Find all occurrences of highlight text
223
+ search_text = self.highlight_text
224
+ display_text = text
225
+ if self.ignore_case:
226
+ search_text = search_text.lower()
227
+ display_text = text.lower()
228
+
229
+ # Draw text with highlighting
230
+ fm = QFontMetrics(painter.font())
231
+ x = rect.left() + 2
232
+ y = rect.top() + fm.ascent() + 2
233
+
234
+ # Find all match positions
235
+ matches = []
236
+ start = 0
237
+ while True:
238
+ pos = display_text.find(search_text, start)
239
+ if pos == -1:
240
+ break
241
+ matches.append((pos, pos + len(search_text)))
242
+ start = pos + 1
243
+
244
+ # Draw text segments with highlighting
245
+ last_pos = 0
246
+ for start_pos, end_pos in matches:
247
+ # Draw text before match
248
+ if start_pos > last_pos:
249
+ before_text = text[last_pos:start_pos]
250
+ painter.drawText(x, y, before_text)
251
+ x += fm.horizontalAdvance(before_text)
252
+
253
+ # Draw highlighted match
254
+ match_text = text[start_pos:end_pos]
255
+ # Reduce highlight height to be more compact (just around the text baseline)
256
+ text_height = fm.height() # Full font height (ascent + descent)
257
+ highlight_height = int(text_height * 0.7) # Make it 70% of font height for tighter fit
258
+ highlight_y = y - fm.ascent() + (fm.ascent() - highlight_height) // 2 # Center vertically on text
259
+ highlight_rect = QRect(x, highlight_y, fm.horizontalAdvance(match_text), highlight_height)
260
+ painter.fillRect(highlight_rect, QColor("#90EE90")) # Light green like Heartsome
261
+ painter.drawText(x, y, match_text)
262
+ x += fm.horizontalAdvance(match_text)
263
+
264
+ last_pos = end_pos
265
+
266
+ # Draw remaining text
267
+ if last_pos < len(text):
268
+ remaining_text = text[last_pos:]
269
+ painter.drawText(x, y, remaining_text)
270
+
271
+ painter.restore()
272
+
273
+
274
+ class TmxTagCleanerDialog(QDialog):
275
+ """Dialog for configuring and running tag cleaning on TMX files"""
276
+
277
+ # Default tag patterns with descriptions
278
+ DEFAULT_TAG_PATTERNS = [
279
+ # Basic formatting tags
280
+ {"name": "<b></b>", "pattern": r"</?b>", "description": "Bold tags", "enabled": True, "category": "Formatting"},
281
+ {"name": "<i></i>", "pattern": r"</?i>", "description": "Italic tags", "enabled": True, "category": "Formatting"},
282
+ {"name": "<u></u>", "pattern": r"</?u>", "description": "Underline tags", "enabled": True, "category": "Formatting"},
283
+ {"name": "<bi></bi>", "pattern": r"</?bi>", "description": "Bold-italic tags", "enabled": True, "category": "Formatting"},
284
+ {"name": "<sub></sub>", "pattern": r"</?sub>", "description": "Subscript tags", "enabled": False, "category": "Formatting"},
285
+ {"name": "<sup></sup>", "pattern": r"</?sup>", "description": "Superscript tags", "enabled": False, "category": "Formatting"},
286
+ {"name": "<li-b></li-b>", "pattern": r"</?li-b>", "description": "Bullet list item tags", "enabled": True, "category": "Formatting"},
287
+ {"name": "<li-o></li-o>", "pattern": r"</?li-o>", "description": "Ordered list item tags", "enabled": True, "category": "Formatting"},
288
+
289
+ # TMX/XLIFF inline tags
290
+ {"name": "<bpt>...</bpt>", "pattern": r"<bpt[^>]*>.*?</bpt>", "description": "Begin paired tag", "enabled": False, "category": "TMX/XLIFF"},
291
+ {"name": "<ept>...</ept>", "pattern": r"<ept[^>]*>.*?</ept>", "description": "End paired tag", "enabled": False, "category": "TMX/XLIFF"},
292
+ {"name": "<ph>...</ph>", "pattern": r"<ph[^>]*>.*?</ph>", "description": "Placeholder tag", "enabled": False, "category": "TMX/XLIFF"},
293
+ {"name": "<it>...</it>", "pattern": r"<it[^>]*>.*?</it>", "description": "Isolated tag", "enabled": False, "category": "TMX/XLIFF"},
294
+ {"name": "<hi>...</hi>", "pattern": r"<hi[^>]*>.*?</hi>", "description": "Highlight tag", "enabled": False, "category": "TMX/XLIFF"},
295
+ {"name": "<ut>...</ut>", "pattern": r"<ut[^>]*>.*?</ut>", "description": "Unknown tag", "enabled": False, "category": "TMX/XLIFF"},
296
+
297
+ # memoQ tags
298
+ {"name": "{1} [2} etc.", "pattern": r"(?:\[\d+\}|\{\d+\]|\{\d+\})", "description": "memoQ index tags", "enabled": False, "category": "memoQ"},
299
+
300
+ # Trados tags
301
+ {"name": "<1></1> etc.", "pattern": r"</?(\d+)>", "description": "Trados numbered tags", "enabled": False, "category": "Trados"},
302
+ {"name": "{1}{/1} etc.", "pattern": r"\{/?(\d+)\}", "description": "Trados curly tags", "enabled": False, "category": "Trados"},
303
+
304
+ # Generic XML tags
305
+ {"name": "All XML tags", "pattern": r"<[^>]+>", "description": "Any XML-style tag", "enabled": False, "category": "Generic"},
306
+ ]
307
+
308
+ def __init__(self, parent=None, tu_count: int = 0):
309
+ super().__init__(parent)
310
+ self.setWindowTitle("🧹 Clean Tags from TMX")
311
+ self.setMinimumWidth(600)
312
+ self.setMinimumHeight(500)
313
+ self.tu_count = tu_count
314
+ self.tag_checkboxes: Dict[str, QCheckBox] = {}
315
+
316
+ self.setup_ui()
317
+
318
+ def setup_ui(self):
319
+ """Create the dialog UI"""
320
+ layout = QVBoxLayout(self)
321
+ layout.setSpacing(10)
322
+
323
+ # Info label
324
+ info_label = QLabel(f"Select which tags to remove from {self.tu_count:,} translation units:")
325
+ info_label.setStyleSheet("font-weight: bold; font-size: 11pt;")
326
+ layout.addWidget(info_label)
327
+
328
+ # Tag categories in a scrollable area
329
+ from PyQt6.QtWidgets import QScrollArea
330
+ scroll = QScrollArea()
331
+ scroll.setWidgetResizable(True)
332
+ scroll.setFrameStyle(QFrame.Shape.NoFrame)
333
+
334
+ scroll_content = QWidget()
335
+ scroll_layout = QVBoxLayout(scroll_content)
336
+ scroll_layout.setSpacing(15)
337
+
338
+ # Group tags by category
339
+ categories: Dict[str, list] = {}
340
+ for tag in self.DEFAULT_TAG_PATTERNS:
341
+ cat = tag["category"]
342
+ if cat not in categories:
343
+ categories[cat] = []
344
+ categories[cat].append(tag)
345
+
346
+ # Create a group box for each category
347
+ for category, tags in categories.items():
348
+ group = QGroupBox(category)
349
+ group.setStyleSheet("QGroupBox { font-weight: bold; }")
350
+ group_layout = QVBoxLayout(group)
351
+
352
+ for tag in tags:
353
+ cb = CheckmarkCheckBox(f"{tag['name']} - {tag['description']}")
354
+ cb.setChecked(tag["enabled"])
355
+ cb.tag_pattern = tag["pattern"]
356
+ cb.tag_name = tag["name"]
357
+ self.tag_checkboxes[tag["name"]] = cb
358
+ group_layout.addWidget(cb)
359
+
360
+ scroll_layout.addWidget(group)
361
+
362
+ scroll_layout.addStretch()
363
+ scroll.setWidget(scroll_content)
364
+ layout.addWidget(scroll)
365
+
366
+ # Quick select buttons
367
+ quick_layout = QHBoxLayout()
368
+ btn_select_all = QPushButton("Select All")
369
+ btn_select_all.clicked.connect(self.select_all)
370
+ quick_layout.addWidget(btn_select_all)
371
+
372
+ btn_select_none = QPushButton("Select None")
373
+ btn_select_none.clicked.connect(self.select_none)
374
+ quick_layout.addWidget(btn_select_none)
375
+
376
+ btn_select_formatting = QPushButton("Select Formatting Only")
377
+ btn_select_formatting.clicked.connect(self.select_formatting_only)
378
+ quick_layout.addWidget(btn_select_formatting)
379
+
380
+ quick_layout.addStretch()
381
+ layout.addLayout(quick_layout)
382
+
383
+ # Separator
384
+ separator = QFrame()
385
+ separator.setFrameStyle(QFrame.Shape.HLine | QFrame.Shadow.Sunken)
386
+ layout.addWidget(separator)
387
+
388
+ # Replacement option
389
+ replace_group = QGroupBox("Tag Replacement")
390
+ replace_layout = QVBoxLayout(replace_group)
391
+
392
+ self.replace_nothing = CheckmarkRadioButton("Remove tags completely (no replacement)")
393
+ self.replace_space = CheckmarkRadioButton("Replace tags with a space")
394
+ self.replace_nothing.setChecked(True)
395
+
396
+ replace_layout.addWidget(self.replace_nothing)
397
+ replace_layout.addWidget(self.replace_space)
398
+ layout.addWidget(replace_group)
399
+
400
+ # Scope options
401
+ scope_group = QGroupBox("Scope")
402
+ scope_layout = QVBoxLayout(scope_group)
403
+
404
+ self.scope_both = CheckmarkRadioButton("Clean both source and target segments")
405
+ self.scope_source = CheckmarkRadioButton("Clean source segments only")
406
+ self.scope_target = CheckmarkRadioButton("Clean target segments only")
407
+ self.scope_both.setChecked(True)
408
+
409
+ scope_layout.addWidget(self.scope_both)
410
+ scope_layout.addWidget(self.scope_source)
411
+ scope_layout.addWidget(self.scope_target)
412
+ layout.addWidget(scope_group)
413
+
414
+ # Dialog buttons
415
+ button_box = QDialogButtonBox()
416
+ self.btn_clean = QPushButton("🧹 Clean Tags")
417
+ self.btn_clean.setDefault(True)
418
+ self.btn_preview = QPushButton("👁️ Preview")
419
+ btn_cancel = QPushButton("Cancel")
420
+
421
+ button_box.addButton(self.btn_clean, QDialogButtonBox.ButtonRole.AcceptRole)
422
+ button_box.addButton(self.btn_preview, QDialogButtonBox.ButtonRole.ActionRole)
423
+ button_box.addButton(btn_cancel, QDialogButtonBox.ButtonRole.RejectRole)
424
+
425
+ self.btn_clean.clicked.connect(self.accept)
426
+ self.btn_preview.clicked.connect(self.preview_cleaning)
427
+ btn_cancel.clicked.connect(self.reject)
428
+
429
+ layout.addWidget(button_box)
430
+
431
+ def select_all(self):
432
+ """Select all tag patterns"""
433
+ for cb in self.tag_checkboxes.values():
434
+ cb.setChecked(True)
435
+
436
+ def select_none(self):
437
+ """Deselect all tag patterns"""
438
+ for cb in self.tag_checkboxes.values():
439
+ cb.setChecked(False)
440
+
441
+ def select_formatting_only(self):
442
+ """Select only formatting tags"""
443
+ for name, cb in self.tag_checkboxes.items():
444
+ # Enable formatting tags (b, i, u, bi, sub, sup)
445
+ is_formatting = name in ["<b></b>", "<i></i>", "<u></u>", "<bi></bi>", "<sub></sub>", "<sup></sup>"]
446
+ cb.setChecked(is_formatting)
447
+
448
+ def get_selected_patterns(self) -> List[str]:
449
+ """Get list of selected regex patterns"""
450
+ patterns = []
451
+ for cb in self.tag_checkboxes.values():
452
+ if cb.isChecked():
453
+ patterns.append(cb.tag_pattern)
454
+ return patterns
455
+
456
+ def get_replacement(self) -> str:
457
+ """Get replacement string (empty or space)"""
458
+ return " " if self.replace_space.isChecked() else ""
459
+
460
+ def get_scope(self) -> str:
461
+ """Get scope: 'both', 'source', or 'target'"""
462
+ if self.scope_source.isChecked():
463
+ return "source"
464
+ elif self.scope_target.isChecked():
465
+ return "target"
466
+ return "both"
467
+
468
+ def preview_cleaning(self):
469
+ """Show preview of what will be cleaned"""
470
+ patterns = self.get_selected_patterns()
471
+ if not patterns:
472
+ QMessageBox.information(self, "Preview", "No tag patterns selected.")
473
+ return
474
+
475
+ preview_text = "Selected patterns:\n\n"
476
+ for cb in self.tag_checkboxes.values():
477
+ if cb.isChecked():
478
+ preview_text += f" • {cb.tag_name}\n"
479
+
480
+ preview_text += f"\nReplacement: {'[space]' if self.replace_space.isChecked() else '[nothing]'}"
481
+ preview_text += f"\nScope: {self.get_scope()}"
482
+ preview_text += f"\n\nThis will process {self.tu_count:,} translation units."
483
+
484
+ QMessageBox.information(self, "Preview", preview_text)
485
+
486
+
487
+ class TmxEditorUIQt(QWidget):
488
+ """TMX Editor user interface - PyQt6 version"""
489
+
490
+ def __init__(self, parent=None, standalone=False, db_manager=None):
491
+ """
492
+ Initialize TMX Editor UI
493
+
494
+ Args:
495
+ parent: Parent widget (None for standalone window)
496
+ standalone: If True, creates own window. If False, embeds in parent
497
+ db_manager: DatabaseManager instance for database-backed TMX files (optional)
498
+ """
499
+ super().__init__(parent)
500
+ self.db_manager = db_manager # Database manager for database mode
501
+ self.tmx_file: Optional[TmxFile] = None
502
+ self.tmx_file_id: Optional[int] = None # Database file ID when in database mode
503
+ self.load_mode: str = "ram" # "ram" or "database"
504
+ self.current_page = 0
505
+ self.items_per_page = 50
506
+ self.filtered_tus: List[TmxTranslationUnit] = []
507
+ self.src_lang = ""
508
+ self.tgt_lang = ""
509
+ self.filter_source = ""
510
+ self.filter_target = ""
511
+ self.standalone = standalone
512
+ self.current_edit_tu: Optional[TmxTranslationUnit] = None
513
+ self.tu_row_map: Dict[int, TmxTranslationUnit] = {} # Maps table row to TU
514
+
515
+ # Create highlight delegates for text highlighting (one per column)
516
+ self.highlight_delegate_source = HighlightDelegate(self, column=1)
517
+ self.highlight_delegate_target = HighlightDelegate(self, column=2)
518
+
519
+ self.setup_ui()
520
+
521
+ if standalone:
522
+ self.setWindowTitle("TMX Editor - Supervertaler")
523
+ self.resize(1200, 700)
524
+
525
+ def setup_ui(self):
526
+ """Create the user interface - Heartsome-style layout"""
527
+ main_layout = QVBoxLayout(self)
528
+ main_layout.setContentsMargins(0, 0, 0, 0)
529
+ main_layout.setSpacing(0)
530
+
531
+ # Toolbar
532
+ self.create_toolbar(main_layout)
533
+
534
+ # Top header panel: Language Pair and Search Filter (Heartsome style)
535
+ self.create_top_header_panel(main_layout)
536
+
537
+ # Main content area with splitter (grid center, attributes right)
538
+ from PyQt6.QtWidgets import QSplitter
539
+ content_splitter = QSplitter(Qt.Orientation.Horizontal)
540
+
541
+ # Center panel: Grid editor
542
+ center_panel = QWidget()
543
+ center_layout = QVBoxLayout(center_panel)
544
+ center_layout.setContentsMargins(0, 0, 0, 0)
545
+ center_layout.setSpacing(0)
546
+
547
+ # Grid editor
548
+ self.create_grid_editor(center_layout)
549
+
550
+ # Pagination controls
551
+ self.create_pagination_controls(center_layout)
552
+
553
+ content_splitter.addWidget(center_panel)
554
+ content_splitter.setStretchFactor(0, 1)
555
+
556
+ # Right panel: Attributes Editor
557
+ right_panel = self.create_attributes_editor()
558
+ content_splitter.addWidget(right_panel)
559
+ content_splitter.setStretchFactor(1, 0)
560
+ content_splitter.setSizes([1000, 300]) # Initial widths
561
+
562
+ main_layout.addWidget(content_splitter)
563
+
564
+ # Status bar
565
+ self.create_status_bar(main_layout)
566
+
567
+ def create_toolbar(self, parent_layout):
568
+ """Create toolbar with common actions"""
569
+ toolbar = QFrame()
570
+ toolbar.setFrameStyle(QFrame.Shape.Box)
571
+ toolbar.setStyleSheet("background-color: #f0f0f0; padding: 5px;")
572
+ toolbar_layout = QHBoxLayout(toolbar)
573
+ toolbar_layout.setContentsMargins(5, 5, 5, 5)
574
+
575
+ # File operations
576
+ btn_new = QPushButton("📁 New")
577
+ btn_new.clicked.connect(self.new_tmx)
578
+ toolbar_layout.addWidget(btn_new)
579
+
580
+ btn_open = QPushButton("📂 Open")
581
+ btn_open.clicked.connect(self.open_tmx)
582
+ toolbar_layout.addWidget(btn_open)
583
+
584
+ btn_save = QPushButton("💾 Save")
585
+ btn_save.clicked.connect(self.save_tmx)
586
+ toolbar_layout.addWidget(btn_save)
587
+
588
+ btn_save_as = QPushButton("💾 Save As...")
589
+ btn_save_as.clicked.connect(self.save_tmx_as)
590
+ toolbar_layout.addWidget(btn_save_as)
591
+
592
+ btn_close = QPushButton("❌ Close")
593
+ btn_close.clicked.connect(self.close_tmx)
594
+ toolbar_layout.addWidget(btn_close)
595
+
596
+ toolbar_layout.addWidget(QFrame()) # Spacer
597
+
598
+ # Edit operations
599
+ btn_add = QPushButton("➕ Add TU")
600
+ btn_add.clicked.connect(self.add_translation_unit)
601
+ toolbar_layout.addWidget(btn_add)
602
+
603
+ btn_delete = QPushButton("❌ Delete")
604
+ btn_delete.clicked.connect(self.delete_selected_tu)
605
+ toolbar_layout.addWidget(btn_delete)
606
+
607
+ toolbar_layout.addWidget(QFrame()) # Spacer
608
+
609
+ # View operations
610
+ btn_header = QPushButton("ℹ️ Header")
611
+ btn_header.clicked.connect(self.edit_header)
612
+ toolbar_layout.addWidget(btn_header)
613
+
614
+ btn_stats = QPushButton("📊 Stats")
615
+ btn_stats.clicked.connect(self.show_statistics)
616
+ toolbar_layout.addWidget(btn_stats)
617
+
618
+ btn_validate = QPushButton("✓ Validate")
619
+ btn_validate.clicked.connect(self.validate_tmx)
620
+ toolbar_layout.addWidget(btn_validate)
621
+
622
+ btn_clean_tags = QPushButton("🧹 Clean Tags")
623
+ btn_clean_tags.clicked.connect(self.show_clean_tags_dialog)
624
+ toolbar_layout.addWidget(btn_clean_tags)
625
+
626
+ toolbar_layout.addStretch()
627
+ parent_layout.addWidget(toolbar)
628
+
629
+ def create_top_header_panel(self, parent_layout):
630
+ """Create top header panel with Language Pair and Search Filter - Heartsome style"""
631
+ header_frame = QFrame()
632
+ header_frame.setFrameStyle(QFrame.Shape.Box)
633
+ header_frame.setStyleSheet("background-color: #fffacd; padding: 5px; border: 1px solid #d0d0d0;")
634
+ header_layout = QHBoxLayout(header_frame)
635
+ header_layout.setContentsMargins(10, 5, 10, 5)
636
+ header_layout.setSpacing(15)
637
+
638
+ # Language Pair section
639
+ lang_label = QLabel("Source:")
640
+ lang_label.setStyleSheet("font-weight: bold;")
641
+ header_layout.addWidget(lang_label)
642
+
643
+ self.src_lang_combo = QComboBox()
644
+ self.src_lang_combo.setMinimumWidth(100)
645
+ self.src_lang_combo.currentTextChanged.connect(self.on_language_changed)
646
+ header_layout.addWidget(self.src_lang_combo)
647
+
648
+ lang_label2 = QLabel("Target:")
649
+ lang_label2.setStyleSheet("font-weight: bold;")
650
+ header_layout.addWidget(lang_label2)
651
+
652
+ self.tgt_lang_combo = QComboBox()
653
+ self.tgt_lang_combo.setMinimumWidth(100)
654
+ self.tgt_lang_combo.currentTextChanged.connect(self.on_language_changed)
655
+ header_layout.addWidget(self.tgt_lang_combo)
656
+
657
+ # Spacer
658
+ header_layout.addSpacing(20)
659
+
660
+ # Filter section
661
+ filter_label = QLabel("Filter:")
662
+ filter_label.setStyleSheet("font-weight: bold;")
663
+ header_layout.addWidget(filter_label)
664
+
665
+ # Source search
666
+ self.src_search_label = QLabel(f"Source: {self.src_lang if self.src_lang else 'en'}")
667
+ header_layout.addWidget(self.src_search_label)
668
+ self.filter_source_entry = QLineEdit()
669
+ self.filter_source_entry.setPlaceholderText("Enter source text")
670
+ self.filter_source_entry.setMinimumWidth(150)
671
+ self.filter_source_entry.returnPressed.connect(self.apply_filters)
672
+ header_layout.addWidget(self.filter_source_entry)
673
+
674
+ # Target search
675
+ self.tgt_search_label = QLabel(f"Target: {self.tgt_lang if self.tgt_lang else 'nl'}")
676
+ header_layout.addWidget(self.tgt_search_label)
677
+ self.filter_target_entry = QLineEdit()
678
+ self.filter_target_entry.setPlaceholderText("Enter target text")
679
+ self.filter_target_entry.setMinimumWidth(150)
680
+ self.filter_target_entry.returnPressed.connect(self.apply_filters)
681
+ header_layout.addWidget(self.filter_target_entry)
682
+
683
+ # Filter dropdown
684
+ self.filter_all_segments = QComboBox()
685
+ self.filter_all_segments.addItems(["All Segments"])
686
+ self.filter_all_segments.setMinimumWidth(120)
687
+ header_layout.addWidget(self.filter_all_segments)
688
+
689
+ # Search button
690
+ btn_apply = QPushButton("Search")
691
+ btn_apply.clicked.connect(self.apply_filters)
692
+ btn_apply.setStyleSheet("background-color: #ff9800; color: white; padding: 4px 8px; font-size: 9pt;")
693
+ header_layout.addWidget(btn_apply)
694
+
695
+ # Checkboxes - use custom CheckmarkCheckBox style like AutoFingers
696
+ self.filter_ignore_case = CheckmarkCheckBox("Ignore case")
697
+ self.filter_ignore_case.setChecked(True)
698
+ header_layout.addWidget(self.filter_ignore_case)
699
+
700
+ self.filter_ignore_tags = CheckmarkCheckBox("Ignore tags")
701
+ header_layout.addWidget(self.filter_ignore_tags)
702
+
703
+ # Clear button
704
+ btn_clear = QPushButton("Clear")
705
+ btn_clear.clicked.connect(self.clear_filters)
706
+ btn_clear.setStyleSheet("background-color: #9e9e9e; color: white; padding: 4px 8px; font-size: 9pt;")
707
+ header_layout.addWidget(btn_clear)
708
+
709
+ header_layout.addStretch()
710
+
711
+ parent_layout.addWidget(header_frame)
712
+
713
+ def create_language_panel(self, parent_layout):
714
+ """Create language selection panel - compact version for sidebar"""
715
+ lang_frame = QGroupBox("Language Pair")
716
+ lang_frame.setStyleSheet("background-color: #e8f4f8; padding: 5px;")
717
+ lang_layout = QVBoxLayout(lang_frame)
718
+ lang_layout.setContentsMargins(5, 5, 5, 5)
719
+
720
+ src_layout = QHBoxLayout()
721
+ src_layout.addWidget(QLabel("Source:"))
722
+ self.src_lang_combo = QComboBox()
723
+ self.src_lang_combo.setMinimumWidth(120)
724
+ self.src_lang_combo.currentTextChanged.connect(self.on_language_changed)
725
+ src_layout.addWidget(self.src_lang_combo)
726
+ lang_layout.addLayout(src_layout)
727
+
728
+ tgt_layout = QHBoxLayout()
729
+ tgt_layout.addWidget(QLabel("Target:"))
730
+ self.tgt_lang_combo = QComboBox()
731
+ self.tgt_lang_combo.setMinimumWidth(120)
732
+ self.tgt_lang_combo.currentTextChanged.connect(self.on_language_changed)
733
+ tgt_layout.addWidget(self.tgt_lang_combo)
734
+ lang_layout.addLayout(tgt_layout)
735
+
736
+ btn_all_langs = QPushButton("🌐 All Languages")
737
+ btn_all_langs.clicked.connect(self.show_all_languages)
738
+ btn_all_langs.setStyleSheet("background-color: #4CAF50; color: white; padding: 3px; font-size: 9pt;")
739
+ lang_layout.addWidget(btn_all_langs)
740
+
741
+ parent_layout.addWidget(lang_frame)
742
+
743
+ def create_filter_panel(self, parent_layout):
744
+ """Create filter panel - Heartsome style"""
745
+ filter_frame = QGroupBox("🔍 Search & Filter")
746
+ filter_frame.setStyleSheet("background-color: #fff3cd; padding: 5px;")
747
+ filter_layout = QVBoxLayout(filter_frame)
748
+ filter_layout.setContentsMargins(5, 5, 5, 5)
749
+
750
+ # Search section
751
+ search_layout = QVBoxLayout()
752
+
753
+ src_search_layout = QHBoxLayout()
754
+ src_search_layout.addWidget(QLabel(f"Source: {self.src_lang if self.src_lang else 'en'}"))
755
+ self.filter_source_entry = QLineEdit()
756
+ self.filter_source_entry.setPlaceholderText("Enter source for search")
757
+ self.filter_source_entry.returnPressed.connect(self.apply_filters)
758
+ src_search_layout.addWidget(self.filter_source_entry)
759
+ search_layout.addLayout(src_search_layout)
760
+
761
+ tgt_search_layout = QHBoxLayout()
762
+ tgt_search_layout.addWidget(QLabel(f"Target: {self.tgt_lang if self.tgt_lang else 'nl'}"))
763
+ self.filter_target_entry = QLineEdit()
764
+ self.filter_target_entry.setPlaceholderText("Enter translation for search")
765
+ self.filter_target_entry.returnPressed.connect(self.apply_filters)
766
+ tgt_search_layout.addWidget(self.filter_target_entry)
767
+ search_layout.addLayout(tgt_search_layout)
768
+
769
+ btn_apply = QPushButton("Search")
770
+ btn_apply.clicked.connect(self.apply_filters)
771
+ btn_apply.setStyleSheet("background-color: #ff9800; color: white; padding: 3px; font-size: 9pt;")
772
+ search_layout.addWidget(btn_apply)
773
+
774
+ filter_layout.addLayout(search_layout)
775
+
776
+ # Filter options
777
+ filter_options_layout = QVBoxLayout()
778
+
779
+ filter_label = QLabel("Filter:")
780
+ filter_label.setStyleSheet("font-weight: bold;")
781
+ filter_options_layout.addWidget(filter_label)
782
+
783
+ self.filter_all_segments = QComboBox()
784
+ self.filter_all_segments.addItems(["All Segments"])
785
+ filter_options_layout.addWidget(self.filter_all_segments)
786
+
787
+ self.filter_ignore_case = CheckmarkCheckBox("✔ Ignore case")
788
+ self.filter_ignore_case.setChecked(True)
789
+ filter_options_layout.addWidget(self.filter_ignore_case)
790
+
791
+ self.filter_ignore_tags = CheckmarkCheckBox("Ignore tags")
792
+ filter_options_layout.addWidget(self.filter_ignore_tags)
793
+
794
+ filter_layout.addLayout(filter_options_layout)
795
+
796
+ btn_clear = QPushButton("Clear")
797
+ btn_clear.clicked.connect(self.clear_filters)
798
+ btn_clear.setStyleSheet("background-color: #9e9e9e; color: white; padding: 3px; font-size: 9pt;")
799
+ filter_layout.addWidget(btn_clear)
800
+
801
+ parent_layout.addWidget(filter_frame)
802
+
803
+ def create_grid_editor(self, parent_layout):
804
+ """Create grid editor for translation units using QTableWidget - Heartsome style"""
805
+ self.table = QTableWidget()
806
+ self.table.setColumnCount(4) # No., Source, Target, System Attributes
807
+ self.table.setHorizontalHeaderLabels(['No.', 'Source', 'Target', 'System Attributes'])
808
+ self.table.horizontalHeader().setStretchLastSection(False)
809
+ self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # No. column fixed
810
+ self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Source stretch
811
+ self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Target stretch
812
+ self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Attributes fixed
813
+ self.table.setColumnWidth(0, 50) # No. column
814
+ self.table.setColumnWidth(3, 120) # System Attributes column
815
+ self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
816
+ self.table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
817
+ self.table.setAlternatingRowColors(True)
818
+ self.table.setStyleSheet("""
819
+ QTableWidget {
820
+ gridline-color: #d0d0d0;
821
+ background-color: white;
822
+ }
823
+ QTableWidget::item {
824
+ padding: 2px;
825
+ }
826
+ QTableWidget::item:selected {
827
+ background-color: #add8e6; /* Light blue for selected row */
828
+ }
829
+ """)
830
+
831
+ # Enable inline editing for Source and Target columns (columns 1 and 2)
832
+ self.table.setEditTriggers(
833
+ QAbstractItemView.EditTrigger.DoubleClicked |
834
+ QAbstractItemView.EditTrigger.SelectedClicked |
835
+ QAbstractItemView.EditTrigger.EditKeyPressed
836
+ )
837
+
838
+ # Set highlight delegates for Source and Target columns
839
+ self.table.setItemDelegateForColumn(1, self.highlight_delegate_source) # Source column
840
+ self.table.setItemDelegateForColumn(2, self.highlight_delegate_target) # Target column
841
+
842
+ # Enable word wrap
843
+ self.table.setWordWrap(True)
844
+
845
+ # Connect signals
846
+ self.table.itemSelectionChanged.connect(self.on_table_selection_changed)
847
+ self.table.itemDoubleClicked.connect(self.on_table_double_clicked)
848
+ self.table.itemChanged.connect(self.on_cell_edited) # Handle cell edits
849
+ self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
850
+ self.table.customContextMenuRequested.connect(self.show_context_menu)
851
+
852
+ parent_layout.addWidget(self.table)
853
+
854
+ def create_pagination_controls(self, parent_layout):
855
+ """Create pagination controls"""
856
+ page_frame = QFrame()
857
+ page_frame.setFrameStyle(QFrame.Shape.Box)
858
+ page_frame.setStyleSheet("background-color: #f0f0f0; padding: 5px;")
859
+ page_layout = QHBoxLayout(page_frame)
860
+ page_layout.setContentsMargins(10, 5, 10, 5)
861
+
862
+ self.page_label = QLabel("Page 0 of 0 (0 TUs)")
863
+ page_layout.addWidget(self.page_label)
864
+
865
+ page_layout.addStretch()
866
+
867
+ # Navigation buttons
868
+ btn_first = QPushButton("⏮️ First")
869
+ btn_first.clicked.connect(self.first_page)
870
+ page_layout.addWidget(btn_first)
871
+
872
+ btn_prev = QPushButton("◀️ Prev")
873
+ btn_prev.clicked.connect(self.prev_page)
874
+ page_layout.addWidget(btn_prev)
875
+
876
+ btn_next = QPushButton("Next ▶️")
877
+ btn_next.clicked.connect(self.next_page)
878
+ page_layout.addWidget(btn_next)
879
+
880
+ btn_last = QPushButton("Last ⏭️")
881
+ btn_last.clicked.connect(self.last_page)
882
+ page_layout.addWidget(btn_last)
883
+
884
+ parent_layout.addWidget(page_frame)
885
+
886
+ def create_attributes_editor(self) -> QWidget:
887
+ """Create Attributes Editor panel (right side) - Heartsome style"""
888
+ attributes_panel = QWidget()
889
+ attributes_layout = QVBoxLayout(attributes_panel)
890
+ attributes_layout.setContentsMargins(5, 5, 5, 5)
891
+ attributes_layout.setSpacing(5)
892
+
893
+ title = QLabel("Attributes Editor")
894
+ title.setStyleSheet("font-size: 11pt; font-weight: bold; padding: 5px;")
895
+ attributes_layout.addWidget(title)
896
+
897
+ # System Attributes section
898
+ sys_attr_group = QGroupBox("System Attributes")
899
+ sys_attr_layout = QFormLayout(sys_attr_group)
900
+ sys_attr_layout.setSpacing(5)
901
+
902
+ # Create fields for system attributes
903
+ self.attr_creation_date = QLineEdit()
904
+ self.attr_creation_date.setReadOnly(True)
905
+ self.attr_creation_date.setStyleSheet("background-color: #f5f5f5;")
906
+ sys_attr_layout.addRow("Creation Date:", self.attr_creation_date)
907
+
908
+ self.attr_creation_id = QLineEdit()
909
+ self.attr_creation_id.setReadOnly(True)
910
+ self.attr_creation_id.setStyleSheet("background-color: #f5f5f5;")
911
+ sys_attr_layout.addRow("Creation ID:", self.attr_creation_id)
912
+
913
+ self.attr_change_date = QLineEdit()
914
+ self.attr_change_date.setReadOnly(True)
915
+ self.attr_change_date.setStyleSheet("background-color: #f5f5f5;")
916
+ sys_attr_layout.addRow("Change Date:", self.attr_change_date)
917
+
918
+ self.attr_change_id = QLineEdit()
919
+ self.attr_change_id.setReadOnly(True)
920
+ self.attr_change_id.setStyleSheet("background-color: #f5f5f5;")
921
+ sys_attr_layout.addRow("Change ID:", self.attr_change_id)
922
+
923
+ attributes_layout.addWidget(sys_attr_group)
924
+
925
+ # Custom Attributes section
926
+ custom_attr_group = QGroupBox("Custom Attributes")
927
+ custom_attr_layout = QVBoxLayout(custom_attr_group)
928
+ self.custom_attributes_text = QTextEdit()
929
+ self.custom_attributes_text.setReadOnly(True)
930
+ self.custom_attributes_text.setMaximumHeight(100)
931
+ self.custom_attributes_text.setPlaceholderText("No custom attributes")
932
+ custom_attr_layout.addWidget(self.custom_attributes_text)
933
+ attributes_layout.addWidget(custom_attr_group)
934
+
935
+ # Language-specific Attributes section
936
+ lang_attr_group = QGroupBox("Language-specific Attributes")
937
+ lang_attr_layout = QFormLayout(lang_attr_group)
938
+
939
+ self.lang_attr_creation_date = QLineEdit()
940
+ self.lang_attr_creation_date.setReadOnly(True)
941
+ self.lang_attr_creation_date.setStyleSheet("background-color: #f5f5f5;")
942
+ lang_attr_layout.addRow("Creation Date:", self.lang_attr_creation_date)
943
+
944
+ self.lang_attr_change_date = QLineEdit()
945
+ self.lang_attr_change_date.setReadOnly(True)
946
+ self.lang_attr_change_date.setStyleSheet("background-color: #f5f5f5;")
947
+ lang_attr_layout.addRow("Change Date:", self.lang_attr_change_date)
948
+
949
+ attributes_layout.addWidget(lang_attr_group)
950
+
951
+ # Comments section
952
+ comments_group = QGroupBox("Comments")
953
+ comments_layout = QVBoxLayout(comments_group)
954
+ self.comments_text = QTextEdit()
955
+ self.comments_text.setReadOnly(True)
956
+ self.comments_text.setPlaceholderText("No comments")
957
+ comments_layout.addWidget(self.comments_text)
958
+ attributes_layout.addWidget(comments_group)
959
+
960
+ attributes_layout.addStretch()
961
+
962
+ return attributes_panel
963
+
964
+ def update_attributes_display(self, tu: TmxTranslationUnit):
965
+ """Update the Attributes Editor panel with TU attributes"""
966
+ # System Attributes
967
+ self.attr_creation_date.setText(tu.creation_date if tu.creation_date else "")
968
+ self.attr_creation_id.setText(tu.creation_id if tu.creation_id else "")
969
+ self.attr_change_date.setText(tu.change_date if tu.change_date else "")
970
+ self.attr_change_id.setText(tu.change_id if tu.change_id else "")
971
+
972
+ # Language-specific attributes (from segments)
973
+ src_seg = tu.get_segment(self.src_lang)
974
+ if src_seg:
975
+ self.lang_attr_creation_date.setText(src_seg.creation_date if src_seg.creation_date else "")
976
+ self.lang_attr_change_date.setText(src_seg.change_date if src_seg.change_date else "")
977
+ else:
978
+ self.lang_attr_creation_date.setText("")
979
+ self.lang_attr_change_date.setText("")
980
+
981
+ # Custom attributes and comments - empty for now
982
+ self.custom_attributes_text.clear()
983
+ self.comments_text.clear()
984
+
985
+ def clear_attributes_display(self):
986
+ """Clear the Attributes Editor panel"""
987
+ self.attr_creation_date.clear()
988
+ self.attr_creation_id.clear()
989
+ self.attr_change_date.clear()
990
+ self.attr_change_id.clear()
991
+ self.lang_attr_creation_date.clear()
992
+ self.lang_attr_change_date.clear()
993
+ self.custom_attributes_text.clear()
994
+ self.comments_text.clear()
995
+
996
+ def create_status_bar(self, parent_layout):
997
+ """Create status bar"""
998
+ self.status_bar = QLabel("Ready")
999
+ self.status_bar.setStyleSheet("background-color: #e0e0e0; padding: 3px; border: 1px solid #ccc;")
1000
+ self.status_bar.setMinimumHeight(20)
1001
+ parent_layout.addWidget(self.status_bar)
1002
+
1003
+ # ===== File Operations =====
1004
+
1005
+ def new_tmx(self):
1006
+ """Create new TMX file"""
1007
+ if self.tmx_file and self.tmx_file.is_modified:
1008
+ reply = QMessageBox.question(
1009
+ self, "Unsaved Changes",
1010
+ "Current file has unsaved changes. Continue?",
1011
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
1012
+ )
1013
+ if reply != QMessageBox.StandardButton.Yes:
1014
+ return
1015
+
1016
+ # Prompt for languages
1017
+ dialog = QDialog(self)
1018
+ dialog.setWindowTitle("New TMX File")
1019
+ dialog.resize(400, 250)
1020
+
1021
+ layout = QVBoxLayout(dialog)
1022
+
1023
+ title = QLabel("Create New TMX File")
1024
+ title.setStyleSheet("font-size: 12pt; font-weight: bold;")
1025
+ layout.addWidget(title)
1026
+
1027
+ form = QFormLayout()
1028
+
1029
+ src_entry = QLineEdit("en-US")
1030
+ form.addRow("Source Language:", src_entry)
1031
+
1032
+ tgt_entry = QLineEdit("nl-NL")
1033
+ form.addRow("Target Language:", tgt_entry)
1034
+
1035
+ creator_entry = QLineEdit(os.getlogin() if hasattr(os, 'getlogin') else "user")
1036
+ form.addRow("Creator ID:", creator_entry)
1037
+
1038
+ layout.addLayout(form)
1039
+
1040
+ buttons = QDialogButtonBox(
1041
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
1042
+ )
1043
+ buttons.accepted.connect(dialog.accept)
1044
+ buttons.rejected.connect(dialog.reject)
1045
+ layout.addWidget(buttons)
1046
+
1047
+ if dialog.exec() == QDialog.DialogCode.Accepted:
1048
+ src = src_entry.text().strip()
1049
+ tgt = tgt_entry.text().strip()
1050
+ creator = creator_entry.text().strip()
1051
+
1052
+ if not src or not tgt:
1053
+ QMessageBox.warning(self, "Error", "Please enter both source and target languages")
1054
+ return
1055
+
1056
+ self.tmx_file = TmxFile()
1057
+ self.tmx_file.header.srclang = src
1058
+ self.tmx_file.header.creation_id = creator
1059
+ self.tmx_file.header.change_id = creator
1060
+ self.tmx_file.languages = [src, tgt]
1061
+
1062
+ # Add one empty translation unit
1063
+ tu = TmxTranslationUnit(tu_id=1, creation_id=creator)
1064
+ tu.set_segment(src, "")
1065
+ tu.set_segment(tgt, "")
1066
+ self.tmx_file.add_translation_unit(tu)
1067
+
1068
+ self.src_lang = src
1069
+ self.tgt_lang = tgt
1070
+
1071
+ self.refresh_ui()
1072
+ self.set_status(f"Created new TMX file: {src} → {tgt}")
1073
+
1074
+ def open_tmx(self):
1075
+ """Open TMX file with RAM/DB/Auto mode selection"""
1076
+ if self.tmx_file and self.tmx_file.is_modified:
1077
+ reply = QMessageBox.question(
1078
+ self, "Unsaved Changes",
1079
+ "Current file has unsaved changes. Continue?",
1080
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
1081
+ )
1082
+ if reply != QMessageBox.StandardButton.Yes:
1083
+ return
1084
+
1085
+ file_path, _ = QFileDialog.getOpenFileName(
1086
+ self, "Open TMX File", "", "TMX files (*.tmx);;All files (*.*)"
1087
+ )
1088
+
1089
+ if not file_path:
1090
+ return
1091
+
1092
+ # Get file size
1093
+ file_size = os.path.getsize(file_path)
1094
+ file_size_mb = file_size / (1024 * 1024)
1095
+
1096
+ # Determine recommended mode
1097
+ if file_size_mb < 50:
1098
+ recommended_mode = "ram"
1099
+ recommended_text = "RAM mode"
1100
+ elif file_size_mb > 100:
1101
+ recommended_mode = "database"
1102
+ recommended_text = "Database mode"
1103
+ else:
1104
+ recommended_mode = "database"
1105
+ recommended_text = "Database mode"
1106
+
1107
+ # Show mode selection dialog
1108
+ dialog = QDialog(self)
1109
+ dialog.setWindowTitle("Open TMX File")
1110
+ dialog.resize(500, 250)
1111
+ layout = QVBoxLayout(dialog)
1112
+
1113
+ # File info
1114
+ info_label = QLabel(f"File: {os.path.basename(file_path)}\nSize: {file_size_mb:.2f} MB")
1115
+ info_label.setStyleSheet("font-weight: bold; font-size: 10pt; padding: 10px;")
1116
+ layout.addWidget(info_label)
1117
+
1118
+ # Important note about what this dialog does
1119
+ explanation_label = QLabel(
1120
+ "Choose how to load this TMX file:\n"
1121
+ "• RAM Mode: Entire file loads into memory (fast, but limited by RAM)\n"
1122
+ "• Database Mode: File stored in database (slower load, but handles huge files)\n"
1123
+ "• Auto: Automatically selects the best mode for this file size"
1124
+ )
1125
+ explanation_label.setStyleSheet("background-color: #fff9e6; padding: 10px; border: 1px solid #ffd700; border-radius: 5px; font-size: 9pt;")
1126
+ layout.addWidget(explanation_label)
1127
+
1128
+ # Mode selection with CheckmarkCheckBox style (mutually exclusive)
1129
+ mode_group = QGroupBox("Select Load Mode (click to choose)")
1130
+ mode_layout = QVBoxLayout(mode_group)
1131
+ mode_layout.setSpacing(8)
1132
+
1133
+ # Auto mode - show what it will actually do
1134
+ auto_text = f"Auto → Current file will load in {recommended_text}"
1135
+ mode_auto = CheckmarkCheckBox(auto_text)
1136
+ mode_auto.setChecked(True)
1137
+ mode_layout.addWidget(mode_auto)
1138
+
1139
+ # RAM mode
1140
+ mode_ram = CheckmarkCheckBox("RAM Mode → Loads entire file into RAM memory")
1141
+ mode_layout.addWidget(mode_ram)
1142
+
1143
+ # Database mode
1144
+ mode_db = CheckmarkCheckBox("Database Mode → Stores file in SQLite database")
1145
+ mode_layout.addWidget(mode_db)
1146
+
1147
+ # Make them mutually exclusive (like radio buttons)
1148
+ # Prevent unchecking - always keep one selected
1149
+ _updating_checks = False
1150
+
1151
+ def handle_toggle(toggled_widget):
1152
+ """Handle toggle - make mutually exclusive and prevent unchecking"""
1153
+ nonlocal _updating_checks
1154
+ if _updating_checks:
1155
+ return
1156
+
1157
+ if toggled_widget.isChecked():
1158
+ # Uncheck others
1159
+ _updating_checks = True
1160
+ for widget in [mode_auto, mode_ram, mode_db]:
1161
+ if widget != toggled_widget:
1162
+ widget.setChecked(False)
1163
+ _updating_checks = False
1164
+ else:
1165
+ # Prevent unchecking - re-check this one (like radio buttons)
1166
+ _updating_checks = True
1167
+ toggled_widget.setChecked(True)
1168
+ _updating_checks = False
1169
+
1170
+ mode_auto.toggled.connect(lambda checked: handle_toggle(mode_auto))
1171
+ mode_ram.toggled.connect(lambda checked: handle_toggle(mode_ram))
1172
+ mode_db.toggled.connect(lambda checked: handle_toggle(mode_db))
1173
+
1174
+ layout.addWidget(mode_group)
1175
+
1176
+ # Buttons
1177
+ buttons = QDialogButtonBox(
1178
+ QDialogButtonBox.StandardButton.Open | QDialogButtonBox.StandardButton.Cancel
1179
+ )
1180
+ buttons.accepted.connect(dialog.accept)
1181
+ buttons.rejected.connect(dialog.reject)
1182
+ layout.addWidget(buttons)
1183
+
1184
+ if dialog.exec() != QDialog.DialogCode.Accepted:
1185
+ return
1186
+
1187
+ # Determine selected mode (only one should be checked due to mutual exclusivity)
1188
+ if mode_auto.isChecked():
1189
+ selected_mode = recommended_mode
1190
+ elif mode_ram.isChecked():
1191
+ selected_mode = "ram"
1192
+ elif mode_db.isChecked():
1193
+ selected_mode = "database"
1194
+ else:
1195
+ # Fallback: if somehow none is checked, use auto
1196
+ selected_mode = recommended_mode
1197
+
1198
+ # Load file
1199
+ try:
1200
+ if selected_mode == "database":
1201
+ self._open_tmx_database(file_path, file_size)
1202
+ else:
1203
+ self._open_tmx_ram(file_path)
1204
+ except Exception as e:
1205
+ QMessageBox.critical(self, "Error", f"Failed to open TMX file:\n{str(e)}")
1206
+
1207
+ def _open_tmx_ram(self, file_path: str):
1208
+ """Open TMX file in RAM mode"""
1209
+ self.tmx_file = TmxParser.parse_file(file_path)
1210
+ self.tmx_file.file_path = file_path
1211
+ self.load_mode = "ram"
1212
+ self.tmx_file_id = None
1213
+
1214
+ # Set default languages (first two in file)
1215
+ langs = self.tmx_file.get_languages()
1216
+ if len(langs) >= 2:
1217
+ self.src_lang = langs[0]
1218
+ self.tgt_lang = langs[1]
1219
+ elif len(langs) == 1:
1220
+ self.src_lang = langs[0]
1221
+ self.tgt_lang = langs[0]
1222
+
1223
+ self.refresh_ui()
1224
+ self.set_status(f"Opened (RAM): {os.path.basename(file_path)} ({self.tmx_file.get_tu_count()} TUs)")
1225
+
1226
+ def _open_tmx_database(self, file_path: str, file_size: int):
1227
+ """Open TMX file in Database mode"""
1228
+ if not self.db_manager:
1229
+ QMessageBox.warning(self, "Warning", "Database manager not available. Using RAM mode instead.")
1230
+ self._open_tmx_ram(file_path)
1231
+ return
1232
+
1233
+ # Parse file first to get header and TU count
1234
+ temp_tmx = TmxParser.parse_file(file_path)
1235
+ tu_count = temp_tmx.get_tu_count()
1236
+ languages = temp_tmx.get_languages()
1237
+
1238
+ # Prepare header data
1239
+ header_data = {
1240
+ 'creation_tool': temp_tmx.header.creation_tool,
1241
+ 'creation_tool_version': temp_tmx.header.creation_tool_version,
1242
+ 'segtype': temp_tmx.header.segtype,
1243
+ 'o_tmf': temp_tmx.header.o_tmf,
1244
+ 'adminlang': temp_tmx.header.adminlang,
1245
+ 'srclang': temp_tmx.header.srclang,
1246
+ 'datatype': temp_tmx.header.datatype,
1247
+ 'creation_date': temp_tmx.header.creation_date,
1248
+ 'creation_id': temp_tmx.header.creation_id,
1249
+ 'change_date': temp_tmx.header.change_date,
1250
+ 'change_id': temp_tmx.header.change_id,
1251
+ }
1252
+
1253
+ # Store file metadata
1254
+ file_name = os.path.basename(file_path)
1255
+ # Use a normalized path for database storage (to handle same file opened multiple times)
1256
+ db_file_path = f"tmx://{file_name}"
1257
+
1258
+ self.tmx_file_id = self.db_manager.tmx_store_file(
1259
+ file_path=db_file_path,
1260
+ file_name=file_name,
1261
+ original_file_path=file_path,
1262
+ load_mode="database",
1263
+ file_size=file_size,
1264
+ header_data=header_data,
1265
+ tu_count=tu_count,
1266
+ languages=languages
1267
+ )
1268
+
1269
+ # Store all translation units with batching for performance
1270
+ progress = QProgressDialog("Loading TMX into database...", "Cancel", 0, tu_count, self)
1271
+ progress.setWindowModality(Qt.WindowModality.WindowModal)
1272
+ progress.setMinimumDuration(0) # Show immediately
1273
+
1274
+ # Style the progress bar for better visibility - clearer blue
1275
+ progress.setStyleSheet("""
1276
+ QProgressBar {
1277
+ border: 2px solid #1976D2;
1278
+ border-radius: 5px;
1279
+ text-align: center;
1280
+ font-weight: bold;
1281
+ background-color: #E3F2FD;
1282
+ height: 25px;
1283
+ }
1284
+ QProgressBar::chunk {
1285
+ background-color: #2196F3;
1286
+ border-radius: 3px;
1287
+ }
1288
+ """)
1289
+
1290
+ progress.show()
1291
+ QApplication.processEvents() # Force initial display
1292
+
1293
+ # Batch size for database operations (commit every N TUs)
1294
+ BATCH_SIZE = 100
1295
+
1296
+ try:
1297
+ # Start transaction for better performance
1298
+ self.db_manager.connection.execute("BEGIN TRANSACTION")
1299
+
1300
+ for i, tu in enumerate(temp_tmx.translation_units):
1301
+ if progress.wasCanceled():
1302
+ # Rollback transaction
1303
+ self.db_manager.connection.rollback()
1304
+ # Delete partial data
1305
+ self.db_manager.tmx_delete_file(self.tmx_file_id)
1306
+ return
1307
+
1308
+ # Store TU (no commit - batch operation)
1309
+ tu_db_id = self.db_manager.tmx_store_translation_unit(
1310
+ tmx_file_id=self.tmx_file_id,
1311
+ tu_id=tu.tu_id,
1312
+ creation_date=tu.creation_date,
1313
+ creation_id=tu.creation_id,
1314
+ change_date=tu.change_date,
1315
+ change_id=tu.change_id,
1316
+ srclang=tu.srclang,
1317
+ custom_attributes=None, # TODO: Extract custom attributes
1318
+ comments=None, # TODO: Extract comments
1319
+ commit=False # Batch commit
1320
+ )
1321
+
1322
+ # Store segments (no commit - batch operation)
1323
+ for lang, segment in tu.segments.items():
1324
+ self.db_manager.tmx_store_segment(
1325
+ tu_db_id=tu_db_id,
1326
+ lang=lang,
1327
+ text=segment.text,
1328
+ creation_date=segment.creation_date,
1329
+ creation_id=segment.creation_id,
1330
+ change_date=segment.change_date,
1331
+ change_id=segment.change_id,
1332
+ commit=False # Batch commit
1333
+ )
1334
+
1335
+ # Commit batch and update progress every BATCH_SIZE items
1336
+ if (i + 1) % BATCH_SIZE == 0 or (i + 1) == tu_count:
1337
+ self.db_manager.connection.commit()
1338
+ if (i + 1) < tu_count:
1339
+ # Start next transaction
1340
+ self.db_manager.connection.execute("BEGIN TRANSACTION")
1341
+
1342
+ # Update progress (less frequently for better performance)
1343
+ progress.setValue(i + 1)
1344
+ # Process events every batch, not every item
1345
+ QApplication.processEvents()
1346
+
1347
+ except Exception as e:
1348
+ # Rollback on error
1349
+ self.db_manager.connection.rollback()
1350
+ raise
1351
+ finally:
1352
+ # Ensure transaction is closed
1353
+ try:
1354
+ self.db_manager.connection.commit()
1355
+ except:
1356
+ pass
1357
+ progress.close()
1358
+
1359
+ # Set mode and clear RAM file
1360
+ self.load_mode = "database"
1361
+ self.tmx_file = None # No longer needed in RAM
1362
+
1363
+ # Set default languages
1364
+ if len(languages) >= 2:
1365
+ self.src_lang = languages[0]
1366
+ self.tgt_lang = languages[1]
1367
+ elif len(languages) == 1:
1368
+ self.src_lang = languages[0]
1369
+ self.tgt_lang = languages[0]
1370
+
1371
+ self.refresh_ui()
1372
+ self.set_status(f"Opened (Database): {file_name} ({tu_count} TUs)")
1373
+
1374
+ def save_tmx(self):
1375
+ """Save TMX file"""
1376
+ if self.load_mode == "database":
1377
+ # Database mode - export to TMX file
1378
+ if not self.db_manager or not self.tmx_file_id:
1379
+ QMessageBox.warning(self, "Warning", "No file to save")
1380
+ return
1381
+
1382
+ # Get file info to determine save path
1383
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
1384
+ if not file_info:
1385
+ QMessageBox.warning(self, "Warning", "File information not found")
1386
+ return
1387
+
1388
+ original_path = file_info.get('original_file_path')
1389
+ if not original_path:
1390
+ # No original path, use Save As
1391
+ self.save_tmx_as()
1392
+ return
1393
+
1394
+ # Export database to TMX file
1395
+ self._export_database_to_tmx(original_path)
1396
+ return
1397
+
1398
+ # RAM mode
1399
+ if not self.tmx_file:
1400
+ QMessageBox.warning(self, "Warning", "No file to save")
1401
+ return
1402
+
1403
+ if not self.tmx_file.file_path:
1404
+ self.save_tmx_as()
1405
+ return
1406
+
1407
+ try:
1408
+ # Update change date
1409
+ self.tmx_file.header.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
1410
+
1411
+ TmxParser.save_file(self.tmx_file, self.tmx_file.file_path)
1412
+ self.tmx_file.is_modified = False
1413
+ self.set_status(f"Saved: {os.path.basename(self.tmx_file.file_path)}")
1414
+
1415
+ except Exception as e:
1416
+ QMessageBox.critical(self, "Error", f"Failed to save TMX file:\n{str(e)}")
1417
+
1418
+ def close_tmx(self):
1419
+ """Close current TMX file"""
1420
+ # Check for unsaved changes (only relevant for RAM mode)
1421
+ if self.load_mode == "ram" and self.tmx_file and self.tmx_file.is_modified:
1422
+ reply = QMessageBox.question(
1423
+ self, "Unsaved Changes",
1424
+ "Current file has unsaved changes. Close without saving?",
1425
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel
1426
+ )
1427
+ if reply == QMessageBox.StandardButton.Cancel:
1428
+ return
1429
+ elif reply == QMessageBox.StandardButton.No:
1430
+ # User wants to save first
1431
+ self.save_tmx()
1432
+ if self.tmx_file and self.tmx_file.is_modified:
1433
+ # Save was cancelled or failed
1434
+ return
1435
+
1436
+ # For database mode, optionally delete from database (or keep for future sessions)
1437
+ # For now, we'll keep it in database - user can reopen it
1438
+
1439
+ # Clear the file
1440
+ self.tmx_file = None
1441
+ self.tmx_file_id = None
1442
+ self.load_mode = "ram"
1443
+ self.filtered_tus = []
1444
+ self.current_page = 0
1445
+ self.tu_row_map.clear()
1446
+
1447
+ # Clear UI
1448
+ self.table.setRowCount(0)
1449
+ self.page_label.setText("No file open")
1450
+ self.clear_attributes_display()
1451
+
1452
+ # Clear language combos
1453
+ self.src_lang_combo.clear()
1454
+ self.tgt_lang_combo.clear()
1455
+
1456
+ # Clear filter fields
1457
+ self.filter_source_entry.clear()
1458
+ self.filter_target_entry.clear()
1459
+
1460
+ # Clear header labels
1461
+ if hasattr(self, 'src_search_label'):
1462
+ self.src_search_label.setText("Source:")
1463
+ if hasattr(self, 'tgt_search_label'):
1464
+ self.tgt_search_label.setText("Target:")
1465
+
1466
+ self.src_lang = ""
1467
+ self.tgt_lang = ""
1468
+
1469
+ self.set_status("TMX file closed")
1470
+
1471
+ def save_tmx_as(self):
1472
+ """Save TMX file with new name"""
1473
+ file_path, _ = QFileDialog.getSaveFileName(
1474
+ self, "Save TMX File", "", "TMX files (*.tmx);;All files (*.*)"
1475
+ )
1476
+
1477
+ if not file_path:
1478
+ return
1479
+
1480
+ try:
1481
+ if self.load_mode == "database":
1482
+ # Export database to TMX file
1483
+ self._export_database_to_tmx(file_path)
1484
+ else:
1485
+ # RAM mode - save directly
1486
+ if not self.tmx_file:
1487
+ QMessageBox.warning(self, "Warning", "No file to save")
1488
+ return
1489
+
1490
+ # Update change date
1491
+ self.tmx_file.header.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
1492
+
1493
+ TmxParser.save_file(self.tmx_file, file_path)
1494
+ self.tmx_file.file_path = file_path
1495
+ self.tmx_file.is_modified = False
1496
+ self.set_status(f"Saved as: {os.path.basename(file_path)}")
1497
+
1498
+ except Exception as e:
1499
+ QMessageBox.critical(self, "Error", f"Failed to save TMX file:\n{str(e)}")
1500
+
1501
+ def _export_database_to_tmx(self, file_path: str):
1502
+ """Export database-backed TMX to TMX file"""
1503
+ if not self.db_manager or not self.tmx_file_id:
1504
+ raise ValueError("Database manager or file ID not available")
1505
+
1506
+ # Get file info
1507
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
1508
+ if not file_info:
1509
+ raise ValueError("File information not found")
1510
+
1511
+ # Create TmxFile object from database
1512
+ tmx_file = TmxFile()
1513
+
1514
+ # Set header from database
1515
+ header_data = file_info['header_data']
1516
+ for key, value in header_data.items():
1517
+ if hasattr(tmx_file.header, key):
1518
+ setattr(tmx_file.header, key, value)
1519
+
1520
+ # Get all translation units (without pagination for export)
1521
+ progress = QProgressDialog("Exporting TMX file...", "Cancel", 0, file_info['tu_count'], self)
1522
+ progress.setWindowModality(Qt.WindowModality.WindowModal)
1523
+ progress.setMinimumDuration(0)
1524
+
1525
+ # Style the progress bar for better visibility - clearer blue
1526
+ progress.setStyleSheet("""
1527
+ QProgressBar {
1528
+ border: 2px solid #1976D2;
1529
+ border-radius: 5px;
1530
+ text-align: center;
1531
+ font-weight: bold;
1532
+ background-color: #E3F2FD;
1533
+ height: 25px;
1534
+ }
1535
+ QProgressBar::chunk {
1536
+ background-color: #2196F3;
1537
+ border-radius: 3px;
1538
+ }
1539
+ """)
1540
+
1541
+ progress.show()
1542
+
1543
+ try:
1544
+ offset = 0
1545
+ batch_size = 1000
1546
+
1547
+ while True:
1548
+ db_tus = self.db_manager.tmx_get_translation_units(
1549
+ tmx_file_id=self.tmx_file_id,
1550
+ offset=offset,
1551
+ limit=batch_size,
1552
+ src_lang=None, # Get all languages
1553
+ tgt_lang=None,
1554
+ src_filter=None,
1555
+ tgt_filter=None,
1556
+ ignore_case=True
1557
+ )
1558
+
1559
+ if not db_tus:
1560
+ break
1561
+
1562
+ # Convert to TmxTranslationUnit objects
1563
+ for db_tu in db_tus:
1564
+ tu = self._db_tu_to_tmx_tu(db_tu)
1565
+ tmx_file.add_translation_unit(tu)
1566
+ progress.setValue(len(tmx_file.translation_units))
1567
+ QApplication.processEvents()
1568
+
1569
+ if progress.wasCanceled():
1570
+ raise Exception("Export cancelled")
1571
+
1572
+ offset += batch_size
1573
+ if len(db_tus) < batch_size:
1574
+ break
1575
+
1576
+ finally:
1577
+ progress.close()
1578
+
1579
+ # Save to file
1580
+ tmx_file.header.change_date = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
1581
+ TmxParser.save_file(tmx_file, file_path)
1582
+
1583
+ # Update database file info
1584
+ self.db_manager.cursor.execute("""
1585
+ UPDATE tmx_files
1586
+ SET original_file_path = ?, last_modified = CURRENT_TIMESTAMP
1587
+ WHERE id = ?
1588
+ """, (file_path, self.tmx_file_id))
1589
+ self.db_manager.connection.commit()
1590
+
1591
+ self.set_status(f"Exported: {os.path.basename(file_path)} ({len(tmx_file.translation_units)} TUs)")
1592
+
1593
+ # ===== Edit Operations =====
1594
+
1595
+ def add_translation_unit(self):
1596
+ """Add new translation unit"""
1597
+ if not self.src_lang or not self.tgt_lang:
1598
+ QMessageBox.warning(self, "Warning", "Please select source and target languages")
1599
+ return
1600
+
1601
+ if self.load_mode == "database":
1602
+ if not self.db_manager or not self.tmx_file_id:
1603
+ QMessageBox.warning(self, "Warning", "Please create or open a TMX file first")
1604
+ return
1605
+
1606
+ # Get next TU ID (find max tu_id for this file)
1607
+ self.db_manager.cursor.execute("""
1608
+ SELECT MAX(tu_id) FROM tmx_translation_units
1609
+ WHERE tmx_file_id = ?
1610
+ """, (self.tmx_file_id,))
1611
+ result = self.db_manager.cursor.fetchone()
1612
+ new_id = (result[0] + 1) if result[0] else 1
1613
+
1614
+ # Get creation_id from file info
1615
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
1616
+ creation_id = file_info.get('header_data', {}).get('creation_id', '') if file_info else ''
1617
+
1618
+ # Create new TU in database
1619
+ tu_db_id = self.db_manager.tmx_store_translation_unit(
1620
+ tmx_file_id=self.tmx_file_id,
1621
+ tu_id=new_id,
1622
+ creation_id=creation_id,
1623
+ srclang=self.src_lang
1624
+ )
1625
+
1626
+ # Add segments
1627
+ self.db_manager.tmx_store_segment(tu_db_id, self.src_lang, "")
1628
+ self.db_manager.tmx_store_segment(tu_db_id, self.tgt_lang, "")
1629
+
1630
+ # Update file TU count
1631
+ self.db_manager.cursor.execute("""
1632
+ UPDATE tmx_files
1633
+ SET tu_count = tu_count + 1, last_modified = CURRENT_TIMESTAMP
1634
+ WHERE id = ?
1635
+ """, (self.tmx_file_id,))
1636
+ self.db_manager.connection.commit()
1637
+
1638
+ self.apply_filters() # Refresh view
1639
+ self.set_status(f"Added TU #{new_id}")
1640
+ return
1641
+
1642
+ # RAM mode
1643
+ if not self.tmx_file:
1644
+ QMessageBox.warning(self, "Warning", "Please create or open a TMX file first")
1645
+ return
1646
+
1647
+ # Create new TU
1648
+ new_id = self.tmx_file.get_tu_count() + 1
1649
+ tu = TmxTranslationUnit(tu_id=new_id,
1650
+ creation_id=self.tmx_file.header.creation_id)
1651
+ tu.set_segment(self.src_lang, "")
1652
+ tu.set_segment(self.tgt_lang, "")
1653
+
1654
+ self.tmx_file.add_translation_unit(tu)
1655
+ self.apply_filters() # Refresh view
1656
+ self.set_status(f"Added TU #{new_id}")
1657
+
1658
+ def delete_selected_tu(self):
1659
+ """Delete selected translation unit"""
1660
+ current_row = self.table.currentRow()
1661
+ if current_row < 0:
1662
+ QMessageBox.information(self, "Info", "Please select a translation unit to delete")
1663
+ return
1664
+
1665
+ if current_row not in self.tu_row_map:
1666
+ return
1667
+
1668
+ tu = self.tu_row_map[current_row]
1669
+
1670
+ reply = QMessageBox.question(
1671
+ self, "Confirm Delete",
1672
+ f"Delete translation unit #{tu.tu_id}?",
1673
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
1674
+ )
1675
+
1676
+ if reply == QMessageBox.StandardButton.Yes:
1677
+ if self.load_mode == "database":
1678
+ # Delete from database
1679
+ if self.db_manager and self.tmx_file_id:
1680
+ # Delete TU (segments will be deleted via CASCADE)
1681
+ self.db_manager.cursor.execute("""
1682
+ DELETE FROM tmx_translation_units
1683
+ WHERE tmx_file_id = ? AND tu_id = ?
1684
+ """, (self.tmx_file_id, tu.tu_id))
1685
+ self.db_manager.connection.commit()
1686
+
1687
+ # Update file TU count
1688
+ self.db_manager.cursor.execute("""
1689
+ UPDATE tmx_files
1690
+ SET tu_count = tu_count - 1, last_modified = CURRENT_TIMESTAMP
1691
+ WHERE id = ?
1692
+ """, (self.tmx_file_id,))
1693
+ self.db_manager.connection.commit()
1694
+ else:
1695
+ # Delete from RAM
1696
+ self.tmx_file.translation_units.remove(tu)
1697
+ self.tmx_file.is_modified = True
1698
+
1699
+ self.apply_filters()
1700
+ self.set_status(f"Deleted TU #{tu.tu_id}")
1701
+
1702
+ # ===== View Operations =====
1703
+
1704
+ def on_language_changed(self):
1705
+ """Handle language selection change"""
1706
+ self.src_lang = self.src_lang_combo.currentText()
1707
+ self.tgt_lang = self.tgt_lang_combo.currentText()
1708
+
1709
+ # Update filter labels to show current language codes
1710
+ if hasattr(self, 'src_search_label'):
1711
+ self.src_search_label.setText(f"Source: {self.src_lang}")
1712
+ if hasattr(self, 'tgt_search_label'):
1713
+ self.tgt_search_label.setText(f"Target: {self.tgt_lang}")
1714
+
1715
+ self.apply_filters()
1716
+
1717
+ def show_all_languages(self):
1718
+ """Show dialog with all languages in TMX"""
1719
+ languages = []
1720
+
1721
+ if self.load_mode == "database":
1722
+ if self.db_manager and self.tmx_file_id:
1723
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
1724
+ if file_info:
1725
+ languages = file_info.get('languages', [])
1726
+ else:
1727
+ if not self.tmx_file:
1728
+ return
1729
+ languages = self.tmx_file.get_languages()
1730
+
1731
+ if not languages:
1732
+ QMessageBox.information(self, "Info", "No languages found")
1733
+ return
1734
+
1735
+ dialog = QDialog(self)
1736
+ dialog.setWindowTitle("All Languages")
1737
+ dialog.resize(400, 400)
1738
+
1739
+ layout = QVBoxLayout(dialog)
1740
+
1741
+ title = QLabel("Languages in this TMX file:")
1742
+ title.setStyleSheet("font-size: 11pt; font-weight: bold;")
1743
+ layout.addWidget(title)
1744
+
1745
+ list_widget = QListWidget()
1746
+ for lang in languages:
1747
+ list_widget.addItem(lang)
1748
+ layout.addWidget(list_widget)
1749
+
1750
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
1751
+ buttons.rejected.connect(dialog.reject)
1752
+ layout.addWidget(buttons)
1753
+
1754
+ dialog.exec()
1755
+
1756
+ def edit_header(self):
1757
+ """Edit TMX header metadata"""
1758
+ # Get header data
1759
+ header_data = {}
1760
+
1761
+ if self.load_mode == "database":
1762
+ if not self.db_manager or not self.tmx_file_id:
1763
+ QMessageBox.warning(self, "Warning", "Please create or open a TMX file first")
1764
+ return
1765
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
1766
+ if file_info:
1767
+ header_data = file_info.get('header_data', {})
1768
+ else:
1769
+ if not self.tmx_file:
1770
+ QMessageBox.warning(self, "Warning", "Please create or open a TMX file first")
1771
+ return
1772
+ # Get header from RAM file
1773
+ header_data = {
1774
+ 'creation_tool': self.tmx_file.header.creation_tool,
1775
+ 'creation_tool_version': self.tmx_file.header.creation_tool_version,
1776
+ 'segtype': self.tmx_file.header.segtype,
1777
+ 'o_tmf': self.tmx_file.header.o_tmf,
1778
+ 'adminlang': self.tmx_file.header.adminlang,
1779
+ 'srclang': self.tmx_file.header.srclang,
1780
+ 'datatype': self.tmx_file.header.datatype,
1781
+ 'creation_date': self.tmx_file.header.creation_date,
1782
+ 'creation_id': self.tmx_file.header.creation_id,
1783
+ 'change_date': self.tmx_file.header.change_date,
1784
+ 'change_id': self.tmx_file.header.change_id,
1785
+ }
1786
+
1787
+ dialog = QDialog(self)
1788
+ dialog.setWindowTitle("TMX Header Metadata")
1789
+ dialog.resize(500, 500)
1790
+
1791
+ layout = QVBoxLayout(dialog)
1792
+
1793
+ title = QLabel("TMX Header Information")
1794
+ title.setStyleSheet("font-size: 12pt; font-weight: bold;")
1795
+ layout.addWidget(title)
1796
+
1797
+ form = QFormLayout()
1798
+ fields = {}
1799
+
1800
+ for field_name, field_label in [
1801
+ ('creation_tool', 'Creation Tool'),
1802
+ ('creation_tool_version', 'Tool Version'),
1803
+ ('segtype', 'Segment Type'),
1804
+ ('o_tmf', 'O-TMF'),
1805
+ ('adminlang', 'Admin Language'),
1806
+ ('srclang', 'Source Language'),
1807
+ ('datatype', 'Data Type'),
1808
+ ('creation_id', 'Creator ID'),
1809
+ ('change_id', 'Last Modified By')
1810
+ ]:
1811
+ entry = QLineEdit()
1812
+ entry.setText(str(header_data.get(field_name, '')))
1813
+ form.addRow(f"{field_label}:", entry)
1814
+ fields[field_name] = entry
1815
+
1816
+ # Read-only dates
1817
+ form.addRow("Creation Date:", QLabel(header_data.get('creation_date', '')))
1818
+ form.addRow("Last Modified:", QLabel(header_data.get('change_date', '')))
1819
+
1820
+ layout.addLayout(form)
1821
+
1822
+ buttons = QDialogButtonBox(
1823
+ QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
1824
+ )
1825
+
1826
+ def save_header():
1827
+ # Update header data
1828
+ updated_header = {}
1829
+ for field_name, entry in fields.items():
1830
+ updated_header[field_name] = entry.text()
1831
+
1832
+ updated_header['change_date'] = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
1833
+
1834
+ if self.load_mode == "database":
1835
+ # Update database
1836
+ header_json = json.dumps(updated_header)
1837
+ self.db_manager.cursor.execute("""
1838
+ UPDATE tmx_files
1839
+ SET header_data = ?, last_modified = CURRENT_TIMESTAMP
1840
+ WHERE id = ?
1841
+ """, (header_json, self.tmx_file_id))
1842
+ self.db_manager.connection.commit()
1843
+ else:
1844
+ # Update RAM file
1845
+ for field_name, value in updated_header.items():
1846
+ if hasattr(self.tmx_file.header, field_name):
1847
+ setattr(self.tmx_file.header, field_name, value)
1848
+ self.tmx_file.is_modified = True
1849
+
1850
+ dialog.accept()
1851
+ self.set_status("Header updated")
1852
+
1853
+ buttons.accepted.connect(save_header)
1854
+ buttons.rejected.connect(dialog.reject)
1855
+ layout.addWidget(buttons)
1856
+
1857
+ dialog.exec()
1858
+
1859
+ def show_statistics(self):
1860
+ """Show TMX file statistics"""
1861
+ if self.load_mode == "database":
1862
+ if not self.db_manager or not self.tmx_file_id:
1863
+ QMessageBox.warning(self, "Warning", "No file open")
1864
+ return
1865
+
1866
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
1867
+ if not file_info:
1868
+ QMessageBox.warning(self, "Warning", "File information not found")
1869
+ return
1870
+
1871
+ total_tus = file_info.get('tu_count', 0)
1872
+ languages = file_info.get('languages', [])
1873
+
1874
+ # Count segments per language (would need separate query for accurate counts)
1875
+ # For now, show basic stats
1876
+ stats = f"TMX File Statistics\n\n"
1877
+ stats += f"Total Translation Units: {total_tus}\n"
1878
+ stats += f"Languages: {len(languages)}\n"
1879
+ stats += f"Load Mode: Database\n"
1880
+ stats += f"File Size: {file_info.get('file_size', 0) / (1024*1024):.2f} MB\n\n"
1881
+ stats += "Languages:\n"
1882
+ for lang in sorted(languages):
1883
+ stats += f" - {lang}\n"
1884
+
1885
+ QMessageBox.information(self, "Statistics", stats)
1886
+ return
1887
+
1888
+ # RAM mode
1889
+ if not self.tmx_file:
1890
+ return
1891
+
1892
+ total_tus = self.tmx_file.get_tu_count()
1893
+ languages = self.tmx_file.get_languages()
1894
+
1895
+ # Count segments per language
1896
+ lang_counts = {lang: 0 for lang in languages}
1897
+ total_chars = {lang: 0 for lang in languages}
1898
+
1899
+ for tu in self.tmx_file.translation_units:
1900
+ for lang, segment in tu.segments.items():
1901
+ lang_counts[lang] += 1
1902
+ total_chars[lang] += len(segment.text)
1903
+
1904
+ # Build statistics message
1905
+ stats = f"TMX File Statistics\n\n"
1906
+ stats += f"Total Translation Units: {total_tus}\n"
1907
+ stats += f"Languages: {len(languages)}\n"
1908
+ stats += f"Load Mode: RAM\n\n"
1909
+ stats += "Segments per Language:\n"
1910
+
1911
+ for lang in sorted(languages):
1912
+ avg_chars = total_chars[lang] / lang_counts[lang] if lang_counts[lang] > 0 else 0
1913
+ stats += f" {lang}: {lang_counts[lang]} segments (avg {avg_chars:.1f} chars)\n"
1914
+
1915
+ QMessageBox.information(self, "Statistics", stats)
1916
+
1917
+ # ===== Filter Operations =====
1918
+
1919
+ def apply_filters(self):
1920
+ """Apply filters and refresh grid"""
1921
+ if self.load_mode == "database":
1922
+ # Database mode - filters are applied in refresh_current_page via query
1923
+ self.current_page = 0
1924
+ self.refresh_current_page()
1925
+ return
1926
+
1927
+ # RAM mode
1928
+ if not self.tmx_file:
1929
+ return
1930
+
1931
+ ignore_case = self.filter_ignore_case.isChecked()
1932
+
1933
+ # Get filter text (store original case for highlighting)
1934
+ source_filter_text = self.filter_source_entry.text()
1935
+ target_filter_text = self.filter_target_entry.text()
1936
+
1937
+ # Store filters (lowercase for comparison if ignore_case)
1938
+ self.filter_source = source_filter_text.lower() if ignore_case else source_filter_text
1939
+ self.filter_target = target_filter_text.lower() if ignore_case else target_filter_text
1940
+
1941
+ # Filter TUs
1942
+ self.filtered_tus = []
1943
+ for tu in self.tmx_file.translation_units:
1944
+ src_seg = tu.get_segment(self.src_lang)
1945
+ tgt_seg = tu.get_segment(self.tgt_lang)
1946
+
1947
+ src_text = src_seg.text if src_seg else ""
1948
+ tgt_text = tgt_seg.text if tgt_seg else ""
1949
+
1950
+ # Apply filters (case-insensitive if ignore_case is checked)
1951
+ if ignore_case:
1952
+ src_text = src_text.lower()
1953
+ tgt_text = tgt_text.lower()
1954
+
1955
+ if self.filter_source and self.filter_source not in src_text:
1956
+ continue
1957
+ if self.filter_target and self.filter_target not in tgt_text:
1958
+ continue
1959
+
1960
+ self.filtered_tus.append(tu)
1961
+
1962
+ self.current_page = 0
1963
+ self.refresh_current_page()
1964
+
1965
+ def clear_filters(self):
1966
+ """Clear all filters"""
1967
+ self.filter_source_entry.clear()
1968
+ self.filter_target_entry.clear()
1969
+ self.filter_source = ""
1970
+ self.filter_target = ""
1971
+ self.apply_filters()
1972
+
1973
+ # ===== Pagination =====
1974
+
1975
+ def _db_tu_to_tmx_tu(self, db_tu_data: Dict) -> TmxTranslationUnit:
1976
+ """Convert database TU data to TmxTranslationUnit object"""
1977
+ tu = TmxTranslationUnit(tu_id=db_tu_data['tu_id'])
1978
+ tu.creation_date = db_tu_data.get('creation_date', '')
1979
+ tu.creation_id = db_tu_data.get('creation_id', '')
1980
+ tu.change_date = db_tu_data.get('change_date', '')
1981
+ tu.change_id = db_tu_data.get('change_id', '')
1982
+ tu.srclang = db_tu_data.get('srclang', '')
1983
+
1984
+ # Add segments
1985
+ for lang, seg_data in db_tu_data.get('segments', {}).items():
1986
+ seg = TmxSegment(
1987
+ lang=lang,
1988
+ text=seg_data.get('text', ''),
1989
+ creation_date=seg_data.get('creation_date', ''),
1990
+ creation_id=seg_data.get('creation_id', ''),
1991
+ change_date=seg_data.get('change_date', ''),
1992
+ change_id=seg_data.get('change_id', '')
1993
+ )
1994
+ tu.segments[lang] = seg
1995
+
1996
+ return tu
1997
+
1998
+ def refresh_current_page(self):
1999
+ """Refresh current page in table"""
2000
+ # Update column headers with language codes
2001
+ self.table.setHorizontalHeaderLabels([
2002
+ 'No.',
2003
+ f'{self.src_lang}',
2004
+ f'{self.tgt_lang}',
2005
+ 'System Attributes'
2006
+ ])
2007
+
2008
+ if self.load_mode == "database":
2009
+ # Database mode - query database
2010
+ if not self.db_manager or not self.tmx_file_id:
2011
+ self.page_label.setText("No file open")
2012
+ self.table.setRowCount(0)
2013
+ self.tu_row_map.clear()
2014
+ self.clear_attributes_display()
2015
+ return
2016
+
2017
+ # Get filters
2018
+ ignore_case = self.filter_ignore_case.isChecked()
2019
+ source_filter = self.filter_source_entry.text() if self.filter_source_entry else ""
2020
+ target_filter = self.filter_target_entry.text() if self.filter_target_entry else ""
2021
+
2022
+ # Count total items
2023
+ total_items = self.db_manager.tmx_count_translation_units(
2024
+ tmx_file_id=self.tmx_file_id,
2025
+ src_lang=self.src_lang,
2026
+ tgt_lang=self.tgt_lang,
2027
+ src_filter=source_filter,
2028
+ tgt_filter=target_filter,
2029
+ ignore_case=ignore_case
2030
+ )
2031
+
2032
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page if total_items > 0 else 0
2033
+
2034
+ if total_pages == 0:
2035
+ self.page_label.setText("No items")
2036
+ self.table.setRowCount(0)
2037
+ self.tu_row_map.clear()
2038
+ self.clear_attributes_display()
2039
+ return
2040
+
2041
+ # Query current page
2042
+ offset = self.current_page * self.items_per_page
2043
+ db_tus = self.db_manager.tmx_get_translation_units(
2044
+ tmx_file_id=self.tmx_file_id,
2045
+ offset=offset,
2046
+ limit=self.items_per_page,
2047
+ src_lang=self.src_lang,
2048
+ tgt_lang=self.tgt_lang,
2049
+ src_filter=source_filter,
2050
+ tgt_filter=target_filter,
2051
+ ignore_case=ignore_case
2052
+ )
2053
+
2054
+ # Convert to TmxTranslationUnit objects for display
2055
+ tus = [self._db_tu_to_tmx_tu(db_tu) for db_tu in db_tus]
2056
+
2057
+ else:
2058
+ # RAM mode
2059
+ if not self.tmx_file:
2060
+ self.page_label.setText("No file open")
2061
+ self.table.setRowCount(0)
2062
+ self.tu_row_map.clear()
2063
+ self.clear_attributes_display()
2064
+ return
2065
+
2066
+ # Calculate page range
2067
+ total_items = len(self.filtered_tus)
2068
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page if total_items > 0 else 0
2069
+
2070
+ if total_pages == 0:
2071
+ self.page_label.setText("No items")
2072
+ self.table.setRowCount(0)
2073
+ self.tu_row_map.clear()
2074
+ self.clear_attributes_display()
2075
+ return
2076
+
2077
+ start_idx = self.current_page * self.items_per_page
2078
+ end_idx = min(start_idx + self.items_per_page, total_items)
2079
+ tus = self.filtered_tus[start_idx:end_idx]
2080
+
2081
+ # Set row count
2082
+ self.table.setRowCount(len(tus))
2083
+ self.tu_row_map.clear()
2084
+
2085
+ # Add items to table
2086
+ for i, tu in enumerate(tus):
2087
+ row = i
2088
+ src_seg = tu.get_segment(self.src_lang)
2089
+ tgt_seg = tu.get_segment(self.tgt_lang)
2090
+
2091
+ src_text = src_seg.text if src_seg else ""
2092
+ tgt_text = tgt_seg.text if tgt_seg else ""
2093
+
2094
+ # Clean up text for display (remove newlines)
2095
+ src_display = src_text.replace('\n', ' ').replace('\r', '')
2096
+ tgt_display = tgt_text.replace('\n', ' ').replace('\r', '')
2097
+
2098
+ # Create items
2099
+ no_item = QTableWidgetItem(str(tu.tu_id))
2100
+ no_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
2101
+ no_item.setFlags(no_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
2102
+
2103
+ # Source item - editable with original text stored
2104
+ src_item = QTableWidgetItem(src_display)
2105
+ src_item.setFlags(src_item.flags() | Qt.ItemFlag.ItemIsEditable) # Make editable
2106
+ src_item.setData(Qt.ItemDataRole.UserRole, src_text) # Store original text with newlines
2107
+
2108
+ # Target item - editable with original text stored
2109
+ tgt_item = QTableWidgetItem(tgt_display)
2110
+ tgt_item.setFlags(tgt_item.flags() | Qt.ItemFlag.ItemIsEditable) # Make editable
2111
+ tgt_item.setData(Qt.ItemDataRole.UserRole, tgt_text) # Store original text with newlines
2112
+
2113
+ # System Attributes column - show "N/A" for now
2114
+ attr_item = QTableWidgetItem("N/A")
2115
+ attr_item.setFlags(attr_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
2116
+ attr_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
2117
+
2118
+ self.table.setItem(row, 0, no_item)
2119
+ self.table.setItem(row, 1, src_item)
2120
+ self.table.setItem(row, 2, tgt_item)
2121
+ self.table.setItem(row, 3, attr_item)
2122
+
2123
+ # Store TU reference
2124
+ self.tu_row_map[row] = tu
2125
+
2126
+ # Update highlight delegates with current filter text
2127
+ ignore_case = self.filter_ignore_case.isChecked()
2128
+ source_filter = self.filter_source_entry.text() if self.filter_source_entry else ""
2129
+ target_filter = self.filter_target_entry.text() if self.filter_target_entry else ""
2130
+
2131
+ self.highlight_delegate_source.set_highlight(source_filter, ignore_case)
2132
+ self.highlight_delegate_target.set_highlight(target_filter, ignore_case)
2133
+
2134
+ # Trigger repaint to show highlighting
2135
+ self.table.viewport().update()
2136
+
2137
+ # Update page label
2138
+ self.page_label.setText(f"Page {self.current_page + 1} of {total_pages} ({total_items} TUs)")
2139
+
2140
+ def create_status_bar(self, parent_layout):
2141
+ """Create status bar - Heartsome style"""
2142
+ status_frame = QFrame()
2143
+ status_frame.setFrameStyle(QFrame.Shape.Box)
2144
+ status_frame.setStyleSheet("background-color: #e0e0e0; padding: 3px; border-top: 1px solid #ccc;")
2145
+ status_layout = QHBoxLayout(status_frame)
2146
+ status_layout.setContentsMargins(5, 2, 5, 2)
2147
+
2148
+ self.status_bar = QLabel("Ready")
2149
+ self.status_bar.setStyleSheet("font-size: 9pt;")
2150
+ status_layout.addWidget(self.status_bar)
2151
+
2152
+ status_layout.addStretch()
2153
+
2154
+ # Right side could show memory usage, language, date/time like Heartsome
2155
+ # For now just keep it simple
2156
+
2157
+ parent_layout.addWidget(status_frame)
2158
+
2159
+ def on_table_selection_changed(self):
2160
+ """Handle table selection change - update attributes"""
2161
+ current_row = self.table.currentRow()
2162
+ if current_row >= 0 and current_row in self.tu_row_map:
2163
+ tu = self.tu_row_map[current_row]
2164
+ self.update_attributes_display(tu)
2165
+ else:
2166
+ self.clear_attributes_display()
2167
+
2168
+ def on_table_double_clicked(self, item):
2169
+ """Handle double-click on table - edit inline"""
2170
+ # Inline editing is handled by QTableWidget edit triggers
2171
+ pass
2172
+
2173
+ def on_cell_edited(self, item: QTableWidgetItem):
2174
+ """Handle cell edit - save changes directly to TMX data"""
2175
+ row = item.row()
2176
+ col = item.column()
2177
+
2178
+ if row not in self.tu_row_map:
2179
+ return
2180
+
2181
+ tu = self.tu_row_map[row]
2182
+ new_text = item.text()
2183
+
2184
+ # Temporarily disconnect to avoid recursion
2185
+ self.table.itemChanged.disconnect(self.on_cell_edited)
2186
+
2187
+ try:
2188
+ if col == 1: # Source column
2189
+ if self.load_mode == "database":
2190
+ # Update database
2191
+ if self.db_manager and self.tmx_file_id:
2192
+ self.db_manager.tmx_update_segment(
2193
+ tmx_file_id=self.tmx_file_id,
2194
+ tu_id=tu.tu_id,
2195
+ lang=self.src_lang,
2196
+ text=new_text
2197
+ )
2198
+ else:
2199
+ # Update RAM
2200
+ tu.set_segment(self.src_lang, new_text)
2201
+ self.tmx_file.is_modified = True
2202
+
2203
+ # Update display (remove newlines for display)
2204
+ display_text = new_text.replace('\n', ' ').replace('\r', '')
2205
+ item.setText(display_text)
2206
+ # Store original text
2207
+ item.setData(Qt.ItemDataRole.UserRole, new_text)
2208
+
2209
+ elif col == 2: # Target column
2210
+ if self.load_mode == "database":
2211
+ # Update database
2212
+ if self.db_manager and self.tmx_file_id:
2213
+ self.db_manager.tmx_update_segment(
2214
+ tmx_file_id=self.tmx_file_id,
2215
+ tu_id=tu.tu_id,
2216
+ lang=self.tgt_lang,
2217
+ text=new_text
2218
+ )
2219
+ else:
2220
+ # Update RAM
2221
+ tu.set_segment(self.tgt_lang, new_text)
2222
+ self.tmx_file.is_modified = True
2223
+
2224
+ # Update display (remove newlines for display)
2225
+ display_text = new_text.replace('\n', ' ').replace('\r', '')
2226
+ item.setText(display_text)
2227
+ # Store original text
2228
+ item.setData(Qt.ItemDataRole.UserRole, new_text)
2229
+
2230
+ # Update the TU object in memory for display
2231
+ tu.set_segment(self.src_lang if col == 1 else self.tgt_lang, new_text)
2232
+
2233
+ # Update attributes display if this is the selected row
2234
+ if self.table.currentRow() == row:
2235
+ self.update_attributes_display(tu)
2236
+
2237
+ self.set_status(f"Updated TU #{tu.tu_id}")
2238
+
2239
+ finally:
2240
+ # Reconnect signal
2241
+ self.table.itemChanged.connect(self.on_cell_edited)
2242
+
2243
+ def show_context_menu(self, position):
2244
+ """Show context menu on right-click"""
2245
+ menu = QMenu(self)
2246
+
2247
+ edit_action = menu.addAction("Edit")
2248
+ edit_action.triggered.connect(self.edit_selected_tu)
2249
+
2250
+ menu.addSeparator()
2251
+
2252
+ refresh_action = menu.addAction("Refresh")
2253
+ refresh_action.triggered.connect(self.refresh_current_page)
2254
+
2255
+ menu.exec(self.table.mapToGlobal(position))
2256
+
2257
+ def edit_selected_tu(self):
2258
+ """Edit selected TU from context menu - just focus the cell for inline editing"""
2259
+ current_row = self.table.currentRow()
2260
+ if current_row >= 0:
2261
+ # Select the target cell and start editing
2262
+ self.table.edit(self.table.item(current_row, 2))
2263
+
2264
+ def first_page(self):
2265
+ """Go to first page"""
2266
+ self.current_page = 0
2267
+ self.refresh_current_page()
2268
+
2269
+ def prev_page(self):
2270
+ """Go to previous page"""
2271
+ if self.current_page > 0:
2272
+ self.current_page -= 1
2273
+ self.refresh_current_page()
2274
+
2275
+ def next_page(self):
2276
+ """Go to next page"""
2277
+ # Get total items count
2278
+ if self.load_mode == "database":
2279
+ if not self.db_manager or not self.tmx_file_id:
2280
+ return
2281
+ ignore_case = self.filter_ignore_case.isChecked()
2282
+ source_filter = self.filter_source_entry.text() if self.filter_source_entry else ""
2283
+ target_filter = self.filter_target_entry.text() if self.filter_target_entry else ""
2284
+ total_items = self.db_manager.tmx_count_translation_units(
2285
+ tmx_file_id=self.tmx_file_id,
2286
+ src_lang=self.src_lang,
2287
+ tgt_lang=self.tgt_lang,
2288
+ src_filter=source_filter,
2289
+ tgt_filter=target_filter,
2290
+ ignore_case=ignore_case
2291
+ )
2292
+ else:
2293
+ total_items = len(self.filtered_tus)
2294
+
2295
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page if total_items > 0 else 0
2296
+
2297
+ if self.current_page < total_pages - 1:
2298
+ self.current_page += 1
2299
+ self.refresh_current_page()
2300
+
2301
+ def last_page(self):
2302
+ """Go to last page"""
2303
+ # Get total items count
2304
+ if self.load_mode == "database":
2305
+ if not self.db_manager or not self.tmx_file_id:
2306
+ return
2307
+ ignore_case = self.filter_ignore_case.isChecked()
2308
+ source_filter = self.filter_source_entry.text() if self.filter_source_entry else ""
2309
+ target_filter = self.filter_target_entry.text() if self.filter_target_entry else ""
2310
+ total_items = self.db_manager.tmx_count_translation_units(
2311
+ tmx_file_id=self.tmx_file_id,
2312
+ src_lang=self.src_lang,
2313
+ tgt_lang=self.tgt_lang,
2314
+ src_filter=source_filter,
2315
+ tgt_filter=target_filter,
2316
+ ignore_case=ignore_case
2317
+ )
2318
+ else:
2319
+ total_items = len(self.filtered_tus)
2320
+
2321
+ total_pages = (total_items + self.items_per_page - 1) // self.items_per_page if total_items > 0 else 0
2322
+
2323
+ if total_pages > 0:
2324
+ self.current_page = total_pages - 1
2325
+ self.refresh_current_page()
2326
+
2327
+ # ===== Tools =====
2328
+
2329
+ def validate_tmx(self):
2330
+ """Validate TMX file structure"""
2331
+ if self.load_mode == "database":
2332
+ # For database mode, validation is less critical (data is already structured)
2333
+ QMessageBox.information(self, "Validation",
2334
+ "Database-backed TMX files are automatically validated during import.\n"
2335
+ "✓ Structure is valid (stored in normalized database format)")
2336
+ return
2337
+
2338
+ # RAM mode validation
2339
+ if not self.tmx_file:
2340
+ QMessageBox.warning(self, "Warning", "Please create or open a TMX file first")
2341
+ return
2342
+
2343
+ issues = []
2344
+
2345
+ # Check header
2346
+ if not self.tmx_file.header.srclang:
2347
+ issues.append("Missing source language in header")
2348
+
2349
+ # Check translation units
2350
+ for tu in self.tmx_file.translation_units:
2351
+ if not tu.segments:
2352
+ issues.append(f"TU #{tu.tu_id}: No segments")
2353
+ continue
2354
+
2355
+ # Check for empty segments
2356
+ for lang, seg in tu.segments.items():
2357
+ if not seg.text.strip():
2358
+ issues.append(f"TU #{tu.tu_id}: Empty segment for {lang}")
2359
+
2360
+ if issues:
2361
+ issues_text = "\n".join(issues[:20]) # Show first 20 issues
2362
+ if len(issues) > 20:
2363
+ issues_text += f"\n... and {len(issues) - 20} more issues"
2364
+
2365
+ QMessageBox.warning(self, "Validation Issues",
2366
+ f"Found {len(issues)} issue(s):\n\n{issues_text}")
2367
+ else:
2368
+ QMessageBox.information(self, "Validation", "✓ No issues found. TMX file is valid!")
2369
+
2370
+ def show_clean_tags_dialog(self):
2371
+ """Show the tag cleaning configuration dialog"""
2372
+ import re
2373
+
2374
+ # Get TU count
2375
+ tu_count = 0
2376
+ if self.load_mode == "database":
2377
+ if self.db_manager and self.tmx_file_id:
2378
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
2379
+ tu_count = file_info.get('tu_count', 0) if file_info else 0
2380
+ else:
2381
+ if not self.tmx_file:
2382
+ QMessageBox.warning(self, "Warning", "Please create or open a TMX file first")
2383
+ return
2384
+ tu_count = len(self.tmx_file.translation_units)
2385
+
2386
+ if tu_count == 0:
2387
+ QMessageBox.warning(self, "Warning", "No translation units to clean")
2388
+ return
2389
+
2390
+ # Show dialog
2391
+ dialog = TmxTagCleanerDialog(self, tu_count)
2392
+ if dialog.exec() == QDialog.DialogCode.Accepted:
2393
+ patterns = dialog.get_selected_patterns()
2394
+ if not patterns:
2395
+ QMessageBox.information(self, "Clean Tags", "No tag patterns selected.")
2396
+ return
2397
+
2398
+ replacement = dialog.get_replacement()
2399
+ scope = dialog.get_scope()
2400
+
2401
+ # Confirm action
2402
+ reply = QMessageBox.question(
2403
+ self, "Confirm Tag Cleaning",
2404
+ f"This will clean tags from {tu_count:,} translation units.\n\n"
2405
+ f"Scope: {scope}\n"
2406
+ f"Patterns: {len(patterns)} selected\n"
2407
+ f"Replacement: {'[space]' if replacement else '[nothing]'}\n\n"
2408
+ "This action cannot be undone. Continue?",
2409
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
2410
+ )
2411
+
2412
+ if reply == QMessageBox.StandardButton.Yes:
2413
+ self.clean_tags(patterns, replacement, scope)
2414
+
2415
+ def clean_tags(self, patterns: List[str], replacement: str, scope: str):
2416
+ """
2417
+ Clean tags from TMX segments
2418
+
2419
+ Args:
2420
+ patterns: List of regex patterns to match tags
2421
+ replacement: String to replace tags with (empty or space)
2422
+ scope: 'both', 'source', or 'target'
2423
+ """
2424
+ import re
2425
+ import html
2426
+
2427
+ # Create patterns for both literal and XML-escaped versions
2428
+ # TMX files may store tags as literal <b> or escaped &lt;b&gt;
2429
+ expanded_patterns = []
2430
+ for p in patterns:
2431
+ expanded_patterns.append(p) # Original pattern for literal tags
2432
+ # Create escaped version: < becomes &lt; and > becomes &gt;
2433
+ escaped_p = p.replace('<', '&lt;').replace('>', '&gt;')
2434
+ if escaped_p != p:
2435
+ expanded_patterns.append(escaped_p)
2436
+
2437
+ # Combine all patterns into one regex for efficiency
2438
+ combined_pattern = "|".join(f"({p})" for p in expanded_patterns)
2439
+ regex = re.compile(combined_pattern)
2440
+
2441
+ # Progress dialog
2442
+ progress = QProgressDialog("Cleaning tags...", "Cancel", 0, 100, self)
2443
+ progress.setWindowModality(Qt.WindowModality.WindowModal)
2444
+ progress.setMinimumDuration(500)
2445
+
2446
+ cleaned_count = 0
2447
+ total_tags_removed = 0
2448
+
2449
+ def clean_text(text: str) -> tuple:
2450
+ """Clean text and return (cleaned_text, tags_removed_count)"""
2451
+ if not text:
2452
+ return text, 0
2453
+ tag_count = len(regex.findall(text)) # Count before replacement
2454
+ cleaned = regex.sub(replacement, text)
2455
+ # Clean up double spaces if replacement is space
2456
+ if replacement == " ":
2457
+ cleaned = re.sub(r' {2,}', ' ', cleaned).strip()
2458
+ return cleaned, tag_count
2459
+
2460
+ if self.load_mode == "database":
2461
+ # Database mode
2462
+ if not self.db_manager or not self.tmx_file_id:
2463
+ return
2464
+
2465
+ # Get all TUs from database
2466
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
2467
+ if not file_info:
2468
+ return
2469
+
2470
+ total_tus = file_info.get('tu_count', 0)
2471
+ all_languages = file_info.get('languages', [])
2472
+ header_srclang = file_info.get('srclang', all_languages[0] if all_languages else None)
2473
+
2474
+ # Determine which languages to clean based on scope
2475
+ langs_to_clean = []
2476
+ if scope == 'both':
2477
+ langs_to_clean = all_languages
2478
+ elif scope == 'source':
2479
+ langs_to_clean = [header_srclang] if header_srclang else []
2480
+ elif scope == 'target':
2481
+ langs_to_clean = [l for l in all_languages if l != header_srclang]
2482
+
2483
+ # Get TUs in batches - get all segments for each TU
2484
+ batch_size = 100
2485
+ for offset in range(0, total_tus, batch_size):
2486
+ if progress.wasCanceled():
2487
+ break
2488
+
2489
+ # Get batch of TUs with all languages
2490
+ tus = self.db_manager.tmx_get_translation_units(
2491
+ self.tmx_file_id,
2492
+ source_lang=self.src_lang,
2493
+ target_lang=self.tgt_lang,
2494
+ limit=batch_size,
2495
+ offset=offset
2496
+ )
2497
+
2498
+ for tu in tus:
2499
+ modified = False
2500
+
2501
+ # Clean all applicable languages
2502
+ for lang in langs_to_clean:
2503
+ seg = tu.get_segment(lang)
2504
+ if seg and seg.text:
2505
+ cleaned, count = clean_text(seg.text)
2506
+ if count > 0:
2507
+ # Update in database
2508
+ self.db_manager.tmx_update_segment(
2509
+ self.tmx_file_id, tu.tu_id, lang, cleaned
2510
+ )
2511
+ total_tags_removed += count
2512
+ modified = True
2513
+
2514
+ if modified:
2515
+ cleaned_count += 1
2516
+
2517
+ progress.setValue(int((offset + batch_size) / total_tus * 100))
2518
+ QApplication.processEvents()
2519
+
2520
+ else:
2521
+ # RAM mode
2522
+ if not self.tmx_file:
2523
+ return
2524
+
2525
+ total_tus = len(self.tmx_file.translation_units)
2526
+
2527
+ for i, tu in enumerate(self.tmx_file.translation_units):
2528
+ if progress.wasCanceled():
2529
+ break
2530
+
2531
+ modified = False
2532
+
2533
+ # Clean ALL segments in the TU, not just displayed languages
2534
+ # "both" = all languages, "source" = srclang from header, "target" = all non-source
2535
+ header_srclang = self.tmx_file.header.srclang if self.tmx_file.header else None
2536
+
2537
+ for lang, segment in tu.segments.items():
2538
+ should_clean = False
2539
+
2540
+ if scope == 'both':
2541
+ should_clean = True
2542
+ elif scope == 'source' and lang == header_srclang:
2543
+ should_clean = True
2544
+ elif scope == 'target' and lang != header_srclang:
2545
+ should_clean = True
2546
+
2547
+ if should_clean and segment.text:
2548
+ cleaned, count = clean_text(segment.text)
2549
+ if count > 0:
2550
+ segment.text = cleaned
2551
+ total_tags_removed += count
2552
+ modified = True
2553
+
2554
+ if modified:
2555
+ cleaned_count += 1
2556
+ self.tmx_file.is_modified = True
2557
+
2558
+ if i % 100 == 0:
2559
+ progress.setValue(int(i / total_tus * 100))
2560
+ QApplication.processEvents()
2561
+
2562
+ progress.setValue(100)
2563
+ progress.close()
2564
+
2565
+ # Refresh the grid
2566
+ self.apply_filters()
2567
+
2568
+ # Show results
2569
+ QMessageBox.information(
2570
+ self, "Tag Cleaning Complete",
2571
+ f"✅ Cleaning complete!\n\n"
2572
+ f"Translation units modified: {cleaned_count:,}\n"
2573
+ f"Total tags removed: {total_tags_removed:,}\n\n"
2574
+ f"{'Remember to save your changes!' if self.load_mode == 'ram' else 'Changes saved to database.'}"
2575
+ )
2576
+
2577
+ # ===== UI Helpers =====
2578
+
2579
+ def refresh_ui(self):
2580
+ """Refresh entire UI after loading file"""
2581
+ languages = []
2582
+
2583
+ if self.load_mode == "database":
2584
+ # Database mode - get languages from database
2585
+ if self.db_manager and self.tmx_file_id:
2586
+ file_info = self.db_manager.tmx_get_file_info(self.tmx_file_id)
2587
+ if file_info:
2588
+ languages = file_info.get('languages', [])
2589
+ else:
2590
+ # RAM mode
2591
+ if not self.tmx_file:
2592
+ return
2593
+ languages = self.tmx_file.get_languages()
2594
+
2595
+ if not languages:
2596
+ return
2597
+
2598
+ # Update language combos - block signals to prevent on_language_changed firing during setup
2599
+ self.src_lang_combo.blockSignals(True)
2600
+ self.tgt_lang_combo.blockSignals(True)
2601
+
2602
+ self.src_lang_combo.clear()
2603
+ self.src_lang_combo.addItems(languages)
2604
+
2605
+ self.tgt_lang_combo.clear()
2606
+ self.tgt_lang_combo.addItems(languages)
2607
+
2608
+ if self.src_lang in languages:
2609
+ self.src_lang_combo.setCurrentText(self.src_lang)
2610
+ elif languages:
2611
+ self.src_lang_combo.setCurrentIndex(0)
2612
+ self.src_lang = languages[0]
2613
+
2614
+ # Ensure target is different from source if possible
2615
+ if self.tgt_lang in languages and self.tgt_lang != self.src_lang:
2616
+ self.tgt_lang_combo.setCurrentText(self.tgt_lang)
2617
+ elif len(languages) > 1:
2618
+ # Pick first language that's different from source
2619
+ for lang in languages:
2620
+ if lang != self.src_lang:
2621
+ self.tgt_lang_combo.setCurrentText(lang)
2622
+ self.tgt_lang = lang
2623
+ break
2624
+ elif languages:
2625
+ self.tgt_lang_combo.setCurrentIndex(0)
2626
+ self.tgt_lang = languages[0]
2627
+
2628
+ self.src_lang_combo.blockSignals(False)
2629
+ self.tgt_lang_combo.blockSignals(False)
2630
+
2631
+ # Update filter labels
2632
+ if hasattr(self, 'src_search_label'):
2633
+ self.src_search_label.setText(f"Source: {self.src_lang}")
2634
+ if hasattr(self, 'tgt_search_label'):
2635
+ self.tgt_search_label.setText(f"Target: {self.tgt_lang}")
2636
+
2637
+ # Apply filters (will refresh grid)
2638
+ self.apply_filters()
2639
+
2640
+ def set_status(self, message: str):
2641
+ """Set status bar message"""
2642
+ self.status_bar.setText(message)
2643
+
2644
+
2645
+ # === Standalone Application ===
2646
+
2647
+ if __name__ == "__main__":
2648
+ """Run TMX Editor as a standalone application"""
2649
+ import sys
2650
+ from pathlib import Path
2651
+
2652
+ # Determine database path (dev vs regular mode)
2653
+ import os
2654
+ ENABLE_PRIVATE_FEATURES = os.path.exists(
2655
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".supervertaler.local")
2656
+ )
2657
+ user_data_path = Path("user_data_private" if ENABLE_PRIVATE_FEATURES else "user_data")
2658
+ db_path = user_data_path / "resources" / "supervertaler.db"
2659
+
2660
+ # Ensure database directory exists
2661
+ db_path.parent.mkdir(parents=True, exist_ok=True)
2662
+
2663
+ # Create QApplication
2664
+ app = QApplication(sys.argv)
2665
+ app.setApplicationName("TMX Editor")
2666
+ app.setOrganizationName("Supervertaler")
2667
+
2668
+ # Create database manager
2669
+ from modules.database_manager import DatabaseManager
2670
+
2671
+ def log_callback(message: str):
2672
+ """Simple log callback for standalone mode"""
2673
+ print(f"[TMX Editor] {message}")
2674
+
2675
+ db_manager = DatabaseManager(
2676
+ db_path=str(db_path),
2677
+ log_callback=log_callback
2678
+ )
2679
+ db_manager.connect()
2680
+
2681
+ # Create main window
2682
+ from PyQt6.QtWidgets import QMainWindow
2683
+
2684
+ class StandaloneWindow(QMainWindow):
2685
+ """Standalone window for TMX Editor"""
2686
+ def __init__(self):
2687
+ super().__init__()
2688
+ self.setWindowTitle("TMX Editor - Professional Translation Memory Editor")
2689
+ self.setGeometry(100, 100, 1400, 900)
2690
+
2691
+ # Create TMX Editor widget with database manager
2692
+ self.tmx_editor = TmxEditorUIQt(parent=self, standalone=True, db_manager=db_manager)
2693
+ self.setCentralWidget(self.tmx_editor)
2694
+
2695
+ # Create menu bar
2696
+ menubar = self.menuBar()
2697
+
2698
+ # File menu
2699
+ file_menu = menubar.addMenu("File")
2700
+ open_action = file_menu.addAction("Open TMX...")
2701
+ open_action.setShortcut(QKeySequence.StandardKey.Open)
2702
+ open_action.triggered.connect(self.tmx_editor.open_tmx)
2703
+
2704
+ save_action = file_menu.addAction("Save")
2705
+ save_action.setShortcut(QKeySequence.StandardKey.Save)
2706
+ save_action.triggered.connect(self.tmx_editor.save_tmx)
2707
+
2708
+ save_as_action = file_menu.addAction("Save As...")
2709
+ save_as_action.setShortcut(QKeySequence.StandardKey.SaveAs)
2710
+ save_as_action.triggered.connect(self.tmx_editor.save_tmx_as)
2711
+
2712
+ file_menu.addSeparator()
2713
+
2714
+ close_action = file_menu.addAction("Close")
2715
+ close_action.triggered.connect(self.tmx_editor.close_tmx)
2716
+
2717
+ file_menu.addSeparator()
2718
+
2719
+ exit_action = file_menu.addAction("Exit")
2720
+ exit_action.setShortcut(QKeySequence.StandardKey.Quit)
2721
+ exit_action.triggered.connect(self.close)
2722
+
2723
+ # Edit menu
2724
+ edit_menu = menubar.addMenu("Edit")
2725
+ add_tu_action = edit_menu.addAction("Add Translation Unit")
2726
+ add_tu_action.triggered.connect(self.tmx_editor.add_translation_unit)
2727
+
2728
+ delete_tu_action = edit_menu.addAction("Delete Selected TU")
2729
+ delete_tu_action.triggered.connect(self.tmx_editor.delete_selected_tu)
2730
+
2731
+ edit_menu.addSeparator()
2732
+
2733
+ # Bulk Operations submenu
2734
+ bulk_menu = edit_menu.addMenu("Bulk Operations")
2735
+ clean_tags_action = bulk_menu.addAction("🧹 Clean Tags...")
2736
+ clean_tags_action.triggered.connect(self.tmx_editor.show_clean_tags_dialog)
2737
+
2738
+ # View menu
2739
+ view_menu = menubar.addMenu("View")
2740
+ stats_action = view_menu.addAction("Statistics")
2741
+ stats_action.triggered.connect(self.tmx_editor.show_statistics)
2742
+ langs_action = view_menu.addAction("All Languages")
2743
+ langs_action.triggered.connect(self.tmx_editor.show_all_languages)
2744
+ header_action = view_menu.addAction("Header Metadata")
2745
+ header_action.triggered.connect(self.tmx_editor.edit_header)
2746
+
2747
+ # Tools menu
2748
+ tools_menu = menubar.addMenu("Tools")
2749
+ validate_action = tools_menu.addAction("Validate TMX")
2750
+ validate_action.triggered.connect(self.tmx_editor.validate_tmx)
2751
+
2752
+ clean_tags_tool_action = tools_menu.addAction("🧹 Clean Tags...")
2753
+ clean_tags_tool_action.triggered.connect(self.tmx_editor.show_clean_tags_dialog)
2754
+
2755
+ def closeEvent(self, event):
2756
+ """Handle window close - check for unsaved changes"""
2757
+ if self.tmx_editor.load_mode == "ram" and self.tmx_editor.tmx_file and self.tmx_editor.tmx_file.is_modified:
2758
+ reply = QMessageBox.question(
2759
+ self, "Unsaved Changes",
2760
+ "Current file has unsaved changes. Close without saving?",
2761
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel
2762
+ )
2763
+ if reply == QMessageBox.StandardButton.Cancel:
2764
+ event.ignore()
2765
+ return
2766
+ elif reply == QMessageBox.StandardButton.No:
2767
+ self.tmx_editor.save_tmx()
2768
+ if self.tmx_editor.tmx_file and self.tmx_editor.tmx_file.is_modified:
2769
+ event.ignore()
2770
+ return
2771
+
2772
+ # Close database connection
2773
+ if db_manager:
2774
+ db_manager.close()
2775
+
2776
+ event.accept()
2777
+
2778
+ # Create and show window
2779
+ window = StandaloneWindow()
2780
+ window.show()
2781
+
2782
+ # Run application
2783
+ sys.exit(app.exec())
2784
+