supervertaler 1.9.109__py3-none-any.whl → 1.9.131__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Supervertaler.py +1240 -130
- modules/shortcut_manager.py +29 -1
- modules/termview_widget.py +9 -2
- modules/unified_prompt_library.py +12 -9
- modules/unified_prompt_manager_qt.py +369 -114
- {supervertaler-1.9.109.dist-info → supervertaler-1.9.131.dist-info}/METADATA +53 -3
- {supervertaler-1.9.109.dist-info → supervertaler-1.9.131.dist-info}/RECORD +11 -11
- {supervertaler-1.9.109.dist-info → supervertaler-1.9.131.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.109.dist-info → supervertaler-1.9.131.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.109.dist-info → supervertaler-1.9.131.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.109.dist-info → supervertaler-1.9.131.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -3,8 +3,8 @@ Supervertaler
|
|
|
3
3
|
=============
|
|
4
4
|
The Ultimate Translation Workbench.
|
|
5
5
|
Modern PyQt6 interface with specialised modules to handle any problem.
|
|
6
|
-
Version: 1.9.
|
|
7
|
-
Release Date: January
|
|
6
|
+
Version: 1.9.123 (QuickMenu now supports generic AI tasks)
|
|
7
|
+
Release Date: January 19, 2026
|
|
8
8
|
Framework: PyQt6
|
|
9
9
|
|
|
10
10
|
This is the modern edition of Supervertaler using PyQt6 framework.
|
|
@@ -34,9 +34,9 @@ License: MIT
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
37
|
+
__version__ = "1.9.131"
|
|
38
38
|
__phase__ = "0.9"
|
|
39
|
-
__release_date__ = "2026-01-
|
|
39
|
+
__release_date__ = "2026-01-19"
|
|
40
40
|
__edition__ = "Qt"
|
|
41
41
|
|
|
42
42
|
import sys
|
|
@@ -123,10 +123,10 @@ try:
|
|
|
123
123
|
QButtonGroup, QDialogButtonBox, QTabWidget, QGroupBox, QGridLayout, QCheckBox,
|
|
124
124
|
QProgressBar, QProgressDialog, QFormLayout, QTabBar, QPlainTextEdit, QAbstractItemDelegate,
|
|
125
125
|
QFrame, QListWidget, QListWidgetItem, QStackedWidget, QTreeWidget, QTreeWidgetItem,
|
|
126
|
-
QScrollArea, QSizePolicy, QSlider, QToolButton
|
|
126
|
+
QScrollArea, QSizePolicy, QSlider, QToolButton, QAbstractItemView
|
|
127
127
|
)
|
|
128
128
|
from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal, QObject, QUrl
|
|
129
|
-
from PyQt6.QtGui import QFont, QAction, QKeySequence, QIcon, QTextOption, QColor, QDesktopServices, QTextCharFormat, QTextCursor, QBrush, QSyntaxHighlighter, QPalette, QTextBlockFormat
|
|
129
|
+
from PyQt6.QtGui import QFont, QAction, QKeySequence, QIcon, QTextOption, QColor, QDesktopServices, QTextCharFormat, QTextCursor, QBrush, QSyntaxHighlighter, QPalette, QTextBlockFormat, QCursor
|
|
130
130
|
from PyQt6.QtWidgets import QStyleOptionViewItem, QStyle
|
|
131
131
|
from PyQt6.QtCore import QRectF
|
|
132
132
|
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
|
@@ -170,11 +170,11 @@ def cleanup_ahk_process():
|
|
|
170
170
|
try:
|
|
171
171
|
_ahk_process.terminate()
|
|
172
172
|
_ahk_process.wait(timeout=1)
|
|
173
|
-
print("[
|
|
173
|
+
print("[Hotkeys] AHK process terminated on exit")
|
|
174
174
|
except:
|
|
175
175
|
try:
|
|
176
176
|
_ahk_process.kill()
|
|
177
|
-
print("[
|
|
177
|
+
print("[Hotkeys] AHK process killed on exit")
|
|
178
178
|
except:
|
|
179
179
|
pass
|
|
180
180
|
|
|
@@ -910,6 +910,7 @@ class _CtrlReturnEventFilter(QObject):
|
|
|
910
910
|
|
|
911
911
|
return False
|
|
912
912
|
|
|
913
|
+
|
|
913
914
|
class GridTableEventFilter:
|
|
914
915
|
"""Mixin to pass keyboard shortcuts from editor to table"""
|
|
915
916
|
pass
|
|
@@ -2559,7 +2560,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
2559
2560
|
menu.addSeparator()
|
|
2560
2561
|
|
|
2561
2562
|
# Add to dictionary action
|
|
2562
|
-
add_to_dict_action = QAction(f"📖 Add '{misspelled_word}' to Dictionary", self)
|
|
2563
|
+
add_to_dict_action = QAction(f"📖 Add '{misspelled_word}' to Dictionary (Alt+D)", self)
|
|
2563
2564
|
add_to_dict_action.triggered.connect(
|
|
2564
2565
|
lambda checked, w=misspelled_word: self._add_to_dictionary(w)
|
|
2565
2566
|
)
|
|
@@ -5322,7 +5323,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
5322
5323
|
dialog.exec()
|
|
5323
5324
|
|
|
5324
5325
|
# Navigate to Settings → Features tab
|
|
5325
|
-
self.main_tabs.setCurrentIndex(
|
|
5326
|
+
self.main_tabs.setCurrentIndex(4) # Settings tab
|
|
5326
5327
|
if hasattr(self, 'settings_tabs'):
|
|
5327
5328
|
# Find the Features tab index
|
|
5328
5329
|
for i in range(self.settings_tabs.count()):
|
|
@@ -5592,6 +5593,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
5592
5593
|
self.termview_shortcuts = []
|
|
5593
5594
|
self._termview_last_key = None # Track last key for double-tap detection
|
|
5594
5595
|
self._termview_last_time = 0 # Track timing for double-tap
|
|
5596
|
+
|
|
5597
|
+
# Double-tap Shift detection for context menu
|
|
5598
|
+
self._shift_last_press_time = 0
|
|
5599
|
+
self._shift_double_tap_threshold = 0.35 # 350ms window
|
|
5600
|
+
|
|
5595
5601
|
for i in range(0, 10): # 0-9
|
|
5596
5602
|
shortcut_id = f"termview_insert_{i}"
|
|
5597
5603
|
default_key = f"Alt+{i}"
|
|
@@ -5635,6 +5641,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
5635
5641
|
self._ctrl_return_event_filter = _CtrlReturnEventFilter(self)
|
|
5636
5642
|
QApplication.instance().installEventFilter(self._ctrl_return_event_filter)
|
|
5637
5643
|
|
|
5644
|
+
# Note: Double-tap Shift for context menu is handled by AutoHotkey (double_shift_menu.ahk)
|
|
5645
|
+
# Qt's event system makes reliable double-tap detection difficult in Python
|
|
5646
|
+
|
|
5638
5647
|
# Ctrl+Shift+Enter - Always confirm all selected segments
|
|
5639
5648
|
create_shortcut("editor_confirm_selected", "Ctrl+Shift+Return", self.confirm_selected_segments)
|
|
5640
5649
|
|
|
@@ -5668,6 +5677,84 @@ class SupervertalerQt(QMainWindow):
|
|
|
5668
5677
|
|
|
5669
5678
|
# Ctrl+Shift+2 - Quick add term with Priority 2
|
|
5670
5679
|
create_shortcut("editor_quick_add_priority_2", "Ctrl+Shift+2", lambda: self._quick_add_term_with_priority(2))
|
|
5680
|
+
|
|
5681
|
+
# Alt+D - Add word at cursor to dictionary
|
|
5682
|
+
create_shortcut("editor_add_to_dictionary", "Alt+D", self.add_word_to_dictionary_shortcut)
|
|
5683
|
+
|
|
5684
|
+
# Ctrl+N - Focus Segment Note tab
|
|
5685
|
+
create_shortcut("editor_focus_notes", "Ctrl+N", self.focus_segment_notes)
|
|
5686
|
+
|
|
5687
|
+
# Alt+K - Open QuickMenu directly
|
|
5688
|
+
create_shortcut("editor_open_quickmenu", "Alt+K", self.open_quickmenu)
|
|
5689
|
+
|
|
5690
|
+
def focus_segment_notes(self):
|
|
5691
|
+
"""Switch to Segment Note tab and focus the notes editor so user can start typing immediately"""
|
|
5692
|
+
if not hasattr(self, 'bottom_tabs'):
|
|
5693
|
+
return
|
|
5694
|
+
|
|
5695
|
+
# Switch to Segment note tab (index 1)
|
|
5696
|
+
self.bottom_tabs.setCurrentIndex(1)
|
|
5697
|
+
|
|
5698
|
+
# Focus the notes editor so user can start typing
|
|
5699
|
+
if hasattr(self, 'bottom_notes_edit'):
|
|
5700
|
+
self.bottom_notes_edit.setFocus()
|
|
5701
|
+
|
|
5702
|
+
def open_quickmenu(self):
|
|
5703
|
+
"""Open QuickMenu popup at current cursor position for quick AI prompt selection.
|
|
5704
|
+
|
|
5705
|
+
User can navigate with arrow keys and press Enter to select a prompt.
|
|
5706
|
+
"""
|
|
5707
|
+
try:
|
|
5708
|
+
# Get QuickMenu items from prompt library
|
|
5709
|
+
quickmenu_items = []
|
|
5710
|
+
if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt:
|
|
5711
|
+
lib = getattr(self.prompt_manager_qt, 'library', None)
|
|
5712
|
+
if lib and hasattr(lib, 'get_quickmenu_grid_prompts'):
|
|
5713
|
+
quickmenu_items = lib.get_quickmenu_grid_prompts() or []
|
|
5714
|
+
|
|
5715
|
+
if not quickmenu_items:
|
|
5716
|
+
self.log("⚠️ No QuickMenu prompts available. Add prompts with 'Show in Supervertaler QuickMenu' enabled.")
|
|
5717
|
+
return
|
|
5718
|
+
|
|
5719
|
+
# Find the currently focused widget (source or target cell)
|
|
5720
|
+
focus_widget = QApplication.focusWidget()
|
|
5721
|
+
|
|
5722
|
+
# Build the menu
|
|
5723
|
+
menu = QMenu(self)
|
|
5724
|
+
menu.setTitle("⚡ QuickMenu")
|
|
5725
|
+
|
|
5726
|
+
for rel_path, label in sorted(quickmenu_items, key=lambda x: (x[1] or x[0]).lower()):
|
|
5727
|
+
prompt_menu = menu.addMenu(label or rel_path)
|
|
5728
|
+
|
|
5729
|
+
run_show = QAction("▶ Run (show response)…", self)
|
|
5730
|
+
run_show.triggered.connect(
|
|
5731
|
+
lambda checked=False, p=rel_path, w=focus_widget: self.run_grid_quickmenu_prompt(p, origin_widget=w, behavior="show")
|
|
5732
|
+
)
|
|
5733
|
+
prompt_menu.addAction(run_show)
|
|
5734
|
+
|
|
5735
|
+
run_replace = QAction("↺ Run and replace target selection", self)
|
|
5736
|
+
run_replace.triggered.connect(
|
|
5737
|
+
lambda checked=False, p=rel_path, w=focus_widget: self.run_grid_quickmenu_prompt(p, origin_widget=w, behavior="replace")
|
|
5738
|
+
)
|
|
5739
|
+
prompt_menu.addAction(run_replace)
|
|
5740
|
+
|
|
5741
|
+
# Show menu at cursor position (or center of focused widget)
|
|
5742
|
+
if focus_widget:
|
|
5743
|
+
# Get cursor rectangle if it's a text editor
|
|
5744
|
+
if hasattr(focus_widget, 'cursorRect'):
|
|
5745
|
+
cursor_rect = focus_widget.cursorRect()
|
|
5746
|
+
pos = focus_widget.mapToGlobal(cursor_rect.bottomLeft())
|
|
5747
|
+
else:
|
|
5748
|
+
# Fallback to center of widget
|
|
5749
|
+
pos = focus_widget.mapToGlobal(focus_widget.rect().center())
|
|
5750
|
+
else:
|
|
5751
|
+
# Fallback to mouse cursor position
|
|
5752
|
+
pos = QCursor.pos()
|
|
5753
|
+
|
|
5754
|
+
menu.exec(pos)
|
|
5755
|
+
|
|
5756
|
+
except Exception as e:
|
|
5757
|
+
self.log(f"❌ Error opening QuickMenu: {e}")
|
|
5671
5758
|
|
|
5672
5759
|
def refresh_shortcut_enabled_states(self):
|
|
5673
5760
|
"""Refresh enabled/disabled states and key bindings of all global shortcuts from shortcut manager.
|
|
@@ -6085,8 +6172,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
6085
6172
|
export_txt_action.triggered.connect(self.export_simple_txt)
|
|
6086
6173
|
export_menu.addAction(export_txt_action)
|
|
6087
6174
|
|
|
6088
|
-
export_ai_action = QAction("
|
|
6089
|
-
export_ai_action.triggered.connect(self.
|
|
6175
|
+
export_ai_action = QAction("📄 &AI-Readable Markdown (.md)...", self)
|
|
6176
|
+
export_ai_action.triggered.connect(self.export_bilingual_table_markdown)
|
|
6090
6177
|
export_ai_action.setToolTip("Export segments in [SEGMENT] format for AI translation/review")
|
|
6091
6178
|
export_menu.addAction(export_ai_action)
|
|
6092
6179
|
|
|
@@ -6307,12 +6394,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
6307
6394
|
go_resources_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(1) if hasattr(self, 'main_tabs') else None)
|
|
6308
6395
|
nav_menu.addAction(go_resources_action)
|
|
6309
6396
|
|
|
6397
|
+
go_prompt_manager_action = QAction("🤖 &Prompt Manager", self)
|
|
6398
|
+
go_prompt_manager_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(2) if hasattr(self, 'main_tabs') else None)
|
|
6399
|
+
nav_menu.addAction(go_prompt_manager_action)
|
|
6400
|
+
|
|
6310
6401
|
go_tools_action = QAction("🛠️ &Tools", self)
|
|
6311
|
-
go_tools_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(
|
|
6402
|
+
go_tools_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(3) if hasattr(self, 'main_tabs') else None)
|
|
6312
6403
|
nav_menu.addAction(go_tools_action)
|
|
6313
6404
|
|
|
6314
6405
|
go_settings_action = QAction("⚙️ &Settings", self)
|
|
6315
|
-
go_settings_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(
|
|
6406
|
+
go_settings_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(4) if hasattr(self, 'main_tabs') else None)
|
|
6316
6407
|
nav_menu.addAction(go_settings_action)
|
|
6317
6408
|
|
|
6318
6409
|
view_menu.addSeparator()
|
|
@@ -6415,11 +6506,53 @@ class SupervertalerQt(QMainWindow):
|
|
|
6415
6506
|
# Tools Menu
|
|
6416
6507
|
tools_menu = menubar.addMenu("&Tools")
|
|
6417
6508
|
|
|
6418
|
-
|
|
6509
|
+
# Tools in same order as Tools tab
|
|
6510
|
+
autofingers_action = QAction("✋ &AutoFingers...", self)
|
|
6419
6511
|
autofingers_action.setShortcut("Ctrl+Shift+A")
|
|
6420
6512
|
autofingers_action.triggered.connect(self.show_autofingers)
|
|
6421
6513
|
tools_menu.addAction(autofingers_action)
|
|
6422
6514
|
|
|
6515
|
+
superconverter_action = QAction("🔄 Super&converter...", self)
|
|
6516
|
+
superconverter_action.triggered.connect(lambda: self._navigate_to_tool("Superconverter"))
|
|
6517
|
+
tools_menu.addAction(superconverter_action)
|
|
6518
|
+
|
|
6519
|
+
pdf_rescue_action = QAction("📄 &PDF Rescue...", self)
|
|
6520
|
+
pdf_rescue_action.triggered.connect(lambda: self._navigate_to_tool("PDF Rescue"))
|
|
6521
|
+
tools_menu.addAction(pdf_rescue_action)
|
|
6522
|
+
|
|
6523
|
+
superbench_action = QAction("📊 Super&bench...", self)
|
|
6524
|
+
superbench_action.triggered.connect(lambda: self._navigate_to_tool("Superbench"))
|
|
6525
|
+
tools_menu.addAction(superbench_action)
|
|
6526
|
+
|
|
6527
|
+
superbrowser_action = QAction("🌐 Super&browser...", self)
|
|
6528
|
+
superbrowser_action.triggered.connect(lambda: self._navigate_to_tool("Superbrowser"))
|
|
6529
|
+
tools_menu.addAction(superbrowser_action)
|
|
6530
|
+
|
|
6531
|
+
supercleaner_action = QAction("🧹 Supercleaner...", self)
|
|
6532
|
+
supercleaner_action.triggered.connect(lambda: self._navigate_to_tool("Supercleaner"))
|
|
6533
|
+
tools_menu.addAction(supercleaner_action)
|
|
6534
|
+
|
|
6535
|
+
superlookup_action = QAction("🔍 Super&lookup...", self)
|
|
6536
|
+
superlookup_action.setShortcut("Ctrl+K")
|
|
6537
|
+
superlookup_action.triggered.connect(self._go_to_superlookup)
|
|
6538
|
+
tools_menu.addAction(superlookup_action)
|
|
6539
|
+
|
|
6540
|
+
supervoice_action = QAction("🎤 Super&voice...", self)
|
|
6541
|
+
supervoice_action.triggered.connect(lambda: self._navigate_to_tool("Supervoice"))
|
|
6542
|
+
tools_menu.addAction(supervoice_action)
|
|
6543
|
+
|
|
6544
|
+
encoding_action = QAction("🔧 &Text Encoding Repair...", self)
|
|
6545
|
+
encoding_action.triggered.connect(lambda: self._navigate_to_tool("Text Encoding Repair"))
|
|
6546
|
+
tools_menu.addAction(encoding_action)
|
|
6547
|
+
|
|
6548
|
+
tmx_editor_action = QAction("✏️ T&MX Editor...", self)
|
|
6549
|
+
tmx_editor_action.triggered.connect(lambda: self._navigate_to_tool("TMX Editor"))
|
|
6550
|
+
tools_menu.addAction(tmx_editor_action)
|
|
6551
|
+
|
|
6552
|
+
tracked_changes_action = QAction("🔄 Tracked &Changes...", self)
|
|
6553
|
+
tracked_changes_action.triggered.connect(lambda: self._navigate_to_tool("Tracked Changes"))
|
|
6554
|
+
tools_menu.addAction(tracked_changes_action)
|
|
6555
|
+
|
|
6423
6556
|
tools_menu.addSeparator()
|
|
6424
6557
|
|
|
6425
6558
|
image_extractor_action = QAction("🖼️ &Image Extractor (Superimage)...", self)
|
|
@@ -6758,6 +6891,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
6758
6891
|
settings_tab = self.create_settings_tab()
|
|
6759
6892
|
self.main_tabs.addTab(settings_tab, "⚙️ Settings")
|
|
6760
6893
|
|
|
6894
|
+
# Set startup tab to Grid (index 0)
|
|
6895
|
+
self.main_tabs.setCurrentIndex(0)
|
|
6896
|
+
|
|
6761
6897
|
main_layout.addWidget(self.main_tabs)
|
|
6762
6898
|
|
|
6763
6899
|
# Connect tab changes to handle view refreshes
|
|
@@ -7282,6 +7418,143 @@ class SupervertalerQt(QMainWindow):
|
|
|
7282
7418
|
|
|
7283
7419
|
return prompt_widget
|
|
7284
7420
|
|
|
7421
|
+
|
|
7422
|
+
def create_superconverter_tab(self) -> QWidget:
|
|
7423
|
+
"""Create the Superconverter tab - Format conversion tools"""
|
|
7424
|
+
container = QWidget()
|
|
7425
|
+
main_layout = QVBoxLayout(container)
|
|
7426
|
+
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
7427
|
+
main_layout.setSpacing(10)
|
|
7428
|
+
|
|
7429
|
+
# Header
|
|
7430
|
+
header = QLabel("🔄 Superconverter")
|
|
7431
|
+
header.setStyleSheet("font-size: 16pt; font-weight: bold; color: #1976D2;")
|
|
7432
|
+
main_layout.addWidget(header)
|
|
7433
|
+
|
|
7434
|
+
# Description
|
|
7435
|
+
description = QLabel(
|
|
7436
|
+
"Convert translation data between different formats - perfect for AI workflows, CAT tool exchanges, and data processing."
|
|
7437
|
+
)
|
|
7438
|
+
description.setWordWrap(True)
|
|
7439
|
+
description.setStyleSheet("color: #666; padding: 5px; background-color: #E3F2FD; border-radius: 3px;")
|
|
7440
|
+
main_layout.addWidget(description)
|
|
7441
|
+
|
|
7442
|
+
main_layout.addSpacing(5)
|
|
7443
|
+
|
|
7444
|
+
# Create tabbed interface for different conversion types
|
|
7445
|
+
tabs = QTabWidget()
|
|
7446
|
+
tabs.setStyleSheet("QTabBar::tab { outline: 0; } QTabBar::tab:focus { outline: none; }")
|
|
7447
|
+
|
|
7448
|
+
# Tab 1: Bilingual Export (current project)
|
|
7449
|
+
bilingual_tab = QWidget()
|
|
7450
|
+
bilingual_layout = QVBoxLayout(bilingual_tab)
|
|
7451
|
+
bilingual_layout.setContentsMargins(15, 15, 15, 15)
|
|
7452
|
+
|
|
7453
|
+
bilingual_info = QLabel(
|
|
7454
|
+
"<b>Export Current Project</b><br><br>"
|
|
7455
|
+
"Export your translation project as a Markdown table - perfect for AI chat interfaces like ChatGPT and Claude.<br><br>"
|
|
7456
|
+
"The table format renders beautifully and makes it easy for AI to understand and process your segments."
|
|
7457
|
+
)
|
|
7458
|
+
bilingual_info.setWordWrap(True)
|
|
7459
|
+
bilingual_info.setTextFormat(Qt.TextFormat.RichText)
|
|
7460
|
+
bilingual_layout.addWidget(bilingual_info)
|
|
7461
|
+
|
|
7462
|
+
bilingual_layout.addSpacing(15)
|
|
7463
|
+
|
|
7464
|
+
bilingual_btn = QPushButton("📄 Export as Markdown Table")
|
|
7465
|
+
bilingual_btn.setMinimumHeight(40)
|
|
7466
|
+
bilingual_btn.setStyleSheet(
|
|
7467
|
+
"QPushButton { background-color: #2196F3; color: white; font-weight: bold; "
|
|
7468
|
+
"border: none; border-radius: 5px; padding: 10px; outline: none; }"
|
|
7469
|
+
"QPushButton:hover { background-color: #1976D2; }"
|
|
7470
|
+
)
|
|
7471
|
+
bilingual_btn.clicked.connect(self.export_bilingual_table_markdown)
|
|
7472
|
+
bilingual_layout.addWidget(bilingual_btn)
|
|
7473
|
+
|
|
7474
|
+
bilingual_layout.addStretch()
|
|
7475
|
+
tabs.addTab(bilingual_tab, "📊 Bilingual Table")
|
|
7476
|
+
|
|
7477
|
+
# Tab 2: Document Converter (monolingual docs to Markdown)
|
|
7478
|
+
doc_tab = QWidget()
|
|
7479
|
+
doc_layout = QVBoxLayout(doc_tab)
|
|
7480
|
+
doc_layout.setContentsMargins(15, 15, 15, 15)
|
|
7481
|
+
|
|
7482
|
+
doc_info = QLabel(
|
|
7483
|
+
"<b>Convert Documents to Markdown</b><br><br>"
|
|
7484
|
+
"Convert DOCX or TXT documents to Markdown format, preserving structure (headings, lists, paragraphs).<br><br>"
|
|
7485
|
+
"Perfect for preparing documents for AI processing or publishing on web platforms."
|
|
7486
|
+
)
|
|
7487
|
+
doc_info.setWordWrap(True)
|
|
7488
|
+
doc_info.setTextFormat(Qt.TextFormat.RichText)
|
|
7489
|
+
doc_layout.addWidget(doc_info)
|
|
7490
|
+
|
|
7491
|
+
doc_layout.addSpacing(15)
|
|
7492
|
+
|
|
7493
|
+
# Single file button
|
|
7494
|
+
single_doc_btn = QPushButton("📄 Convert Single Document")
|
|
7495
|
+
single_doc_btn.setMinimumHeight(40)
|
|
7496
|
+
single_doc_btn.setStyleSheet(
|
|
7497
|
+
"QPushButton { background-color: #4CAF50; color: white; font-weight: bold; "
|
|
7498
|
+
"border: none; border-radius: 5px; padding: 10px; outline: none; }"
|
|
7499
|
+
"QPushButton:hover { background-color: #388E3C; }"
|
|
7500
|
+
)
|
|
7501
|
+
single_doc_btn.clicked.connect(self.convert_document_to_markdown)
|
|
7502
|
+
doc_layout.addWidget(single_doc_btn)
|
|
7503
|
+
|
|
7504
|
+
doc_layout.addSpacing(10)
|
|
7505
|
+
|
|
7506
|
+
# Batch conversion button
|
|
7507
|
+
batch_doc_btn = QPushButton("📁 Batch Convert Multiple Documents")
|
|
7508
|
+
batch_doc_btn.setMinimumHeight(40)
|
|
7509
|
+
batch_doc_btn.setStyleSheet(
|
|
7510
|
+
"QPushButton { background-color: #FF9800; color: white; font-weight: bold; "
|
|
7511
|
+
"border: none; border-radius: 5px; padding: 10px; outline: none; }"
|
|
7512
|
+
"QPushButton:hover { background-color: #F57C00; }"
|
|
7513
|
+
)
|
|
7514
|
+
batch_doc_btn.clicked.connect(self.batch_convert_documents_to_markdown)
|
|
7515
|
+
doc_layout.addWidget(batch_doc_btn)
|
|
7516
|
+
|
|
7517
|
+
doc_layout.addStretch()
|
|
7518
|
+
tabs.addTab(doc_tab, "📝 Document → Markdown")
|
|
7519
|
+
|
|
7520
|
+
# Tab 3: TMX Tools (placeholder for future)
|
|
7521
|
+
tmx_tab = QWidget()
|
|
7522
|
+
tmx_layout = QVBoxLayout(tmx_tab)
|
|
7523
|
+
tmx_layout.setContentsMargins(15, 15, 15, 15)
|
|
7524
|
+
|
|
7525
|
+
tmx_info = QLabel(
|
|
7526
|
+
"<b>TMX Conversion Tools</b><br><br>"
|
|
7527
|
+
"Convert Translation Memory eXchange (TMX) files to and from tab-delimited format.<br><br>"
|
|
7528
|
+
"<i>Coming soon...</i>"
|
|
7529
|
+
)
|
|
7530
|
+
tmx_info.setWordWrap(True)
|
|
7531
|
+
tmx_info.setTextFormat(Qt.TextFormat.RichText)
|
|
7532
|
+
tmx_info.setStyleSheet("color: #888;")
|
|
7533
|
+
tmx_layout.addWidget(tmx_info)
|
|
7534
|
+
|
|
7535
|
+
tmx_layout.addSpacing(15)
|
|
7536
|
+
|
|
7537
|
+
tmx_to_tsv_btn = QPushButton("TMX → Tab-delimited")
|
|
7538
|
+
tmx_to_tsv_btn.setMinimumHeight(40)
|
|
7539
|
+
tmx_to_tsv_btn.setEnabled(False)
|
|
7540
|
+
tmx_to_tsv_btn.setStyleSheet("QPushButton:disabled { color: #999; background-color: #E0E0E0; }")
|
|
7541
|
+
tmx_layout.addWidget(tmx_to_tsv_btn)
|
|
7542
|
+
|
|
7543
|
+
tmx_layout.addSpacing(10)
|
|
7544
|
+
|
|
7545
|
+
tsv_to_tmx_btn = QPushButton("Tab-delimited → TMX")
|
|
7546
|
+
tsv_to_tmx_btn.setMinimumHeight(40)
|
|
7547
|
+
tsv_to_tmx_btn.setEnabled(False)
|
|
7548
|
+
tsv_to_tmx_btn.setStyleSheet("QPushButton:disabled { color: #999; background-color: #E0E0E0; }")
|
|
7549
|
+
tmx_layout.addWidget(tsv_to_tmx_btn)
|
|
7550
|
+
|
|
7551
|
+
tmx_layout.addStretch()
|
|
7552
|
+
tabs.addTab(tmx_tab, "🔄 TMX Tools")
|
|
7553
|
+
|
|
7554
|
+
main_layout.addWidget(tabs)
|
|
7555
|
+
|
|
7556
|
+
return container
|
|
7557
|
+
|
|
7285
7558
|
def create_supercleaner_tab(self) -> QWidget:
|
|
7286
7559
|
"""Create the Supercleaner tab - Clean DOCX documents"""
|
|
7287
7560
|
from modules.supercleaner_ui import SupercleanerUI
|
|
@@ -8412,6 +8685,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
8412
8685
|
autofingers_tab = AutoFingersWidget(self)
|
|
8413
8686
|
modules_tabs.addTab(autofingers_tab, "✋ AutoFingers")
|
|
8414
8687
|
|
|
8688
|
+
# Superconverter - Format conversion tools
|
|
8689
|
+
superconverter_tab = self.create_superconverter_tab()
|
|
8690
|
+
modules_tabs.addTab(superconverter_tab, "🔄 Superconverter")
|
|
8691
|
+
|
|
8415
8692
|
pdf_tab = self.create_pdf_rescue_tab()
|
|
8416
8693
|
modules_tabs.addTab(pdf_tab, "📄 PDF Rescue")
|
|
8417
8694
|
|
|
@@ -9734,8 +10011,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
9734
10011
|
elif status in ('draft', 'needs_review'):
|
|
9735
10012
|
run.font.color.rgb = RGBColor(200, 100, 0) # Orange
|
|
9736
10013
|
|
|
9737
|
-
# Notes column -
|
|
9738
|
-
|
|
10014
|
+
# Notes column - populate with segment notes if available
|
|
10015
|
+
notes_text = seg.notes if hasattr(seg, 'notes') else ''
|
|
10016
|
+
cells[4].text = notes_text
|
|
10017
|
+
for para in cells[4].paragraphs:
|
|
10018
|
+
for run in para.runs:
|
|
10019
|
+
run.font.size = Pt(8)
|
|
9739
10020
|
|
|
9740
10021
|
# Add footer with branding
|
|
9741
10022
|
footer_para = doc.add_paragraph()
|
|
@@ -10642,6 +10923,38 @@ class SupervertalerQt(QMainWindow):
|
|
|
10642
10923
|
self._play_sound_effect('glossary_term_error')
|
|
10643
10924
|
self.statusBar().showMessage(f"Error adding term: {e}", 3000)
|
|
10644
10925
|
|
|
10926
|
+
def add_word_to_dictionary_shortcut(self):
|
|
10927
|
+
"""Add word at cursor position to custom dictionary (Alt+D shortcut)
|
|
10928
|
+
|
|
10929
|
+
Finds the misspelled word at the current cursor position and adds it to the
|
|
10930
|
+
custom dictionary. Works when focus is in a grid target cell.
|
|
10931
|
+
"""
|
|
10932
|
+
# Get currently focused widget
|
|
10933
|
+
focused_widget = QApplication.focusWidget()
|
|
10934
|
+
|
|
10935
|
+
# Check if we're in an editable grid cell
|
|
10936
|
+
if not isinstance(focused_widget, EditableGridTextEditor):
|
|
10937
|
+
self.statusBar().showMessage("Alt+D: Place cursor on a misspelled word in the target cell first", 3000)
|
|
10938
|
+
return
|
|
10939
|
+
|
|
10940
|
+
# Get cursor position
|
|
10941
|
+
cursor = focused_widget.textCursor()
|
|
10942
|
+
|
|
10943
|
+
# Try to find misspelled word at cursor
|
|
10944
|
+
word_info = focused_widget._get_misspelled_word_at_cursor(cursor)
|
|
10945
|
+
|
|
10946
|
+
if word_info[0] is None:
|
|
10947
|
+
# No misspelled word found at cursor
|
|
10948
|
+
self.statusBar().showMessage("No misspelled word at cursor position", 3000)
|
|
10949
|
+
return
|
|
10950
|
+
|
|
10951
|
+
word, start_pos, end_pos = word_info
|
|
10952
|
+
|
|
10953
|
+
# Add to dictionary
|
|
10954
|
+
focused_widget._add_to_dictionary(word)
|
|
10955
|
+
|
|
10956
|
+
# Status message already shown by _add_to_dictionary
|
|
10957
|
+
|
|
10645
10958
|
def add_text_to_non_translatables(self, text: str):
|
|
10646
10959
|
"""Add selected text to active non-translatable list(s)"""
|
|
10647
10960
|
if not text or not text.strip():
|
|
@@ -13240,7 +13553,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
13240
13553
|
|
|
13241
13554
|
# ===== TAB 2: AI Settings (LLM, Ollama) =====
|
|
13242
13555
|
ai_tab = self._create_ai_settings_tab()
|
|
13243
|
-
|
|
13556
|
+
ai_scroll = scroll_area_wrapper(ai_tab)
|
|
13557
|
+
settings_tabs.addTab(ai_scroll, "🤖 AI Settings")
|
|
13558
|
+
self.ai_settings_scroll = ai_scroll # Store reference for scrolling to API keys
|
|
13244
13559
|
|
|
13245
13560
|
# ===== TAB 3: Language Pair Settings =====
|
|
13246
13561
|
lang_tab = self._create_language_pair_tab()
|
|
@@ -13760,6 +14075,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
13760
14075
|
full_context_cb.stateChanged.connect(lambda: context_slider.setEnabled(full_context_cb.isChecked()))
|
|
13761
14076
|
update_context_label(context_window_size)
|
|
13762
14077
|
|
|
14078
|
+
prefs_layout.addSpacing(10)
|
|
14079
|
+
|
|
14080
|
+
# QuickMenu document context
|
|
14081
|
+
quickmenu_context_label = QLabel("<b>QuickMenu Document Context:</b>")
|
|
14082
|
+
prefs_layout.addWidget(quickmenu_context_label)
|
|
14083
|
+
|
|
14084
|
+
quickmenu_context_percent = general_prefs.get('quickmenu_context_percent', 50)
|
|
14085
|
+
quickmenu_context_layout = QHBoxLayout()
|
|
14086
|
+
quickmenu_context_layout.addWidget(QLabel(" Document context size:"))
|
|
14087
|
+
quickmenu_context_slider = QSlider(Qt.Orientation.Horizontal)
|
|
14088
|
+
quickmenu_context_slider.setMinimum(0)
|
|
14089
|
+
quickmenu_context_slider.setMaximum(100)
|
|
14090
|
+
quickmenu_context_slider.setValue(quickmenu_context_percent)
|
|
14091
|
+
quickmenu_context_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
14092
|
+
quickmenu_context_slider.setTickInterval(10)
|
|
14093
|
+
quickmenu_context_layout.addWidget(quickmenu_context_slider)
|
|
14094
|
+
quickmenu_context_value_label = QLabel(f"{quickmenu_context_percent}%")
|
|
14095
|
+
quickmenu_context_value_label.setMinimumWidth(60)
|
|
14096
|
+
quickmenu_context_layout.addWidget(quickmenu_context_value_label)
|
|
14097
|
+
quickmenu_context_layout.addStretch()
|
|
14098
|
+
prefs_layout.addLayout(quickmenu_context_layout)
|
|
14099
|
+
|
|
14100
|
+
quickmenu_context_info = QLabel(
|
|
14101
|
+
" ⓘ When using {{SOURCE+TARGET_CONTEXT}} or {{SOURCE_CONTEXT}} placeholders in QuickMenu prompts.\n"
|
|
14102
|
+
" 0% = disabled, 50% = half the document (default), 100% = entire document.\n"
|
|
14103
|
+
" Limit: maximum 100 segments for performance."
|
|
14104
|
+
)
|
|
14105
|
+
quickmenu_context_info.setStyleSheet("font-size: 9pt; color: #666; padding-left: 20px;")
|
|
14106
|
+
quickmenu_context_info.setWordWrap(True)
|
|
14107
|
+
prefs_layout.addWidget(quickmenu_context_info)
|
|
14108
|
+
|
|
14109
|
+
def update_quickmenu_context_label(value):
|
|
14110
|
+
if value == 0:
|
|
14111
|
+
quickmenu_context_value_label.setText("0% (disabled)")
|
|
14112
|
+
else:
|
|
14113
|
+
quickmenu_context_value_label.setText(f"{value}%")
|
|
14114
|
+
|
|
14115
|
+
quickmenu_context_slider.valueChanged.connect(update_quickmenu_context_label)
|
|
14116
|
+
|
|
13763
14117
|
prefs_layout.addSpacing(5)
|
|
13764
14118
|
|
|
13765
14119
|
# Check TM before API call
|
|
@@ -13904,7 +14258,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
13904
14258
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
13905
14259
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
13906
14260
|
ollama_keepwarm_cb,
|
|
13907
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
14261
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
14262
|
+
quickmenu_context_slider
|
|
13908
14263
|
))
|
|
13909
14264
|
layout.addWidget(save_btn)
|
|
13910
14265
|
|
|
@@ -16902,7 +17257,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
16902
17257
|
batch_size_spin, surrounding_spin, full_context_cb, context_slider,
|
|
16903
17258
|
check_tm_cb, auto_propagate_cb, delay_spin,
|
|
16904
17259
|
ollama_keepwarm_cb,
|
|
16905
|
-
llm_matching_cb, auto_markdown_cb, llm_spin
|
|
17260
|
+
llm_matching_cb, auto_markdown_cb, llm_spin,
|
|
17261
|
+
quickmenu_context_slider):
|
|
16906
17262
|
"""Save all AI settings from the unified AI Settings tab"""
|
|
16907
17263
|
# Determine selected provider
|
|
16908
17264
|
if openai_radio.isChecked():
|
|
@@ -16961,6 +17317,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
16961
17317
|
general_prefs['surrounding_segments'] = surrounding_spin.value()
|
|
16962
17318
|
general_prefs['use_full_context'] = full_context_cb.isChecked()
|
|
16963
17319
|
general_prefs['context_window_size'] = context_slider.value()
|
|
17320
|
+
general_prefs['quickmenu_context_percent'] = quickmenu_context_slider.value()
|
|
16964
17321
|
general_prefs['check_tm_before_api'] = check_tm_cb.isChecked()
|
|
16965
17322
|
general_prefs['auto_propagate_100'] = auto_propagate_cb.isChecked()
|
|
16966
17323
|
general_prefs['lookup_delay'] = delay_spin.value()
|
|
@@ -18502,7 +18859,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
18502
18859
|
header.setStretchLastSection(False) # Don't auto-stretch last section (we use Stretch mode for Source/Target)
|
|
18503
18860
|
|
|
18504
18861
|
# Set initial column widths - give Source and Target equal space
|
|
18505
|
-
self.table.setColumnWidth(0,
|
|
18862
|
+
self.table.setColumnWidth(0, 40) # ID - compact, fits up to 3 digits comfortably
|
|
18506
18863
|
self.table.setColumnWidth(1, 40) # Type - narrower
|
|
18507
18864
|
self.table.setColumnWidth(2, 400) # Source
|
|
18508
18865
|
self.table.setColumnWidth(3, 400) # Target
|
|
@@ -20922,6 +21279,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
20922
21279
|
# Initialize TM for this project
|
|
20923
21280
|
self.initialize_tm_database()
|
|
20924
21281
|
|
|
21282
|
+
# Deactivate all resources for new project (user explicitly activates what they need)
|
|
21283
|
+
self._deactivate_all_resources_for_new_project()
|
|
21284
|
+
|
|
20925
21285
|
# Auto-resize rows for better initial display
|
|
20926
21286
|
self.auto_resize_rows()
|
|
20927
21287
|
|
|
@@ -21670,6 +22030,493 @@ class SupervertalerQt(QMainWindow):
|
|
|
21670
22030
|
"Export Error",
|
|
21671
22031
|
f"Failed to export AI format:\n\n{str(e)}"
|
|
21672
22032
|
)
|
|
22033
|
+
|
|
22034
|
+
def export_bilingual_table_markdown(self):
|
|
22035
|
+
"""Export current project segments as Markdown table (for AI systems)"""
|
|
22036
|
+
try:
|
|
22037
|
+
if not self.current_project or not self.current_project.segments:
|
|
22038
|
+
QMessageBox.warning(self, "No Project", "Please open a project with segments first")
|
|
22039
|
+
return
|
|
22040
|
+
|
|
22041
|
+
segments = list(self.current_project.segments)
|
|
22042
|
+
|
|
22043
|
+
# Get language names
|
|
22044
|
+
source_lang = self.current_project.source_lang or "Source"
|
|
22045
|
+
target_lang = self.current_project.target_lang or "Target"
|
|
22046
|
+
|
|
22047
|
+
# Check translation status
|
|
22048
|
+
translated_count = sum(1 for seg in segments if seg.target and seg.target.strip())
|
|
22049
|
+
total_count = len(segments)
|
|
22050
|
+
|
|
22051
|
+
# Show export options dialog
|
|
22052
|
+
dialog = QDialog(self)
|
|
22053
|
+
dialog.setWindowTitle("Export as Markdown Table")
|
|
22054
|
+
dialog.setMinimumWidth(500)
|
|
22055
|
+
|
|
22056
|
+
layout = QVBoxLayout(dialog)
|
|
22057
|
+
|
|
22058
|
+
# Info label
|
|
22059
|
+
info_label = QLabel(
|
|
22060
|
+
"Export segments as a <b>Markdown table</b> optimized for AI systems.\n"
|
|
22061
|
+
"The table format renders beautifully in ChatGPT, Claude, and all AI chat interfaces."
|
|
22062
|
+
)
|
|
22063
|
+
info_label.setWordWrap(True)
|
|
22064
|
+
info_label.setTextFormat(Qt.TextFormat.RichText)
|
|
22065
|
+
layout.addWidget(info_label)
|
|
22066
|
+
|
|
22067
|
+
layout.addSpacing(10)
|
|
22068
|
+
|
|
22069
|
+
# Status info
|
|
22070
|
+
status_label = QLabel(
|
|
22071
|
+
f"📊 <b>{translated_count} of {total_count}</b> segments have translations."
|
|
22072
|
+
if translated_count > 0
|
|
22073
|
+
else f"📊 <b>{total_count}</b> segments (none translated yet)."
|
|
22074
|
+
)
|
|
22075
|
+
status_label.setTextFormat(Qt.TextFormat.RichText)
|
|
22076
|
+
layout.addWidget(status_label)
|
|
22077
|
+
|
|
22078
|
+
layout.addSpacing(10)
|
|
22079
|
+
|
|
22080
|
+
# Content options
|
|
22081
|
+
content_group = QGroupBox("Content Options")
|
|
22082
|
+
content_layout = QVBoxLayout(content_group)
|
|
22083
|
+
|
|
22084
|
+
include_both_radio = CheckmarkRadioButton(f"Include both {source_lang} and {target_lang} (bilingual table)")
|
|
22085
|
+
include_both_radio.setChecked(True)
|
|
22086
|
+
source_only_radio = CheckmarkRadioButton(f"{source_lang} only (for AI translation)")
|
|
22087
|
+
target_only_radio = CheckmarkRadioButton(f"{target_lang} only (translated segments)")
|
|
22088
|
+
|
|
22089
|
+
content_layout.addWidget(include_both_radio)
|
|
22090
|
+
content_layout.addWidget(source_only_radio)
|
|
22091
|
+
content_layout.addWidget(target_only_radio)
|
|
22092
|
+
layout.addWidget(content_group)
|
|
22093
|
+
|
|
22094
|
+
# Filter options
|
|
22095
|
+
filter_group = QGroupBox("Segment Filter")
|
|
22096
|
+
filter_layout = QVBoxLayout(filter_group)
|
|
22097
|
+
|
|
22098
|
+
all_segments_radio = CheckmarkRadioButton("All segments")
|
|
22099
|
+
all_segments_radio.setChecked(True)
|
|
22100
|
+
untranslated_radio = CheckmarkRadioButton("Untranslated segments only (empty target)")
|
|
22101
|
+
translated_radio = CheckmarkRadioButton("Translated segments only (has target)")
|
|
22102
|
+
|
|
22103
|
+
filter_layout.addWidget(all_segments_radio)
|
|
22104
|
+
filter_layout.addWidget(untranslated_radio)
|
|
22105
|
+
filter_layout.addWidget(translated_radio)
|
|
22106
|
+
layout.addWidget(filter_group)
|
|
22107
|
+
|
|
22108
|
+
layout.addSpacing(10)
|
|
22109
|
+
|
|
22110
|
+
# Preview
|
|
22111
|
+
preview_group = QGroupBox("Preview")
|
|
22112
|
+
preview_layout = QVBoxLayout(preview_group)
|
|
22113
|
+
preview_text = QTextEdit()
|
|
22114
|
+
preview_text.setReadOnly(True)
|
|
22115
|
+
preview_text.setMaximumHeight(150)
|
|
22116
|
+
preview_text.setStyleSheet("font-family: Consolas, monospace; font-size: 10px;")
|
|
22117
|
+
preview_layout.addWidget(preview_text)
|
|
22118
|
+
layout.addWidget(preview_group)
|
|
22119
|
+
|
|
22120
|
+
def update_preview():
|
|
22121
|
+
# Show preview with sample data
|
|
22122
|
+
if include_both_radio.isChecked():
|
|
22123
|
+
preview = f"# Translation Project: {self.current_project.name or 'Untitled'}\n\n"
|
|
22124
|
+
preview += f"| Segment | {source_lang} | {target_lang} |\n"
|
|
22125
|
+
preview += "|---------|" + "-" * (len(source_lang) + 2) + "|" + "-" * (len(target_lang) + 2) + "|\n"
|
|
22126
|
+
preview += "| 1 | Source text example... | Target text example... |\n"
|
|
22127
|
+
preview += "| 2 | Another segment... | Translation here... |"
|
|
22128
|
+
elif source_only_radio.isChecked():
|
|
22129
|
+
preview = f"# Translation Project: {self.current_project.name or 'Untitled'}\n\n"
|
|
22130
|
+
preview += f"| Segment | {source_lang} |\n"
|
|
22131
|
+
preview += "|---------|" + "-" * (len(source_lang) + 2) + "|\n"
|
|
22132
|
+
preview += "| 1 | Source text example... |\n"
|
|
22133
|
+
preview += "| 2 | Another segment... |"
|
|
22134
|
+
else:
|
|
22135
|
+
preview = f"# Translation Project: {self.current_project.name or 'Untitled'}\n\n"
|
|
22136
|
+
preview += f"| Segment | {target_lang} |\n"
|
|
22137
|
+
preview += "|---------|" + "-" * (len(target_lang) + 2) + "|\n"
|
|
22138
|
+
preview += "| 1 | Target text example... |\n"
|
|
22139
|
+
preview += "| 2 | Translation here... |"
|
|
22140
|
+
|
|
22141
|
+
preview_text.setPlainText(preview)
|
|
22142
|
+
|
|
22143
|
+
# Connect signals for live preview
|
|
22144
|
+
include_both_radio.toggled.connect(update_preview)
|
|
22145
|
+
source_only_radio.toggled.connect(update_preview)
|
|
22146
|
+
target_only_radio.toggled.connect(update_preview)
|
|
22147
|
+
|
|
22148
|
+
update_preview() # Initial preview
|
|
22149
|
+
|
|
22150
|
+
layout.addSpacing(10)
|
|
22151
|
+
|
|
22152
|
+
# Buttons
|
|
22153
|
+
button_layout = QHBoxLayout()
|
|
22154
|
+
ok_btn = QPushButton("Export")
|
|
22155
|
+
ok_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; border: none; outline: none;")
|
|
22156
|
+
ok_btn.clicked.connect(dialog.accept)
|
|
22157
|
+
cancel_btn = QPushButton("Cancel")
|
|
22158
|
+
cancel_btn.clicked.connect(dialog.reject)
|
|
22159
|
+
|
|
22160
|
+
button_layout.addStretch()
|
|
22161
|
+
button_layout.addWidget(cancel_btn)
|
|
22162
|
+
button_layout.addWidget(ok_btn)
|
|
22163
|
+
layout.addLayout(button_layout)
|
|
22164
|
+
|
|
22165
|
+
if dialog.exec() != QDialog.DialogCode.Accepted:
|
|
22166
|
+
return
|
|
22167
|
+
|
|
22168
|
+
# Determine content mode
|
|
22169
|
+
if source_only_radio.isChecked():
|
|
22170
|
+
content_mode = "source_only"
|
|
22171
|
+
elif target_only_radio.isChecked():
|
|
22172
|
+
content_mode = "target_only"
|
|
22173
|
+
else:
|
|
22174
|
+
content_mode = "bilingual"
|
|
22175
|
+
|
|
22176
|
+
# Filter segments
|
|
22177
|
+
if untranslated_radio.isChecked():
|
|
22178
|
+
filtered_segments = [s for s in segments if not (s.target and s.target.strip())]
|
|
22179
|
+
elif translated_radio.isChecked():
|
|
22180
|
+
filtered_segments = [s for s in segments if s.target and s.target.strip()]
|
|
22181
|
+
else:
|
|
22182
|
+
filtered_segments = segments
|
|
22183
|
+
|
|
22184
|
+
if not filtered_segments:
|
|
22185
|
+
QMessageBox.warning(
|
|
22186
|
+
self, "No Segments",
|
|
22187
|
+
"No segments match the selected filter criteria."
|
|
22188
|
+
)
|
|
22189
|
+
return
|
|
22190
|
+
|
|
22191
|
+
# Get save path
|
|
22192
|
+
default_name = ""
|
|
22193
|
+
if self.current_project.name:
|
|
22194
|
+
default_name = self.current_project.name.replace(" ", "_") + "_table.md"
|
|
22195
|
+
|
|
22196
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
22197
|
+
self,
|
|
22198
|
+
"Export Markdown Table",
|
|
22199
|
+
default_name,
|
|
22200
|
+
"Markdown Files (*.md);;All Files (*.*)"
|
|
22201
|
+
)
|
|
22202
|
+
|
|
22203
|
+
if not file_path:
|
|
22204
|
+
return
|
|
22205
|
+
|
|
22206
|
+
# Ensure .md extension
|
|
22207
|
+
if not file_path.lower().endswith('.md'):
|
|
22208
|
+
file_path += '.md'
|
|
22209
|
+
|
|
22210
|
+
# Build Markdown table
|
|
22211
|
+
output_lines = []
|
|
22212
|
+
output_lines.append(f"# Translation Project: {self.current_project.name or 'Untitled'}")
|
|
22213
|
+
output_lines.append("")
|
|
22214
|
+
output_lines.append(f"**Languages:** {source_lang} → {target_lang}")
|
|
22215
|
+
output_lines.append(f"**Segments:** {len(filtered_segments)}")
|
|
22216
|
+
output_lines.append(f"**Exported:** {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
|
22217
|
+
output_lines.append("")
|
|
22218
|
+
|
|
22219
|
+
# Table header
|
|
22220
|
+
if content_mode == "bilingual":
|
|
22221
|
+
output_lines.append(f"| Segment | {source_lang} | {target_lang} |")
|
|
22222
|
+
output_lines.append("|---------|" + "-" * (len(source_lang) + 2) + "|" + "-" * (len(target_lang) + 2) + "|")
|
|
22223
|
+
elif content_mode == "source_only":
|
|
22224
|
+
output_lines.append(f"| Segment | {source_lang} |")
|
|
22225
|
+
output_lines.append("|---------|" + "-" * (len(source_lang) + 2) + "|")
|
|
22226
|
+
else: # target_only
|
|
22227
|
+
output_lines.append(f"| Segment | {target_lang} |")
|
|
22228
|
+
output_lines.append("|---------|" + "-" * (len(target_lang) + 2) + "|")
|
|
22229
|
+
|
|
22230
|
+
# Table rows
|
|
22231
|
+
for i, seg in enumerate(filtered_segments, 1):
|
|
22232
|
+
# Escape pipe characters in text for Markdown tables
|
|
22233
|
+
source_text = seg.source.replace("|", "\\|").replace("\n", " ")
|
|
22234
|
+
target_text = (seg.target or "").replace("|", "\\|").replace("\n", " ")
|
|
22235
|
+
|
|
22236
|
+
if content_mode == "bilingual":
|
|
22237
|
+
output_lines.append(f"| {i} | {source_text} | {target_text} |")
|
|
22238
|
+
elif content_mode == "source_only":
|
|
22239
|
+
output_lines.append(f"| {i} | {source_text} |")
|
|
22240
|
+
else: # target_only
|
|
22241
|
+
output_lines.append(f"| {i} | {target_text} |")
|
|
22242
|
+
|
|
22243
|
+
# Write file
|
|
22244
|
+
with open(file_path, 'w', encoding='utf-8', newline='\n') as f:
|
|
22245
|
+
f.write('\n'.join(output_lines))
|
|
22246
|
+
|
|
22247
|
+
self.log(f"✓ Exported {len(filtered_segments)} segments as Markdown table to: {os.path.basename(file_path)}")
|
|
22248
|
+
|
|
22249
|
+
QMessageBox.information(
|
|
22250
|
+
self,
|
|
22251
|
+
"Export Complete",
|
|
22252
|
+
f"Successfully exported {len(filtered_segments)} segments as Markdown table to:\n\n{os.path.basename(file_path)}\n\n"
|
|
22253
|
+
f"This format renders beautifully in ChatGPT, Claude, and other AI chat interfaces!"
|
|
22254
|
+
)
|
|
22255
|
+
|
|
22256
|
+
except Exception as e:
|
|
22257
|
+
self.log(f"✗ Markdown table export failed: {str(e)}")
|
|
22258
|
+
QMessageBox.critical(
|
|
22259
|
+
self,
|
|
22260
|
+
"Export Error",
|
|
22261
|
+
f"Failed to export Markdown table:\n\n{str(e)}"
|
|
22262
|
+
)
|
|
22263
|
+
|
|
22264
|
+
def convert_document_to_markdown(self):
|
|
22265
|
+
"""Convert a single DOCX or TXT document to Markdown format, preserving structure"""
|
|
22266
|
+
try:
|
|
22267
|
+
# Select input file
|
|
22268
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
22269
|
+
self,
|
|
22270
|
+
"Select Document to Convert",
|
|
22271
|
+
"",
|
|
22272
|
+
"Documents (*.docx *.txt);;All Files (*.*)"
|
|
22273
|
+
)
|
|
22274
|
+
|
|
22275
|
+
if not file_path:
|
|
22276
|
+
return
|
|
22277
|
+
|
|
22278
|
+
# Determine output path
|
|
22279
|
+
base_name = os.path.splitext(file_path)[0]
|
|
22280
|
+
output_path, _ = QFileDialog.getSaveFileName(
|
|
22281
|
+
self,
|
|
22282
|
+
"Save Markdown File",
|
|
22283
|
+
base_name + ".md",
|
|
22284
|
+
"Markdown Files (*.md);;All Files (*.*)"
|
|
22285
|
+
)
|
|
22286
|
+
|
|
22287
|
+
if not output_path:
|
|
22288
|
+
return
|
|
22289
|
+
|
|
22290
|
+
# Ensure .md extension
|
|
22291
|
+
if not output_path.lower().endswith('.md'):
|
|
22292
|
+
output_path += '.md'
|
|
22293
|
+
|
|
22294
|
+
# Convert based on file type
|
|
22295
|
+
if file_path.lower().endswith('.docx'):
|
|
22296
|
+
self._convert_docx_to_markdown(file_path, output_path)
|
|
22297
|
+
else: # TXT
|
|
22298
|
+
self._convert_txt_to_markdown(file_path, output_path)
|
|
22299
|
+
|
|
22300
|
+
self.log(f"✓ Converted {os.path.basename(file_path)} → {os.path.basename(output_path)}")
|
|
22301
|
+
|
|
22302
|
+
QMessageBox.information(
|
|
22303
|
+
self,
|
|
22304
|
+
"Conversion Complete",
|
|
22305
|
+
f"Successfully converted document to Markdown:\n\n{os.path.basename(output_path)}"
|
|
22306
|
+
)
|
|
22307
|
+
|
|
22308
|
+
except Exception as e:
|
|
22309
|
+
self.log(f"✗ Document conversion failed: {str(e)}")
|
|
22310
|
+
QMessageBox.critical(
|
|
22311
|
+
self,
|
|
22312
|
+
"Conversion Error",
|
|
22313
|
+
f"Failed to convert document:\n\n{str(e)}"
|
|
22314
|
+
)
|
|
22315
|
+
|
|
22316
|
+
def batch_convert_documents_to_markdown(self):
|
|
22317
|
+
"""Batch convert multiple documents to Markdown format"""
|
|
22318
|
+
try:
|
|
22319
|
+
# Select multiple files
|
|
22320
|
+
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
22321
|
+
self,
|
|
22322
|
+
"Select Documents to Convert",
|
|
22323
|
+
"",
|
|
22324
|
+
"Documents (*.docx *.txt);;All Files (*.*)"
|
|
22325
|
+
)
|
|
22326
|
+
|
|
22327
|
+
if not file_paths:
|
|
22328
|
+
return
|
|
22329
|
+
|
|
22330
|
+
# Select output folder
|
|
22331
|
+
output_folder = QFileDialog.getExistingDirectory(
|
|
22332
|
+
self,
|
|
22333
|
+
"Select Output Folder for Markdown Files",
|
|
22334
|
+
"",
|
|
22335
|
+
QFileDialog.Option.ShowDirsOnly
|
|
22336
|
+
)
|
|
22337
|
+
|
|
22338
|
+
if not output_folder:
|
|
22339
|
+
return
|
|
22340
|
+
|
|
22341
|
+
# Progress dialog
|
|
22342
|
+
progress = QProgressDialog(
|
|
22343
|
+
"Converting documents to Markdown...",
|
|
22344
|
+
"Cancel",
|
|
22345
|
+
0,
|
|
22346
|
+
len(file_paths),
|
|
22347
|
+
self
|
|
22348
|
+
)
|
|
22349
|
+
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
22350
|
+
progress.setMinimumDuration(0)
|
|
22351
|
+
|
|
22352
|
+
converted_count = 0
|
|
22353
|
+
failed_count = 0
|
|
22354
|
+
|
|
22355
|
+
for i, file_path in enumerate(file_paths):
|
|
22356
|
+
if progress.wasCanceled():
|
|
22357
|
+
break
|
|
22358
|
+
|
|
22359
|
+
progress.setValue(i)
|
|
22360
|
+
progress.setLabelText(f"Converting: {os.path.basename(file_path)}")
|
|
22361
|
+
QApplication.processEvents()
|
|
22362
|
+
|
|
22363
|
+
try:
|
|
22364
|
+
# Determine output path
|
|
22365
|
+
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
22366
|
+
output_path = os.path.join(output_folder, base_name + ".md")
|
|
22367
|
+
|
|
22368
|
+
# Convert based on file type
|
|
22369
|
+
if file_path.lower().endswith('.docx'):
|
|
22370
|
+
self._convert_docx_to_markdown(file_path, output_path)
|
|
22371
|
+
else: # TXT
|
|
22372
|
+
self._convert_txt_to_markdown(file_path, output_path)
|
|
22373
|
+
|
|
22374
|
+
converted_count += 1
|
|
22375
|
+
self.log(f"✓ Converted: {os.path.basename(file_path)}")
|
|
22376
|
+
|
|
22377
|
+
except Exception as e:
|
|
22378
|
+
failed_count += 1
|
|
22379
|
+
self.log(f"✗ Failed: {os.path.basename(file_path)} - {str(e)}")
|
|
22380
|
+
|
|
22381
|
+
progress.setValue(len(file_paths))
|
|
22382
|
+
|
|
22383
|
+
# Show summary
|
|
22384
|
+
summary = f"Batch conversion complete!\n\n"
|
|
22385
|
+
summary += f"✓ Successfully converted: {converted_count} files\n"
|
|
22386
|
+
if failed_count > 0:
|
|
22387
|
+
summary += f"✗ Failed: {failed_count} files\n"
|
|
22388
|
+
summary += f"\nOutput folder: {output_folder}"
|
|
22389
|
+
|
|
22390
|
+
QMessageBox.information(
|
|
22391
|
+
self,
|
|
22392
|
+
"Batch Conversion Complete",
|
|
22393
|
+
summary
|
|
22394
|
+
)
|
|
22395
|
+
|
|
22396
|
+
except Exception as e:
|
|
22397
|
+
self.log(f"✗ Batch conversion failed: {str(e)}")
|
|
22398
|
+
QMessageBox.critical(
|
|
22399
|
+
self,
|
|
22400
|
+
"Conversion Error",
|
|
22401
|
+
f"Failed to perform batch conversion:\n\n{str(e)}"
|
|
22402
|
+
)
|
|
22403
|
+
|
|
22404
|
+
def _convert_docx_to_markdown(self, docx_path: str, output_path: str):
|
|
22405
|
+
"""Convert DOCX document to Markdown, preserving structure"""
|
|
22406
|
+
from docx import Document
|
|
22407
|
+
|
|
22408
|
+
doc = Document(docx_path)
|
|
22409
|
+
markdown_lines = []
|
|
22410
|
+
|
|
22411
|
+
# Add document title
|
|
22412
|
+
doc_name = os.path.splitext(os.path.basename(docx_path))[0]
|
|
22413
|
+
markdown_lines.append(f"# {doc_name}")
|
|
22414
|
+
markdown_lines.append("")
|
|
22415
|
+
|
|
22416
|
+
for para in doc.paragraphs:
|
|
22417
|
+
if not para.text.strip():
|
|
22418
|
+
markdown_lines.append("")
|
|
22419
|
+
continue
|
|
22420
|
+
|
|
22421
|
+
# Check paragraph style
|
|
22422
|
+
style = para.style.name.lower()
|
|
22423
|
+
text = para.text.strip()
|
|
22424
|
+
|
|
22425
|
+
# Detect ALL CAPS headings (common in patent documents)
|
|
22426
|
+
words = text.split()
|
|
22427
|
+
is_all_caps = (
|
|
22428
|
+
len(words) >= 2 and
|
|
22429
|
+
all(word.isupper() or not word.isalpha() for word in words) and
|
|
22430
|
+
len(text) <= 100 # Reasonable heading length
|
|
22431
|
+
)
|
|
22432
|
+
|
|
22433
|
+
if 'heading 1' in style or style == 'title':
|
|
22434
|
+
markdown_lines.append(f"# {text}")
|
|
22435
|
+
elif 'heading 2' in style:
|
|
22436
|
+
markdown_lines.append(f"## {text}")
|
|
22437
|
+
elif 'heading 3' in style:
|
|
22438
|
+
markdown_lines.append(f"### {text}")
|
|
22439
|
+
elif 'heading 4' in style:
|
|
22440
|
+
markdown_lines.append(f"#### {text}")
|
|
22441
|
+
elif 'heading 5' in style:
|
|
22442
|
+
markdown_lines.append(f"##### {text}")
|
|
22443
|
+
elif 'heading 6' in style:
|
|
22444
|
+
markdown_lines.append(f"###### {text}")
|
|
22445
|
+
elif is_all_caps:
|
|
22446
|
+
# ALL CAPS line without heading style - treat as heading
|
|
22447
|
+
markdown_lines.append(f"## {text.title()}")
|
|
22448
|
+
elif 'list bullet' in style or 'bullet' in style:
|
|
22449
|
+
markdown_lines.append(f"- {text}")
|
|
22450
|
+
elif 'list number' in style or 'numbering' in style:
|
|
22451
|
+
markdown_lines.append(f"1. {text}")
|
|
22452
|
+
elif 'quote' in style or 'quotation' in style:
|
|
22453
|
+
markdown_lines.append(f"> {text}")
|
|
22454
|
+
else:
|
|
22455
|
+
# Regular paragraph - preserve bold/italic if present
|
|
22456
|
+
# Simple formatting detection (runs with bold/italic)
|
|
22457
|
+
has_bold = any(run.bold for run in para.runs if run.text.strip())
|
|
22458
|
+
has_italic = any(run.italic for run in para.runs if run.text.strip())
|
|
22459
|
+
|
|
22460
|
+
if has_bold and has_italic:
|
|
22461
|
+
markdown_lines.append(f"***{text}***")
|
|
22462
|
+
elif has_bold:
|
|
22463
|
+
markdown_lines.append(f"**{text}**")
|
|
22464
|
+
elif has_italic:
|
|
22465
|
+
markdown_lines.append(f"*{text}*")
|
|
22466
|
+
else:
|
|
22467
|
+
markdown_lines.append(text)
|
|
22468
|
+
|
|
22469
|
+
markdown_lines.append("")
|
|
22470
|
+
|
|
22471
|
+
# Write output
|
|
22472
|
+
with open(output_path, 'w', encoding='utf-8', newline='\n') as f:
|
|
22473
|
+
f.write('\n'.join(markdown_lines))
|
|
22474
|
+
|
|
22475
|
+
def _convert_txt_to_markdown(self, txt_path: str, output_path: str):
|
|
22476
|
+
"""Convert TXT document to Markdown with minimal formatting"""
|
|
22477
|
+
with open(txt_path, 'r', encoding='utf-8') as f:
|
|
22478
|
+
lines = f.readlines()
|
|
22479
|
+
|
|
22480
|
+
markdown_lines = []
|
|
22481
|
+
|
|
22482
|
+
# Add document title
|
|
22483
|
+
doc_name = os.path.splitext(os.path.basename(txt_path))[0]
|
|
22484
|
+
markdown_lines.append(f"# {doc_name}")
|
|
22485
|
+
markdown_lines.append("")
|
|
22486
|
+
|
|
22487
|
+
# Process lines - detect simple patterns
|
|
22488
|
+
for line in lines:
|
|
22489
|
+
stripped = line.strip()
|
|
22490
|
+
|
|
22491
|
+
if not stripped:
|
|
22492
|
+
markdown_lines.append("")
|
|
22493
|
+
continue
|
|
22494
|
+
|
|
22495
|
+
# Detect ALL CAPS headings (common in plain text documents)
|
|
22496
|
+
# Must be at least 3 words long and all uppercase
|
|
22497
|
+
words = stripped.split()
|
|
22498
|
+
is_all_caps = (
|
|
22499
|
+
len(words) >= 2 and
|
|
22500
|
+
all(word.isupper() or not word.isalpha() for word in words) and
|
|
22501
|
+
len(stripped) <= 100 # Reasonable heading length
|
|
22502
|
+
)
|
|
22503
|
+
|
|
22504
|
+
if is_all_caps:
|
|
22505
|
+
# Convert to title case and make it a heading
|
|
22506
|
+
markdown_lines.append(f"## {stripped.title()}")
|
|
22507
|
+
# Detect simple bullet points
|
|
22508
|
+
elif stripped.startswith('- ') or stripped.startswith('* '):
|
|
22509
|
+
markdown_lines.append(stripped)
|
|
22510
|
+
# Detect numbered lists
|
|
22511
|
+
elif len(stripped) > 2 and stripped[0].isdigit() and stripped[1] in '.):':
|
|
22512
|
+
markdown_lines.append(stripped)
|
|
22513
|
+
# Regular text
|
|
22514
|
+
else:
|
|
22515
|
+
markdown_lines.append(stripped)
|
|
22516
|
+
|
|
22517
|
+
# Write output
|
|
22518
|
+
with open(output_path, 'w', encoding='utf-8', newline='\n') as f:
|
|
22519
|
+
f.write('\n'.join(markdown_lines))
|
|
21673
22520
|
|
|
21674
22521
|
# ========================================================================
|
|
21675
22522
|
# MULTI-FILE FOLDER IMPORT
|
|
@@ -22912,6 +23759,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
22912
23759
|
self.load_segments_to_grid()
|
|
22913
23760
|
self.initialize_tm_database()
|
|
22914
23761
|
|
|
23762
|
+
# Deactivate all resources for new project (user explicitly activates what they need)
|
|
23763
|
+
self._deactivate_all_resources_for_new_project()
|
|
23764
|
+
|
|
22915
23765
|
# Auto-resize rows for better initial display
|
|
22916
23766
|
self.auto_resize_rows()
|
|
22917
23767
|
|
|
@@ -23406,6 +24256,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
23406
24256
|
self.load_segments_to_grid()
|
|
23407
24257
|
self.initialize_tm_database()
|
|
23408
24258
|
|
|
24259
|
+
# Deactivate all resources for new project (user explicitly activates what they need)
|
|
24260
|
+
self._deactivate_all_resources_for_new_project()
|
|
24261
|
+
|
|
23409
24262
|
# Auto-resize rows for better initial display
|
|
23410
24263
|
self.auto_resize_rows()
|
|
23411
24264
|
|
|
@@ -23672,6 +24525,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
23672
24525
|
self.load_segments_to_grid()
|
|
23673
24526
|
self.initialize_tm_database()
|
|
23674
24527
|
|
|
24528
|
+
# Deactivate all resources for new project (user explicitly activates what they need)
|
|
24529
|
+
self._deactivate_all_resources_for_new_project()
|
|
24530
|
+
|
|
23675
24531
|
# Auto-resize rows for better initial display
|
|
23676
24532
|
self.auto_resize_rows()
|
|
23677
24533
|
|
|
@@ -23881,6 +24737,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
23881
24737
|
self.load_segments_to_grid()
|
|
23882
24738
|
self.initialize_tm_database()
|
|
23883
24739
|
|
|
24740
|
+
# Deactivate all resources for new project (user explicitly activates what they need)
|
|
24741
|
+
self._deactivate_all_resources_for_new_project()
|
|
24742
|
+
|
|
23884
24743
|
# Auto-resize rows for better initial display
|
|
23885
24744
|
self.auto_resize_rows()
|
|
23886
24745
|
|
|
@@ -27508,6 +28367,20 @@ class SupervertalerQt(QMainWindow):
|
|
|
27508
28367
|
# Full processing for non-arrow-key navigation (click, etc.)
|
|
27509
28368
|
self._on_cell_selected_full(current_row, current_col, previous_row, previous_col)
|
|
27510
28369
|
|
|
28370
|
+
def _center_row_in_viewport(self, row: int):
|
|
28371
|
+
"""Center the given row vertically in the visible table viewport.
|
|
28372
|
+
|
|
28373
|
+
Uses Qt's built-in scrollTo() with PositionAtCenter hint.
|
|
28374
|
+
"""
|
|
28375
|
+
if row < 0 or row >= self.table.rowCount():
|
|
28376
|
+
return
|
|
28377
|
+
|
|
28378
|
+
# Get the model index for any cell in this row (use column 0)
|
|
28379
|
+
index = self.table.model().index(row, 0)
|
|
28380
|
+
if index.isValid():
|
|
28381
|
+
# Use Qt's built-in centering - PositionAtCenter puts the item in the center of the viewport
|
|
28382
|
+
self.table.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter)
|
|
28383
|
+
|
|
27511
28384
|
def _on_cell_selected_minimal(self, current_row, previous_row):
|
|
27512
28385
|
"""Minimal UI update for fast arrow key navigation - just highlight and scroll"""
|
|
27513
28386
|
try:
|
|
@@ -27529,7 +28402,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
27529
28402
|
|
|
27530
28403
|
# Auto-center if enabled
|
|
27531
28404
|
if getattr(self, 'auto_center_active_segment', False) and not getattr(self, 'filtering_active', False):
|
|
27532
|
-
self.
|
|
28405
|
+
self._center_row_in_viewport(current_row)
|
|
27533
28406
|
except Exception as e:
|
|
27534
28407
|
if self.debug_mode_enabled:
|
|
27535
28408
|
self.log(f"Error in minimal cell selection: {e}")
|
|
@@ -27574,7 +28447,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
27574
28447
|
current_id_item.setForeground(QColor("white"))
|
|
27575
28448
|
|
|
27576
28449
|
if getattr(self, 'auto_center_active_segment', False) and not getattr(self, 'filtering_active', False):
|
|
27577
|
-
self.
|
|
28450
|
+
self._center_row_in_viewport(current_row)
|
|
27578
28451
|
|
|
27579
28452
|
if not self.current_project or current_row < 0:
|
|
27580
28453
|
return
|
|
@@ -29800,6 +30673,35 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
29800
30673
|
except Exception as e:
|
|
29801
30674
|
self.log(f"⚠ Could not initialize spellcheck: {e}")
|
|
29802
30675
|
|
|
30676
|
+
def _deactivate_all_resources_for_new_project(self):
|
|
30677
|
+
"""Deactivate all TMs, termbases, and NT lists for a freshly imported project.
|
|
30678
|
+
This ensures new projects start with clean slate - user explicitly activates what they need."""
|
|
30679
|
+
if not self.current_project or not hasattr(self.current_project, 'id'):
|
|
30680
|
+
return
|
|
30681
|
+
|
|
30682
|
+
project_id = self.current_project.id
|
|
30683
|
+
if not project_id:
|
|
30684
|
+
return
|
|
30685
|
+
|
|
30686
|
+
# Deactivate all TMs for this project
|
|
30687
|
+
if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr:
|
|
30688
|
+
all_tms = self.tm_metadata_mgr.get_all_tms()
|
|
30689
|
+
for tm in all_tms:
|
|
30690
|
+
self.tm_metadata_mgr.deactivate_tm(tm['id'], project_id)
|
|
30691
|
+
|
|
30692
|
+
# Deactivate all termbases for this project
|
|
30693
|
+
if hasattr(self, 'termbase_mgr') and self.termbase_mgr:
|
|
30694
|
+
all_termbases = self.termbase_mgr.get_all_termbases()
|
|
30695
|
+
for tb in all_termbases:
|
|
30696
|
+
self.termbase_mgr.deactivate_termbase(tb['id'], project_id)
|
|
30697
|
+
|
|
30698
|
+
# Deactivate all NT lists
|
|
30699
|
+
if hasattr(self, 'nt_manager') and self.nt_manager:
|
|
30700
|
+
for list_name in list(self.nt_manager.lists.keys()):
|
|
30701
|
+
self.nt_manager.set_list_active(list_name, False)
|
|
30702
|
+
|
|
30703
|
+
self.log("📋 New project: All TMs, glossaries, and NT lists deactivated (start clean)")
|
|
30704
|
+
|
|
29803
30705
|
def search_and_display_tm_matches(self, source_text: str):
|
|
29804
30706
|
"""Search TM and Termbases and display matches with visual diff for fuzzy matches"""
|
|
29805
30707
|
self.log(f"🚨 search_and_display_tm_matches called with source_text: '{source_text[:50]}...'")
|
|
@@ -30315,11 +31217,18 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
30315
31217
|
normalized_source = clean_source_lower
|
|
30316
31218
|
for quote_char in '\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A':
|
|
30317
31219
|
normalized_source = normalized_source.replace(quote_char, ' ')
|
|
31220
|
+
|
|
31221
|
+
# CRITICAL FIX: Strip trailing punctuation from glossary term before matching
|
|
31222
|
+
# This allows entries like "De huidige uitvinding...problemen." (with period)
|
|
31223
|
+
# to match source text "...problemen." where tokenization strips the period
|
|
31224
|
+
# Strip from both ends to handle quotes and trailing punctuation
|
|
31225
|
+
normalized_term = source_term.lower().rstrip(PUNCT_CHARS).lstrip(PUNCT_CHARS)
|
|
31226
|
+
|
|
30318
31227
|
# Check if term has punctuation - use different pattern
|
|
30319
|
-
if any(char in
|
|
30320
|
-
pattern = re.compile(r'(?<!\w)' + re.escape(
|
|
31228
|
+
if any(char in normalized_term for char in ['.', '%', ',', '-', '/']):
|
|
31229
|
+
pattern = re.compile(r'(?<!\w)' + re.escape(normalized_term) + r'(?!\w)')
|
|
30321
31230
|
else:
|
|
30322
|
-
pattern = re.compile(r"\b" + re.escape(
|
|
31231
|
+
pattern = re.compile(r"\b" + re.escape(normalized_term) + r"\b")
|
|
30323
31232
|
|
|
30324
31233
|
# Try matching on normalized (tag-stripped, quote-stripped) text first,
|
|
30325
31234
|
# then tag-stripped, then original with tags
|
|
@@ -30776,7 +31685,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
30776
31685
|
self.case_sensitive_cb.setChecked(op.case_sensitive)
|
|
30777
31686
|
|
|
30778
31687
|
def _fr_run_set_batch(self, fr_set: FindReplaceSet):
|
|
30779
|
-
"""Run all enabled operations in a F&R Set as a batch."""
|
|
31688
|
+
"""Run all enabled operations in a F&R Set as a batch (optimized for speed)."""
|
|
30780
31689
|
enabled_ops = [op for op in fr_set.operations if op.enabled and op.find_text]
|
|
30781
31690
|
|
|
30782
31691
|
if not enabled_ops:
|
|
@@ -30795,17 +31704,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
30795
31704
|
if reply != QMessageBox.StandardButton.Yes:
|
|
30796
31705
|
return
|
|
30797
31706
|
|
|
30798
|
-
#
|
|
30799
|
-
|
|
30800
|
-
for op in enabled_ops:
|
|
30801
|
-
count = self._execute_single_fr_operation(op)
|
|
30802
|
-
total_replaced += count
|
|
30803
|
-
self.log(f" '{op.find_text}' → '{op.replace_text}': {count} replacement(s)")
|
|
31707
|
+
# OPTIMIZATION: Disable UI updates during batch processing
|
|
31708
|
+
self.table.setUpdatesEnabled(False)
|
|
30804
31709
|
|
|
30805
|
-
|
|
30806
|
-
|
|
30807
|
-
|
|
30808
|
-
|
|
31710
|
+
try:
|
|
31711
|
+
# Run each operation
|
|
31712
|
+
total_replaced = 0
|
|
31713
|
+
for op in enabled_ops:
|
|
31714
|
+
count = self._execute_single_fr_operation(op)
|
|
31715
|
+
total_replaced += count
|
|
31716
|
+
self.log(f" '{op.find_text}' → '{op.replace_text}': {count} replacement(s)")
|
|
31717
|
+
|
|
31718
|
+
self.project_modified = True
|
|
31719
|
+
self.update_window_title()
|
|
31720
|
+
|
|
31721
|
+
finally:
|
|
31722
|
+
# OPTIMIZATION: Re-enable UI updates and refresh only target column cells
|
|
31723
|
+
self.table.setUpdatesEnabled(True)
|
|
31724
|
+
# Update all target cells in-place (batch operations can affect many segments)
|
|
31725
|
+
for row in range(len(self.current_project.segments)):
|
|
31726
|
+
segment = self.current_project.segments[row]
|
|
31727
|
+
target_widget = self.table.cellWidget(row, 3)
|
|
31728
|
+
if target_widget and hasattr(target_widget, 'setPlainText'):
|
|
31729
|
+
target_widget.blockSignals(True)
|
|
31730
|
+
target_widget.setPlainText(segment.target)
|
|
31731
|
+
target_widget.blockSignals(False)
|
|
31732
|
+
self.table.viewport().update()
|
|
30809
31733
|
|
|
30810
31734
|
QMessageBox.information(
|
|
30811
31735
|
self.find_replace_dialog,
|
|
@@ -30814,12 +31738,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
30814
31738
|
)
|
|
30815
31739
|
|
|
30816
31740
|
def _execute_single_fr_operation(self, op: FindReplaceOperation) -> int:
|
|
30817
|
-
"""Execute a single F&R operation on all segments. Returns replacement count."""
|
|
31741
|
+
"""Execute a single F&R operation on all segments (optimized). Returns replacement count."""
|
|
30818
31742
|
import re
|
|
30819
31743
|
count = 0
|
|
30820
31744
|
|
|
31745
|
+
# OPTIMIZATION: Pre-filter segments - only check segments that might contain the text
|
|
31746
|
+
# Quick case-insensitive check to skip segments that definitely don't match
|
|
31747
|
+
search_text_lower = op.find_text.lower() if not op.case_sensitive else None
|
|
31748
|
+
|
|
30821
31749
|
for segment in self.current_project.segments:
|
|
30822
31750
|
texts_to_check = []
|
|
31751
|
+
|
|
31752
|
+
# Pre-filter: skip segments that can't possibly match
|
|
31753
|
+
if not op.case_sensitive:
|
|
31754
|
+
# Quick check: does the segment contain the search text at all?
|
|
31755
|
+
skip_segment = True
|
|
31756
|
+
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
31757
|
+
if search_text_lower in segment.source.lower():
|
|
31758
|
+
skip_segment = False
|
|
31759
|
+
if op.search_in in ("target", "both"):
|
|
31760
|
+
if search_text_lower in segment.target.lower():
|
|
31761
|
+
skip_segment = False
|
|
31762
|
+
if skip_segment:
|
|
31763
|
+
continue
|
|
31764
|
+
|
|
30823
31765
|
if op.search_in in ("source", "both") and self.allow_replace_in_source:
|
|
30824
31766
|
texts_to_check.append(("source", segment.source))
|
|
30825
31767
|
if op.search_in in ("target", "both"):
|
|
@@ -31115,7 +32057,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31115
32057
|
self.find_next_match()
|
|
31116
32058
|
|
|
31117
32059
|
def replace_all_matches(self):
|
|
31118
|
-
"""Replace all matches in target segments"""
|
|
32060
|
+
"""Replace all matches in target segments (optimized for speed)"""
|
|
31119
32061
|
find_text = self.find_input.text()
|
|
31120
32062
|
replace_text = self.replace_input.text()
|
|
31121
32063
|
|
|
@@ -31188,52 +32130,67 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31188
32130
|
if reply != QMessageBox.StandardButton.Yes:
|
|
31189
32131
|
return
|
|
31190
32132
|
|
|
31191
|
-
#
|
|
31192
|
-
|
|
31193
|
-
replaced_count = 0
|
|
32133
|
+
# OPTIMIZATION: Disable UI updates during batch replacement
|
|
32134
|
+
self.table.setUpdatesEnabled(False)
|
|
31194
32135
|
|
|
31195
|
-
|
|
31196
|
-
|
|
31197
|
-
|
|
31198
|
-
|
|
31199
|
-
|
|
31200
|
-
old_text = segment.source
|
|
31201
|
-
else: # col == 3, Target
|
|
31202
|
-
old_text = segment.target
|
|
32136
|
+
try:
|
|
32137
|
+
# Perform replacements
|
|
32138
|
+
import re
|
|
32139
|
+
replaced_count = 0
|
|
32140
|
+
updated_rows = set() # Track which rows need UI updates
|
|
31203
32141
|
|
|
31204
|
-
|
|
31205
|
-
|
|
31206
|
-
|
|
31207
|
-
|
|
31208
|
-
if
|
|
31209
|
-
|
|
31210
|
-
else:
|
|
31211
|
-
|
|
31212
|
-
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
|
|
31216
|
-
# Update the appropriate field
|
|
31217
|
-
if col == 2:
|
|
31218
|
-
segment.source = new_text
|
|
32142
|
+
for row, col in self.find_matches:
|
|
32143
|
+
segment = self.current_project.segments[row]
|
|
32144
|
+
|
|
32145
|
+
# Get the appropriate field
|
|
32146
|
+
if col == 2: # Source
|
|
32147
|
+
old_text = segment.source
|
|
32148
|
+
else: # col == 3, Target
|
|
32149
|
+
old_text = segment.target
|
|
32150
|
+
|
|
32151
|
+
# Perform replacement
|
|
32152
|
+
if match_mode == 2: # Entire segment
|
|
32153
|
+
new_text = replace_text
|
|
31219
32154
|
else:
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
32155
|
+
if case_sensitive:
|
|
32156
|
+
new_text = old_text.replace(find_text, replace_text)
|
|
32157
|
+
else:
|
|
32158
|
+
pattern = re.escape(find_text)
|
|
32159
|
+
new_text = re.sub(pattern, replace_text, old_text, flags=re.IGNORECASE)
|
|
31225
32160
|
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
32161
|
+
if new_text != old_text:
|
|
32162
|
+
replaced_count += 1
|
|
32163
|
+
updated_rows.add(row)
|
|
32164
|
+
|
|
32165
|
+
# Update the appropriate field
|
|
32166
|
+
if col == 2:
|
|
32167
|
+
segment.source = new_text
|
|
32168
|
+
else:
|
|
32169
|
+
old_target = segment.target
|
|
32170
|
+
old_status = segment.status
|
|
32171
|
+
segment.target = new_text
|
|
32172
|
+
# Record undo state for find/replace operation
|
|
32173
|
+
self.record_undo_state(segment.id, old_target, new_text, old_status, old_status)
|
|
32174
|
+
|
|
32175
|
+
# OPTIMIZATION: Update only the affected cell widget in-place
|
|
32176
|
+
cell_widget = self.table.cellWidget(row, col)
|
|
32177
|
+
if cell_widget and hasattr(cell_widget, 'setPlainText'):
|
|
32178
|
+
cell_widget.blockSignals(True)
|
|
32179
|
+
cell_widget.setPlainText(new_text)
|
|
32180
|
+
cell_widget.blockSignals(False)
|
|
32181
|
+
|
|
32182
|
+
self.project_modified = True
|
|
32183
|
+
self.update_window_title()
|
|
32184
|
+
|
|
32185
|
+
# Clear matches
|
|
32186
|
+
self.find_matches = []
|
|
32187
|
+
|
|
32188
|
+
finally:
|
|
32189
|
+
# OPTIMIZATION: Re-enable UI updates without full grid reload
|
|
32190
|
+
self.table.setUpdatesEnabled(True)
|
|
32191
|
+
# Trigger a repaint of updated rows only
|
|
32192
|
+
for row in updated_rows:
|
|
32193
|
+
self.table.viewport().update()
|
|
31237
32194
|
|
|
31238
32195
|
QMessageBox.information(self.find_replace_dialog, "Replace All", f"Replaced {replaced_count} occurrence(s).")
|
|
31239
32196
|
self.log(f"✓ Replaced {replaced_count} occurrence(s) of '{find_text}'")
|
|
@@ -31999,15 +32956,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
31999
32956
|
|
|
32000
32957
|
try:
|
|
32001
32958
|
visible_count = 0
|
|
32002
|
-
row_count = self.table.rowCount()
|
|
32003
32959
|
segments = self.current_project.segments
|
|
32960
|
+
total_segments = len(segments)
|
|
32004
32961
|
|
|
32005
32962
|
# Pre-compute lowercase filter texts
|
|
32006
32963
|
source_filter_lower = source_filter_text.lower() if source_filter_text else None
|
|
32007
32964
|
target_filter_lower = target_filter_text.lower() if target_filter_text else None
|
|
32008
32965
|
|
|
32009
|
-
|
|
32010
|
-
|
|
32966
|
+
# IMPORTANT: Always search through ALL segments, not just visible rows
|
|
32967
|
+
# Pagination state should not affect which segments we search
|
|
32968
|
+
for row in range(total_segments):
|
|
32969
|
+
if row >= total_segments:
|
|
32011
32970
|
break
|
|
32012
32971
|
|
|
32013
32972
|
segment = segments[row]
|
|
@@ -35221,11 +36180,30 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35221
36180
|
except Exception as e:
|
|
35222
36181
|
QMessageBox.warning(self, "Error", f"Could not open api_keys.txt: {str(e)}")
|
|
35223
36182
|
|
|
35224
|
-
def _go_to_settings_tab(self):
|
|
35225
|
-
"""Navigate to Settings tab (from menu)
|
|
36183
|
+
def _go_to_settings_tab(self, subtab_name: str = None):
|
|
36184
|
+
"""Navigate to Settings tab (from menu), optionally to a specific sub-tab
|
|
36185
|
+
|
|
36186
|
+
Args:
|
|
36187
|
+
subtab_name: Name of the sub-tab to navigate to (e.g., "AI Settings")
|
|
36188
|
+
"""
|
|
35226
36189
|
if hasattr(self, 'main_tabs'):
|
|
35227
|
-
# Main tabs: Grid=0,
|
|
35228
|
-
self.main_tabs.setCurrentIndex(
|
|
36190
|
+
# Main tabs: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
|
|
36191
|
+
self.main_tabs.setCurrentIndex(4)
|
|
36192
|
+
|
|
36193
|
+
# Navigate to specific sub-tab if requested
|
|
36194
|
+
if subtab_name and hasattr(self, 'settings_tabs'):
|
|
36195
|
+
for i in range(self.settings_tabs.count()):
|
|
36196
|
+
if subtab_name.lower() in self.settings_tabs.tabText(i).lower():
|
|
36197
|
+
self.settings_tabs.setCurrentIndex(i)
|
|
36198
|
+
|
|
36199
|
+
# If navigating to AI Settings, scroll to bottom (API keys section)
|
|
36200
|
+
if "ai settings" in subtab_name.lower() and hasattr(self, 'ai_settings_scroll'):
|
|
36201
|
+
# Use QTimer to ensure the tab is fully rendered before scrolling
|
|
36202
|
+
from PyQt6.QtCore import QTimer
|
|
36203
|
+
QTimer.singleShot(100, lambda: self.ai_settings_scroll.verticalScrollBar().setValue(
|
|
36204
|
+
self.ai_settings_scroll.verticalScrollBar().maximum()
|
|
36205
|
+
))
|
|
36206
|
+
break
|
|
35229
36207
|
|
|
35230
36208
|
def _go_to_superlookup(self):
|
|
35231
36209
|
"""Navigate to Superlookup in Tools tab"""
|
|
@@ -35240,6 +36218,18 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35240
36218
|
self.modules_tabs.setCurrentIndex(i)
|
|
35241
36219
|
break
|
|
35242
36220
|
|
|
36221
|
+
def _navigate_to_tool(self, tool_name: str):
|
|
36222
|
+
"""Navigate to a specific tool in the Tools tab"""
|
|
36223
|
+
if hasattr(self, 'main_tabs'):
|
|
36224
|
+
# Main tabs: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
|
|
36225
|
+
self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
|
|
36226
|
+
# Then switch to the specific tool sub-tab
|
|
36227
|
+
if hasattr(self, 'modules_tabs'):
|
|
36228
|
+
for i in range(self.modules_tabs.count()):
|
|
36229
|
+
if tool_name in self.modules_tabs.tabText(i):
|
|
36230
|
+
self.modules_tabs.setCurrentIndex(i)
|
|
36231
|
+
break
|
|
36232
|
+
|
|
35243
36233
|
def open_api_keys_file(self):
|
|
35244
36234
|
"""Open API keys file in system text editor"""
|
|
35245
36235
|
api_keys_file = self.user_data_path / "api_keys.txt"
|
|
@@ -36445,7 +37435,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
36445
37435
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
36446
37436
|
)
|
|
36447
37437
|
if reply == QMessageBox.StandardButton.Yes:
|
|
36448
|
-
self._go_to_settings_tab()
|
|
37438
|
+
self._go_to_settings_tab("AI Settings")
|
|
36449
37439
|
return
|
|
36450
37440
|
|
|
36451
37441
|
# Check if API key exists for selected provider
|
|
@@ -36804,7 +37794,19 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
36804
37794
|
return text.strip()
|
|
36805
37795
|
|
|
36806
37796
|
def _quickmenu_build_custom_prompt(self, prompt_relative_path: str, source_text: str, source_lang: str, target_lang: str) -> str:
|
|
36807
|
-
"""Build a
|
|
37797
|
+
"""Build a prompt for QuickMenu using the selected prompt as instructions.
|
|
37798
|
+
|
|
37799
|
+
This is a GENERIC prompt builder (not translation-specific) that allows QuickMenu prompts
|
|
37800
|
+
to do anything: explain, define, search, translate, analyze, etc.
|
|
37801
|
+
|
|
37802
|
+
Supports placeholders:
|
|
37803
|
+
- {{SELECTION}} or {{SOURCE_TEXT}} - The selected text
|
|
37804
|
+
- {{SOURCE_LANGUAGE}} - Source language name
|
|
37805
|
+
- {{TARGET_LANGUAGE}} - Target language name
|
|
37806
|
+
- {{SOURCE+TARGET_CONTEXT}} - Project segments with both source and target (for proofreading)
|
|
37807
|
+
- {{SOURCE_CONTEXT}} - Project segments with source only (for translation questions)
|
|
37808
|
+
- {{TARGET_CONTEXT}} - Project segments with target only (for consistency/style analysis)
|
|
37809
|
+
"""
|
|
36808
37810
|
if not hasattr(self, 'prompt_manager_qt') or not self.prompt_manager_qt:
|
|
36809
37811
|
raise RuntimeError("Prompt manager not available")
|
|
36810
37812
|
|
|
@@ -36821,25 +37823,114 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
36821
37823
|
if not prompt_content:
|
|
36822
37824
|
raise RuntimeError("Prompt content is empty")
|
|
36823
37825
|
|
|
36824
|
-
|
|
36825
|
-
|
|
36826
|
-
|
|
36827
|
-
|
|
36828
|
-
|
|
36829
|
-
|
|
36830
|
-
|
|
36831
|
-
|
|
36832
|
-
|
|
36833
|
-
|
|
37826
|
+
# Build document context if requested (three variants)
|
|
37827
|
+
source_target_context = ""
|
|
37828
|
+
source_only_context = ""
|
|
37829
|
+
target_only_context = ""
|
|
37830
|
+
|
|
37831
|
+
has_source_target = "{{SOURCE+TARGET_CONTEXT}}" in prompt_content
|
|
37832
|
+
has_source_only = "{{SOURCE_CONTEXT}}" in prompt_content
|
|
37833
|
+
has_target_only = "{{TARGET_CONTEXT}}" in prompt_content
|
|
37834
|
+
|
|
37835
|
+
if (has_source_target or has_source_only or has_target_only) and hasattr(self, 'current_project') and self.current_project:
|
|
37836
|
+
if has_source_target:
|
|
37837
|
+
source_target_context = self._build_quickmenu_document_context(mode="both")
|
|
37838
|
+
self.log(f"🔍 QuickMenu: Built SOURCE+TARGET context ({len(source_target_context)} chars)")
|
|
37839
|
+
if has_source_only:
|
|
37840
|
+
source_only_context = self._build_quickmenu_document_context(mode="source")
|
|
37841
|
+
self.log(f"🔍 QuickMenu: Built SOURCE_ONLY context ({len(source_only_context)} chars)")
|
|
37842
|
+
if has_target_only:
|
|
37843
|
+
target_only_context = self._build_quickmenu_document_context(mode="target")
|
|
37844
|
+
self.log(f"🔍 QuickMenu: Built TARGET_ONLY context ({len(target_only_context)} chars)")
|
|
37845
|
+
else:
|
|
37846
|
+
if has_source_target:
|
|
37847
|
+
self.log("⚠️ QuickMenu: {{SOURCE+TARGET_CONTEXT}} requested but no project loaded")
|
|
37848
|
+
if has_source_only:
|
|
37849
|
+
self.log("⚠️ QuickMenu: {{SOURCE_CONTEXT}} requested but no project loaded")
|
|
37850
|
+
if has_target_only:
|
|
37851
|
+
self.log("⚠️ QuickMenu: {{TARGET_CONTEXT}} requested but no project loaded")
|
|
37852
|
+
|
|
37853
|
+
# Replace placeholders in the prompt content
|
|
37854
|
+
prompt_content = prompt_content.replace("{{SOURCE_LANGUAGE}}", source_lang)
|
|
37855
|
+
prompt_content = prompt_content.replace("{{TARGET_LANGUAGE}}", target_lang)
|
|
37856
|
+
prompt_content = prompt_content.replace("{{SOURCE_TEXT}}", source_text)
|
|
37857
|
+
prompt_content = prompt_content.replace("{{SELECTION}}", source_text) # Alternative placeholder
|
|
37858
|
+
prompt_content = prompt_content.replace("{{SOURCE+TARGET_CONTEXT}}", source_target_context)
|
|
37859
|
+
prompt_content = prompt_content.replace("{{SOURCE_CONTEXT}}", source_only_context)
|
|
37860
|
+
prompt_content = prompt_content.replace("{{TARGET_CONTEXT}}", target_only_context)
|
|
37861
|
+
|
|
37862
|
+
# Debug: Log the final prompt being sent
|
|
37863
|
+
self.log(f"📝 QuickMenu: Final prompt ({len(prompt_content)} chars):")
|
|
37864
|
+
self.log("─" * 80)
|
|
37865
|
+
self.log(prompt_content[:500] + ("..." if len(prompt_content) > 500 else ""))
|
|
37866
|
+
self.log("─" * 80)
|
|
37867
|
+
|
|
37868
|
+
# If the prompt doesn't contain the selection/text, append it
|
|
37869
|
+
if "{{SOURCE_TEXT}}" not in prompt_data.get('content', '') and "{{SELECTION}}" not in prompt_data.get('content', ''):
|
|
37870
|
+
prompt_content += f"\n\nText:\n{source_text}"
|
|
37871
|
+
|
|
37872
|
+
return prompt_content
|
|
37873
|
+
|
|
37874
|
+
def _build_quickmenu_document_context(self, mode: str = "both") -> str:
|
|
37875
|
+
"""Build document context for QuickMenu prompts.
|
|
37876
|
+
|
|
37877
|
+
Args:
|
|
37878
|
+
mode: One of:
|
|
37879
|
+
- "both": Include both source and target text (for proofreading)
|
|
37880
|
+
- "source": Include only source text (for translation/terminology questions)
|
|
37881
|
+
- "target": Include only target text (for consistency/style analysis)
|
|
37882
|
+
|
|
37883
|
+
Returns a formatted string with segments from the project for context.
|
|
37884
|
+
Uses the 'quickmenu_context_segments' setting (default: 50% of total segments).
|
|
37885
|
+
"""
|
|
37886
|
+
if not hasattr(self, 'current_project') or not self.current_project or not self.current_project.segments:
|
|
37887
|
+
return "(No project context available)"
|
|
37888
|
+
|
|
36834
37889
|
try:
|
|
36835
|
-
|
|
36836
|
-
|
|
36837
|
-
|
|
36838
|
-
|
|
36839
|
-
|
|
36840
|
-
|
|
36841
|
-
|
|
36842
|
-
|
|
37890
|
+
# Get settings
|
|
37891
|
+
general_prefs = self.load_general_settings()
|
|
37892
|
+
context_percent = general_prefs.get('quickmenu_context_percent', 50) # Default: 50%
|
|
37893
|
+
max_context_segments = general_prefs.get('quickmenu_context_max', 100) # Safety limit
|
|
37894
|
+
|
|
37895
|
+
# Calculate how many segments to include
|
|
37896
|
+
total_segments = len(self.current_project.segments)
|
|
37897
|
+
num_segments = min(
|
|
37898
|
+
int(total_segments * context_percent / 100),
|
|
37899
|
+
max_context_segments
|
|
37900
|
+
)
|
|
37901
|
+
|
|
37902
|
+
if num_segments == 0:
|
|
37903
|
+
return "(Document context disabled)"
|
|
37904
|
+
|
|
37905
|
+
# Get segments (from start of document)
|
|
37906
|
+
context_segments = self.current_project.segments[:num_segments]
|
|
37907
|
+
|
|
37908
|
+
# Format segments based on mode
|
|
37909
|
+
context_parts = []
|
|
37910
|
+
mode_labels = {"both": "SOURCE + TARGET", "source": "SOURCE ONLY", "target": "TARGET ONLY"}
|
|
37911
|
+
context_parts.append(f"=== DOCUMENT CONTEXT ({mode_labels.get(mode, 'UNKNOWN')}) ===")
|
|
37912
|
+
context_parts.append(f"(Showing {num_segments} of {total_segments} segments - {context_percent}%)")
|
|
37913
|
+
context_parts.append("")
|
|
37914
|
+
|
|
37915
|
+
for seg in context_segments:
|
|
37916
|
+
if mode == "both":
|
|
37917
|
+
# Source + Target
|
|
37918
|
+
context_parts.append(f"[{seg.id}] {seg.source}")
|
|
37919
|
+
if seg.target and seg.target.strip():
|
|
37920
|
+
context_parts.append(f" → {seg.target}")
|
|
37921
|
+
elif mode == "source":
|
|
37922
|
+
# Source only
|
|
37923
|
+
context_parts.append(f"[{seg.id}] {seg.source}")
|
|
37924
|
+
elif mode == "target":
|
|
37925
|
+
# Target only (skip empty targets)
|
|
37926
|
+
if seg.target and seg.target.strip():
|
|
37927
|
+
context_parts.append(f"[{seg.id}] {seg.target}")
|
|
37928
|
+
context_parts.append("") # Blank line between segments
|
|
37929
|
+
|
|
37930
|
+
return "\n".join(context_parts)
|
|
37931
|
+
|
|
37932
|
+
except Exception as e:
|
|
37933
|
+
return f"(Error building document context: {e})"
|
|
36843
37934
|
|
|
36844
37935
|
def _quickmenu_show_result_dialog(self, title: str, output_text: str, apply_callback=None):
|
|
36845
37936
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QPushButton, QApplication
|
|
@@ -36919,10 +38010,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
36919
38010
|
)
|
|
36920
38011
|
|
|
36921
38012
|
client = LLMClient(api_key=api_key, provider=provider, model=model)
|
|
38013
|
+
|
|
38014
|
+
# Use translate() with empty text and custom_prompt for generic AI completion
|
|
38015
|
+
# This allows QuickMenu prompts to do anything (explain, define, search, etc.)
|
|
38016
|
+
# not just translation. Same pattern as AI Assistant.
|
|
36922
38017
|
output_text = client.translate(
|
|
36923
|
-
text=
|
|
36924
|
-
source_lang=
|
|
36925
|
-
target_lang=
|
|
38018
|
+
text="", # Empty - we're using custom_prompt for everything
|
|
38019
|
+
source_lang="en", # Dummy values
|
|
38020
|
+
target_lang="en",
|
|
36926
38021
|
custom_prompt=custom_prompt
|
|
36927
38022
|
)
|
|
36928
38023
|
|
|
@@ -38672,11 +39767,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38672
39767
|
return f"[MyMemory error: {str(e)}]"
|
|
38673
39768
|
|
|
38674
39769
|
def load_api_keys(self) -> Dict[str, str]:
|
|
38675
|
-
"""Load API keys
|
|
39770
|
+
"""Load API keys with dev-first priority
|
|
39771
|
+
|
|
39772
|
+
Priority order:
|
|
39773
|
+
1. user_data_private/api_keys.txt (dev mode, gitignored)
|
|
39774
|
+
2. user_data/api_keys.txt (user mode, shipped with app)
|
|
39775
|
+
"""
|
|
38676
39776
|
api_keys = {}
|
|
38677
|
-
api_keys_file = self.user_data_path / "api_keys.txt"
|
|
38678
39777
|
|
|
38679
|
-
|
|
39778
|
+
# Priority 1: Dev mode (gitignored, never shipped)
|
|
39779
|
+
dev_file = Path("user_data_private") / "api_keys.txt"
|
|
39780
|
+
|
|
39781
|
+
# Priority 2: User mode (ships with app)
|
|
39782
|
+
user_file = self.user_data_path / "api_keys.txt"
|
|
39783
|
+
|
|
39784
|
+
# Check dev first, then user
|
|
39785
|
+
if dev_file.exists():
|
|
39786
|
+
api_keys_file = dev_file
|
|
39787
|
+
elif user_file.exists():
|
|
39788
|
+
api_keys_file = user_file
|
|
39789
|
+
else:
|
|
38680
39790
|
return api_keys
|
|
38681
39791
|
|
|
38682
39792
|
try:
|
|
@@ -38806,9 +39916,9 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38806
39916
|
def show_autofingers(self):
|
|
38807
39917
|
"""Show AutoFingers by switching to the AutoFingers tab"""
|
|
38808
39918
|
# Find the AutoFingers tab index and activate it
|
|
38809
|
-
# AutoFingers is in Tools tab (main_tabs index
|
|
39919
|
+
# AutoFingers is in Tools tab (main_tabs index 3)
|
|
38810
39920
|
if hasattr(self, 'main_tabs'):
|
|
38811
|
-
self.main_tabs.setCurrentIndex(
|
|
39921
|
+
self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
|
|
38812
39922
|
# Then switch to AutoFingers sub-tab
|
|
38813
39923
|
if hasattr(self, 'modules_tabs'):
|
|
38814
39924
|
for i in range(self.modules_tabs.count()):
|
|
@@ -43493,7 +44603,7 @@ class SuperlookupTab(QWidget):
|
|
|
43493
44603
|
try:
|
|
43494
44604
|
# Use multiple methods to ensure cleanup
|
|
43495
44605
|
# Method 1: Kill by window title
|
|
43496
|
-
subprocess.run(['taskkill', '/F', '/FI', 'WINDOWTITLE eq
|
|
44606
|
+
subprocess.run(['taskkill', '/F', '/FI', 'WINDOWTITLE eq supervertaler_hotkeys.ahk*'],
|
|
43497
44607
|
capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
43498
44608
|
|
|
43499
44609
|
# Method 2: Kill AutoHotkey processes more aggressively
|
|
@@ -43505,7 +44615,7 @@ class SuperlookupTab(QWidget):
|
|
|
43505
44615
|
try:
|
|
43506
44616
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
|
43507
44617
|
try:
|
|
43508
|
-
if '
|
|
44618
|
+
if 'supervertaler_hotkeys' in ' '.join(proc.cmdline() or []):
|
|
43509
44619
|
proc.kill()
|
|
43510
44620
|
except:
|
|
43511
44621
|
pass
|
|
@@ -43518,8 +44628,8 @@ class SuperlookupTab(QWidget):
|
|
|
43518
44628
|
ahk_exe, source = self._find_autohotkey_executable()
|
|
43519
44629
|
|
|
43520
44630
|
if not ahk_exe:
|
|
43521
|
-
print("[
|
|
43522
|
-
print("[
|
|
44631
|
+
print("[Hotkeys] AutoHotkey not found.")
|
|
44632
|
+
print("[Hotkeys] Global hotkeys (Ctrl+Alt+L, Shift+Shift) will not be available.")
|
|
43523
44633
|
self.hotkey_registered = False
|
|
43524
44634
|
# Show setup dialog (deferred to avoid blocking startup) - unless user opted out
|
|
43525
44635
|
if self.main_window and hasattr(self.main_window, 'general_settings'):
|
|
@@ -43529,11 +44639,11 @@ class SuperlookupTab(QWidget):
|
|
|
43529
44639
|
QTimer.singleShot(2000, self._show_autohotkey_setup_dialog)
|
|
43530
44640
|
return
|
|
43531
44641
|
|
|
43532
|
-
print(f"[
|
|
44642
|
+
print(f"[Hotkeys] Found AutoHotkey at: {ahk_exe} (source: {source})")
|
|
43533
44643
|
|
|
43534
|
-
ahk_script = Path(__file__).parent / "
|
|
43535
|
-
print(f"[
|
|
43536
|
-
print(f"[
|
|
44644
|
+
ahk_script = Path(__file__).parent / "supervertaler_hotkeys.ahk"
|
|
44645
|
+
print(f"[Hotkeys] Looking for script at: {ahk_script}")
|
|
44646
|
+
print(f"[Hotkeys] Script exists: {ahk_script.exists()}")
|
|
43537
44647
|
|
|
43538
44648
|
if ahk_script.exists():
|
|
43539
44649
|
# Start AHK script in background (hidden)
|
|
@@ -43541,16 +44651,16 @@ class SuperlookupTab(QWidget):
|
|
|
43541
44651
|
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
|
43542
44652
|
# Store in global variable for atexit cleanup
|
|
43543
44653
|
_ahk_process = self.ahk_process
|
|
43544
|
-
print(f"[
|
|
44654
|
+
print(f"[Hotkeys] AHK hotkeys registered (Ctrl+Alt+L, Shift+Shift)")
|
|
43545
44655
|
|
|
43546
44656
|
# Start file watcher
|
|
43547
44657
|
self.start_file_watcher()
|
|
43548
44658
|
self.hotkey_registered = True
|
|
43549
44659
|
else:
|
|
43550
|
-
print(f"[
|
|
44660
|
+
print(f"[Hotkeys] AHK script not found: {ahk_script}")
|
|
43551
44661
|
self.hotkey_registered = False
|
|
43552
44662
|
except Exception as e:
|
|
43553
|
-
print(f"[
|
|
44663
|
+
print(f"[Hotkeys] Could not start AHK hotkeys: {e}")
|
|
43554
44664
|
self.hotkey_registered = False
|
|
43555
44665
|
|
|
43556
44666
|
def start_file_watcher(self):
|
|
@@ -43633,11 +44743,11 @@ class SuperlookupTab(QWidget):
|
|
|
43633
44743
|
print(f"[Superlookup] Main window type: {type(main_window).__name__}")
|
|
43634
44744
|
print(f"[Superlookup] Has main_tabs: {hasattr(main_window, 'main_tabs')}")
|
|
43635
44745
|
|
|
43636
|
-
# Switch to Tools tab (main_tabs index
|
|
43637
|
-
# Tab structure: Grid=0,
|
|
44746
|
+
# Switch to Tools tab (main_tabs index 3)
|
|
44747
|
+
# Tab structure: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
|
|
43638
44748
|
if hasattr(main_window, 'main_tabs'):
|
|
43639
44749
|
print(f"[Superlookup] Current main_tab index: {main_window.main_tabs.currentIndex()}")
|
|
43640
|
-
main_window.main_tabs.setCurrentIndex(
|
|
44750
|
+
main_window.main_tabs.setCurrentIndex(3) # Tools tab
|
|
43641
44751
|
print(f"[Superlookup] Switched to Tools tab (index 2)")
|
|
43642
44752
|
QApplication.processEvents() # Force GUI update
|
|
43643
44753
|
else:
|