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.
- Supervertaler.py +47886 -0
- modules/__init__.py +10 -0
- modules/ai_actions.py +964 -0
- modules/ai_attachment_manager.py +343 -0
- modules/ai_file_viewer_dialog.py +210 -0
- modules/autofingers_engine.py +466 -0
- modules/cafetran_docx_handler.py +379 -0
- modules/config_manager.py +469 -0
- modules/database_manager.py +1878 -0
- modules/database_migrations.py +417 -0
- modules/dejavurtf_handler.py +779 -0
- modules/document_analyzer.py +427 -0
- modules/docx_handler.py +689 -0
- modules/encoding_repair.py +319 -0
- modules/encoding_repair_Qt.py +393 -0
- modules/encoding_repair_ui.py +481 -0
- modules/feature_manager.py +350 -0
- modules/figure_context_manager.py +340 -0
- modules/file_dialog_helper.py +148 -0
- modules/find_replace.py +164 -0
- modules/find_replace_qt.py +457 -0
- modules/glossary_manager.py +433 -0
- modules/image_extractor.py +188 -0
- modules/keyboard_shortcuts_widget.py +571 -0
- modules/llm_clients.py +1211 -0
- modules/llm_leaderboard.py +737 -0
- modules/llm_superbench_ui.py +1401 -0
- modules/local_llm_setup.py +1104 -0
- modules/model_update_dialog.py +381 -0
- modules/model_version_checker.py +373 -0
- modules/mqxliff_handler.py +638 -0
- modules/non_translatables_manager.py +743 -0
- modules/pdf_rescue_Qt.py +1822 -0
- modules/pdf_rescue_tkinter.py +909 -0
- modules/phrase_docx_handler.py +516 -0
- modules/project_home_panel.py +209 -0
- modules/prompt_assistant.py +357 -0
- modules/prompt_library.py +689 -0
- modules/prompt_library_migration.py +447 -0
- modules/quick_access_sidebar.py +282 -0
- modules/ribbon_widget.py +597 -0
- modules/sdlppx_handler.py +874 -0
- modules/setup_wizard.py +353 -0
- modules/shortcut_manager.py +932 -0
- modules/simple_segmenter.py +128 -0
- modules/spellcheck_manager.py +727 -0
- modules/statuses.py +207 -0
- modules/style_guide_manager.py +315 -0
- modules/superbench_ui.py +1319 -0
- modules/superbrowser.py +329 -0
- modules/supercleaner.py +600 -0
- modules/supercleaner_ui.py +444 -0
- modules/superdocs.py +19 -0
- modules/superdocs_viewer_qt.py +382 -0
- modules/superlookup.py +252 -0
- modules/tag_cleaner.py +260 -0
- modules/tag_manager.py +333 -0
- modules/term_extractor.py +270 -0
- modules/termbase_entry_editor.py +842 -0
- modules/termbase_import_export.py +488 -0
- modules/termbase_manager.py +1060 -0
- modules/termview_widget.py +1172 -0
- modules/theme_manager.py +499 -0
- modules/tm_editor_dialog.py +99 -0
- modules/tm_manager_qt.py +1280 -0
- modules/tm_metadata_manager.py +545 -0
- modules/tmx_editor.py +1461 -0
- modules/tmx_editor_qt.py +2784 -0
- modules/tmx_generator.py +284 -0
- modules/tracked_changes.py +900 -0
- modules/trados_docx_handler.py +430 -0
- modules/translation_memory.py +715 -0
- modules/translation_results_panel.py +2134 -0
- modules/translation_services.py +282 -0
- modules/unified_prompt_library.py +659 -0
- modules/unified_prompt_manager_qt.py +3951 -0
- modules/voice_commands.py +920 -0
- modules/voice_dictation.py +477 -0
- modules/voice_dictation_lite.py +249 -0
- supervertaler-1.9.153.dist-info/METADATA +896 -0
- supervertaler-1.9.153.dist-info/RECORD +85 -0
- supervertaler-1.9.153.dist-info/WHEEL +5 -0
- supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
- supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
- supervertaler-1.9.153.dist-info/top_level.txt +2 -0
modules/tmx_editor_qt.py
ADDED
|
@@ -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 <b>
|
|
2429
|
+
expanded_patterns = []
|
|
2430
|
+
for p in patterns:
|
|
2431
|
+
expanded_patterns.append(p) # Original pattern for literal tags
|
|
2432
|
+
# Create escaped version: < becomes < and > becomes >
|
|
2433
|
+
escaped_p = p.replace('<', '<').replace('>', '>')
|
|
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
|
+
|