supervertaler 1.9.153__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (85) hide show
  1. Supervertaler.py +47886 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1878 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +333 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1172 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.153.dist-info/METADATA +896 -0
  81. supervertaler-1.9.153.dist-info/RECORD +85 -0
  82. supervertaler-1.9.153.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.153.dist-info/top_level.txt +2 -0
@@ -0,0 +1,457 @@
1
+ """
2
+ Find & Replace Module for Supervertaler (PyQt6)
3
+
4
+ This module provides enhanced Find & Replace functionality including:
5
+ - History dropdowns for recent search/replace terms
6
+ - Saveable F&R Sets for batch operations
7
+ - Import/Export of .svfr files
8
+
9
+ Classes:
10
+ - FindReplaceHistory: Manages and persists recent search/replace terms
11
+ - FindReplaceOperation: Single F&R operation with all settings
12
+ - FindReplaceSet: Collection of F&R operations
13
+ - FindReplaceSetsManager: UI widget for managing F&R sets
14
+ - HistoryComboBox: Editable combo box with history dropdown
15
+
16
+ Author: Michael Beijer
17
+ License: MIT
18
+ """
19
+
20
+ import json
21
+ import os
22
+ from dataclasses import dataclass, field, asdict
23
+ from typing import List, Optional, Callable
24
+ from pathlib import Path
25
+
26
+ from PyQt6.QtWidgets import (
27
+ QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
28
+ QPushButton, QComboBox, QLineEdit, QHeaderView, QAbstractItemView,
29
+ QMessageBox, QFileDialog, QInputDialog, QSplitter, QLabel, QCheckBox
30
+ )
31
+ from PyQt6.QtCore import Qt, pyqtSignal
32
+ from PyQt6.QtGui import QColor
33
+
34
+
35
+ class FindReplaceHistory:
36
+ """Manages and persists recent find/replace terms."""
37
+
38
+ MAX_HISTORY = 20
39
+
40
+ def __init__(self, user_data_path: str):
41
+ self.user_data_path = Path(user_data_path)
42
+ self.history_file = self.user_data_path / "find_replace_history.json"
43
+ self.find_history: List[str] = []
44
+ self.replace_history: List[str] = []
45
+ self._load()
46
+
47
+ def _load(self):
48
+ """Load history from file."""
49
+ if self.history_file.exists():
50
+ try:
51
+ with open(self.history_file, 'r', encoding='utf-8') as f:
52
+ data = json.load(f)
53
+ self.find_history = data.get('find', [])[:self.MAX_HISTORY]
54
+ self.replace_history = data.get('replace', [])[:self.MAX_HISTORY]
55
+ except Exception:
56
+ pass
57
+
58
+ def _save(self):
59
+ """Save history to file."""
60
+ try:
61
+ self.user_data_path.mkdir(parents=True, exist_ok=True)
62
+ with open(self.history_file, 'w', encoding='utf-8') as f:
63
+ json.dump({
64
+ 'find': self.find_history[:self.MAX_HISTORY],
65
+ 'replace': self.replace_history[:self.MAX_HISTORY]
66
+ }, f, ensure_ascii=False, indent=2)
67
+ except Exception:
68
+ pass
69
+
70
+ def add_find(self, text: str):
71
+ """Add a find term to history."""
72
+ if not text or not text.strip():
73
+ return
74
+ text = text.strip()
75
+ # Remove if exists, add to front
76
+ if text in self.find_history:
77
+ self.find_history.remove(text)
78
+ self.find_history.insert(0, text)
79
+ self.find_history = self.find_history[:self.MAX_HISTORY]
80
+ self._save()
81
+
82
+ def add_replace(self, text: str):
83
+ """Add a replace term to history."""
84
+ if text is None:
85
+ return
86
+ text = text.strip() if text else ""
87
+ # Remove if exists, add to front
88
+ if text in self.replace_history:
89
+ self.replace_history.remove(text)
90
+ self.replace_history.insert(0, text)
91
+ self.replace_history = self.replace_history[:self.MAX_HISTORY]
92
+ self._save()
93
+
94
+ def add_operation(self, find_text: str, replace_text: str):
95
+ """Add both find and replace terms."""
96
+ self.add_find(find_text)
97
+ self.add_replace(replace_text)
98
+
99
+
100
+ @dataclass
101
+ class FindReplaceOperation:
102
+ """Single find/replace operation with all settings."""
103
+ find_text: str
104
+ replace_text: str = ""
105
+ search_in: str = "target" # "source", "target", "both"
106
+ match_mode: int = 0 # 0=anything, 1=whole words, 2=entire segment
107
+ case_sensitive: bool = False
108
+ enabled: bool = True
109
+
110
+ def to_dict(self) -> dict:
111
+ return asdict(self)
112
+
113
+ @classmethod
114
+ def from_dict(cls, data: dict) -> 'FindReplaceOperation':
115
+ return cls(**data)
116
+
117
+
118
+ @dataclass
119
+ class FindReplaceSet:
120
+ """Collection of F&R operations that can be saved/loaded."""
121
+ name: str
122
+ operations: List[FindReplaceOperation] = field(default_factory=list)
123
+
124
+ def to_dict(self) -> dict:
125
+ return {
126
+ 'name': self.name,
127
+ 'operations': [op.to_dict() for op in self.operations]
128
+ }
129
+
130
+ @classmethod
131
+ def from_dict(cls, data: dict) -> 'FindReplaceSet':
132
+ ops = [FindReplaceOperation.from_dict(op) for op in data.get('operations', [])]
133
+ return cls(name=data.get('name', 'Unnamed Set'), operations=ops)
134
+
135
+ def add_operation(self, op: FindReplaceOperation):
136
+ self.operations.append(op)
137
+
138
+ def remove_operation(self, index: int):
139
+ if 0 <= index < len(self.operations):
140
+ del self.operations[index]
141
+
142
+
143
+ class HistoryComboBox(QComboBox):
144
+ """Editable combo box with history dropdown."""
145
+
146
+ def __init__(self, parent=None):
147
+ super().__init__(parent)
148
+ self.setEditable(True)
149
+ self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
150
+ self.setMaxVisibleItems(15)
151
+ self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
152
+ self.setMinimumWidth(200)
153
+
154
+ def set_history(self, history: List[str]):
155
+ """Update the dropdown items with history."""
156
+ current_text = self.currentText()
157
+ self.clear()
158
+ self.addItems(history)
159
+ self.setCurrentText(current_text)
160
+
161
+ def text(self) -> str:
162
+ """Get current text (convenience method)."""
163
+ return self.currentText()
164
+
165
+ def setText(self, text: str):
166
+ """Set current text (convenience method)."""
167
+ self.setCurrentText(text)
168
+
169
+
170
+ class FindReplaceSetsManager(QWidget):
171
+ """UI widget for managing F&R sets."""
172
+
173
+ # Signals
174
+ operation_selected = pyqtSignal(object) # Emits FindReplaceOperation
175
+ run_set_requested = pyqtSignal(object) # Emits FindReplaceSet
176
+ set_selected = pyqtSignal(object) # Emits FindReplaceSet for batch run
177
+
178
+ def __init__(self, user_data_path: str, parent=None):
179
+ super().__init__(parent)
180
+ self.user_data_path = Path(user_data_path)
181
+ self.sets_dir = self.user_data_path / "find_replace_sets"
182
+ self.sets_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ self.sets: List[FindReplaceSet] = []
185
+ self.current_set: Optional[FindReplaceSet] = None
186
+
187
+ self._setup_ui()
188
+ self._load_sets()
189
+ self._refresh_sets_table()
190
+
191
+ def _setup_ui(self):
192
+ """Set up the UI layout."""
193
+ layout = QVBoxLayout(self)
194
+ layout.setContentsMargins(0, 5, 0, 0)
195
+
196
+ # Horizontal splitter for sets list and operations
197
+ splitter = QSplitter(Qt.Orientation.Horizontal)
198
+
199
+ # Left side: Saved F&R Sets
200
+ left_widget = QWidget()
201
+ left_layout = QVBoxLayout(left_widget)
202
+ left_layout.setContentsMargins(0, 0, 5, 0)
203
+
204
+ left_layout.addWidget(QLabel("<b>📁 Saved F&R Sets</b>"))
205
+
206
+ self.sets_table = QTableWidget()
207
+ self.sets_table.setColumnCount(2)
208
+ self.sets_table.setHorizontalHeaderLabels(["Name", "Operations"])
209
+ self.sets_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
210
+ self.sets_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
211
+ self.sets_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
212
+ self.sets_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
213
+ self.sets_table.itemSelectionChanged.connect(self._on_set_selected)
214
+ left_layout.addWidget(self.sets_table)
215
+
216
+ # Sets buttons
217
+ sets_btn_layout = QHBoxLayout()
218
+
219
+ new_set_btn = QPushButton("+ New Set")
220
+ new_set_btn.clicked.connect(self._create_new_set)
221
+ sets_btn_layout.addWidget(new_set_btn)
222
+
223
+ import_btn = QPushButton("📥 Import")
224
+ import_btn.clicked.connect(self._import_set)
225
+ sets_btn_layout.addWidget(import_btn)
226
+
227
+ left_layout.addLayout(sets_btn_layout)
228
+
229
+ splitter.addWidget(left_widget)
230
+
231
+ # Right side: Operations in selected set
232
+ right_widget = QWidget()
233
+ right_layout = QVBoxLayout(right_widget)
234
+ right_layout.setContentsMargins(5, 0, 0, 0)
235
+
236
+ self.ops_label = QLabel("<b>📋 Operations in: (none selected)</b>")
237
+ right_layout.addWidget(self.ops_label)
238
+
239
+ self.ops_table = QTableWidget()
240
+ self.ops_table.setColumnCount(5)
241
+ self.ops_table.setHorizontalHeaderLabels(["✓", "Find", "Replace", "Search in", "Match"])
242
+ self.ops_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
243
+ self.ops_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
244
+ self.ops_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
245
+ self.ops_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
246
+ self.ops_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
247
+ self.ops_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
248
+ self.ops_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
249
+ self.ops_table.cellDoubleClicked.connect(self._on_op_double_clicked)
250
+ self.ops_table.cellChanged.connect(self._on_op_cell_changed)
251
+ right_layout.addWidget(self.ops_table)
252
+
253
+ # Operations buttons
254
+ ops_btn_layout = QHBoxLayout()
255
+
256
+ add_op_btn = QPushButton("+ Add Operation")
257
+ add_op_btn.clicked.connect(self._add_empty_operation)
258
+ ops_btn_layout.addWidget(add_op_btn)
259
+
260
+ ops_btn_layout.addStretch()
261
+
262
+ run_all_btn = QPushButton("▶ Run All")
263
+ run_all_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
264
+ run_all_btn.clicked.connect(self._run_all_operations)
265
+ ops_btn_layout.addWidget(run_all_btn)
266
+
267
+ right_layout.addLayout(ops_btn_layout)
268
+
269
+ splitter.addWidget(right_widget)
270
+ splitter.setSizes([300, 500])
271
+
272
+ layout.addWidget(splitter)
273
+
274
+ def _load_sets(self):
275
+ """Load all F&R sets from the sets directory."""
276
+ self.sets = []
277
+ for file_path in self.sets_dir.glob("*.svfr"):
278
+ try:
279
+ with open(file_path, 'r', encoding='utf-8') as f:
280
+ data = json.load(f)
281
+ fr_set = FindReplaceSet.from_dict(data)
282
+ self.sets.append(fr_set)
283
+ except Exception:
284
+ pass
285
+
286
+ # If no sets exist, create a default one
287
+ if not self.sets:
288
+ default_set = FindReplaceSet(name="F&R Set 1")
289
+ self.sets.append(default_set)
290
+ self._save_set(default_set)
291
+
292
+ def _save_set(self, fr_set: FindReplaceSet):
293
+ """Save a F&R set to file."""
294
+ safe_name = "".join(c if c.isalnum() or c in " _-" else "_" for c in fr_set.name)
295
+ file_path = self.sets_dir / f"{safe_name}.svfr"
296
+ try:
297
+ with open(file_path, 'w', encoding='utf-8') as f:
298
+ json.dump(fr_set.to_dict(), f, ensure_ascii=False, indent=2)
299
+ except Exception as e:
300
+ print(f"Error saving F&R set: {e}")
301
+
302
+ def _refresh_sets_table(self):
303
+ """Refresh the sets table."""
304
+ self.sets_table.setRowCount(len(self.sets))
305
+ for i, fr_set in enumerate(self.sets):
306
+ name_item = QTableWidgetItem(fr_set.name)
307
+ name_item.setForeground(QColor("#1976D2")) # Blue color for names
308
+ self.sets_table.setItem(i, 0, name_item)
309
+
310
+ count_item = QTableWidgetItem(str(len(fr_set.operations)))
311
+ count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
312
+ self.sets_table.setItem(i, 1, count_item)
313
+
314
+ # Select first set if none selected
315
+ if self.sets and not self.current_set:
316
+ self.sets_table.selectRow(0)
317
+
318
+ def _refresh_ops_table(self):
319
+ """Refresh the operations table for the current set."""
320
+ self.ops_table.blockSignals(True)
321
+
322
+ if not self.current_set:
323
+ self.ops_table.setRowCount(0)
324
+ self.ops_label.setText("<b>📋 Operations in: (none selected)</b>")
325
+ self.ops_table.blockSignals(False)
326
+ return
327
+
328
+ self.ops_label.setText(f"<b>📋 Operations in: {self.current_set.name}</b>")
329
+ self.ops_table.setRowCount(len(self.current_set.operations))
330
+
331
+ for i, op in enumerate(self.current_set.operations):
332
+ # Enabled checkbox
333
+ enabled_item = QTableWidgetItem("✓" if op.enabled else "")
334
+ enabled_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
335
+ enabled_item.setFlags(enabled_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
336
+ enabled_item.setCheckState(Qt.CheckState.Checked if op.enabled else Qt.CheckState.Unchecked)
337
+ self.ops_table.setItem(i, 0, enabled_item)
338
+
339
+ # Find text
340
+ find_item = QTableWidgetItem(op.find_text)
341
+ self.ops_table.setItem(i, 1, find_item)
342
+
343
+ # Replace text
344
+ replace_item = QTableWidgetItem(op.replace_text)
345
+ self.ops_table.setItem(i, 2, replace_item)
346
+
347
+ # Search in - full text
348
+ search_in_map = {"source": "Source", "target": "Target", "both": "Both"}
349
+ search_item = QTableWidgetItem(search_in_map.get(op.search_in, "Target"))
350
+ self.ops_table.setItem(i, 3, search_item)
351
+
352
+ # Match mode - full text
353
+ match_map = {0: "Anything", 1: "Whole words", 2: "Entire segment"}
354
+ match_item = QTableWidgetItem(match_map.get(op.match_mode, "Anything"))
355
+ self.ops_table.setItem(i, 4, match_item)
356
+
357
+ self.ops_table.blockSignals(False)
358
+
359
+ def _on_set_selected(self):
360
+ """Handle set selection in the sets table."""
361
+ selected_rows = self.sets_table.selectionModel().selectedRows()
362
+ if selected_rows:
363
+ index = selected_rows[0].row()
364
+ if 0 <= index < len(self.sets):
365
+ self.current_set = self.sets[index]
366
+ self._refresh_ops_table()
367
+
368
+ def _on_op_double_clicked(self, row: int, col: int):
369
+ """Handle double-click on an operation to load it into the dialog."""
370
+ if self.current_set and 0 <= row < len(self.current_set.operations):
371
+ op = self.current_set.operations[row]
372
+ self.operation_selected.emit(op)
373
+
374
+ def _on_op_cell_changed(self, row: int, col: int):
375
+ """Handle cell changes in the operations table."""
376
+ if not self.current_set or row >= len(self.current_set.operations):
377
+ return
378
+
379
+ op = self.current_set.operations[row]
380
+ item = self.ops_table.item(row, col)
381
+
382
+ if col == 0: # Enabled checkbox
383
+ op.enabled = item.checkState() == Qt.CheckState.Checked
384
+ elif col == 1: # Find text
385
+ op.find_text = item.text()
386
+ elif col == 2: # Replace text
387
+ op.replace_text = item.text()
388
+
389
+ self._save_set(self.current_set)
390
+
391
+ def _create_new_set(self):
392
+ """Create a new F&R set."""
393
+ name, ok = QInputDialog.getText(self, "New F&R Set", "Enter set name:")
394
+ if ok and name.strip():
395
+ new_set = FindReplaceSet(name=name.strip())
396
+ self.sets.append(new_set)
397
+ self._save_set(new_set)
398
+ self._refresh_sets_table()
399
+ # Select the new set
400
+ self.sets_table.selectRow(len(self.sets) - 1)
401
+
402
+ def _import_set(self):
403
+ """Import a F&R set from a .svfr file."""
404
+ file_path, _ = QFileDialog.getOpenFileName(
405
+ self, "Import F&R Set", "", "Supervertaler F&R Sets (*.svfr);;All Files (*)"
406
+ )
407
+ if file_path:
408
+ try:
409
+ with open(file_path, 'r', encoding='utf-8') as f:
410
+ data = json.load(f)
411
+ fr_set = FindReplaceSet.from_dict(data)
412
+ self.sets.append(fr_set)
413
+ self._save_set(fr_set)
414
+ self._refresh_sets_table()
415
+ QMessageBox.information(self, "Import", f"Imported '{fr_set.name}' with {len(fr_set.operations)} operations.")
416
+ except Exception as e:
417
+ QMessageBox.warning(self, "Import Error", f"Failed to import: {e}")
418
+
419
+ def _add_empty_operation(self):
420
+ """Add an empty operation to the current set."""
421
+ if not self.current_set:
422
+ QMessageBox.information(self, "No Set", "Please select or create a F&R set first.")
423
+ return
424
+
425
+ op = FindReplaceOperation(find_text="", replace_text="")
426
+ self.current_set.add_operation(op)
427
+ self._save_set(self.current_set)
428
+ self._refresh_ops_table()
429
+ self._refresh_sets_table() # Update operation count
430
+
431
+ def _run_all_operations(self):
432
+ """Request to run all enabled operations in the current set."""
433
+ if not self.current_set:
434
+ return
435
+
436
+ enabled_ops = [op for op in self.current_set.operations if op.enabled and op.find_text]
437
+ if not enabled_ops:
438
+ QMessageBox.information(self, "Run All", "No enabled operations with find text.")
439
+ return
440
+
441
+ self.set_selected.emit(self.current_set)
442
+
443
+ def add_current_operation_to_set(self, op: FindReplaceOperation):
444
+ """Add an operation to the current set (called from main dialog)."""
445
+ if not self.current_set:
446
+ # Create a default set if none exists
447
+ if not self.sets:
448
+ self._create_new_set()
449
+ if self.sets:
450
+ self.current_set = self.sets[0]
451
+ self.sets_table.selectRow(0)
452
+
453
+ if self.current_set:
454
+ self.current_set.add_operation(op)
455
+ self._save_set(self.current_set)
456
+ self._refresh_ops_table()
457
+ self._refresh_sets_table() # Update operation count