supervertaler 1.9.147__py3-none-any.whl → 1.9.149__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 CHANGED
@@ -13,7 +13,7 @@ For the classic tkinter edition, see Supervertaler_tkinter.py
13
13
  Key Features:
14
14
  - Complete Translation Matching: Termbase + TM + MT + Multi-LLM
15
15
  - Project Termbases: Dedicated terminology per project with automatic extraction
16
- - Supervoice: AI-powered voice dictation (100+ languages via OpenAI Whisper)
16
+ - Supervoice: AI-enhanced voice dictation (100+ languages via OpenAI Whisper)
17
17
  - Superimage: Extract images from DOCX files with preview
18
18
  - Google Cloud Translation API integration
19
19
  - Multi-LLM Support: OpenAI GPT, Claude, Google Gemini
@@ -34,9 +34,9 @@ License: MIT
34
34
  """
35
35
 
36
36
  # Version Information.
37
- __version__ = "1.9.147"
37
+ __version__ = "1.9.149-beta"
38
38
  __phase__ = "0.9"
39
- __release_date__ = "2026-01-20"
39
+ __release_date__ = "2026-01-21"
40
40
  __edition__ = "Qt"
41
41
 
42
42
  import sys
@@ -61,161 +61,148 @@ def get_resource_path(relative_path: str) -> Path:
61
61
  return base_path / relative_path
62
62
 
63
63
 
64
- def get_user_data_path() -> Path:
64
+ def get_config_pointer_path() -> Path:
65
65
  """
66
- Get the path to user data directory.
67
-
68
- This function returns a PERSISTENT location for user data that survives
69
- pip upgrades and reinstalls. The location varies by context:
70
-
71
- 1. FROZEN BUILDS (Windows EXE): 'user_data' folder next to the EXE
72
- - This allows the EXE to be portable (copy folder = copy everything)
73
- - Path: C:/SomeFolder/Supervertaler/user_data/
66
+ Get path to the config pointer file that stores the user's chosen data location.
74
67
 
75
- 2. PIP INSTALLS: Platform-specific user data directory
76
- - Windows: %LOCALAPPDATA%/Supervertaler/ (e.g., C:/Users/John/AppData/Local/Supervertaler/)
77
- - macOS: ~/Library/Application Support/Supervertaler/
78
- - Linux: ~/.local/share/Supervertaler/ (XDG_DATA_HOME)
79
- - This location persists across pip upgrades!
68
+ This is stored in a standard config location:
69
+ - Windows: %APPDATA%/Supervertaler/config.json
70
+ - macOS: ~/Library/Application Support/Supervertaler/config.json
71
+ - Linux: ~/.config/Supervertaler/config.json
80
72
 
81
- 3. DEVELOPMENT MODE: 'user_data' or 'user_data_private' next to script
82
- - Developers can use ENABLE_PRIVATE_FEATURES to use user_data_private/
83
-
84
- The key insight: pip installs wipe site-packages on upgrade, so we MUST
85
- store user data outside the package directory for pip users.
73
+ This small file just contains a pointer to where the real data lives.
86
74
  """
87
- if getattr(sys, 'frozen', False):
88
- # =====================================================================
89
- # FROZEN BUILD (EXE): Keep data next to executable for portability
90
- # =====================================================================
91
- # Users can copy the entire folder to a USB drive and it works.
92
- # The build script copies user_data/ next to Supervertaler.exe
93
- return Path(sys.executable).parent / "user_data"
94
-
95
- # Check if we're running from site-packages (pip install)
96
- script_path = Path(__file__).resolve()
97
- is_pip_install = "site-packages" in str(script_path)
98
-
99
- if is_pip_install:
100
- # =================================================================
101
- # PIP INSTALL: Use platform-specific persistent location
102
- # =================================================================
103
- # This survives pip upgrades because it's OUTSIDE site-packages!
104
- try:
105
- from platformdirs import user_data_dir
106
- return Path(user_data_dir("Supervertaler", "MichaelBeijer"))
107
- except ImportError:
108
- # Fallback if platformdirs not available (shouldn't happen)
109
- if sys.platform == 'win32':
110
- base = Path(os.environ.get('LOCALAPPDATA', os.path.expanduser('~')))
111
- return base / "Supervertaler"
112
- elif sys.platform == 'darwin':
113
- return Path.home() / "Library" / "Application Support" / "Supervertaler"
114
- else:
115
- # Linux/BSD - follow XDG spec
116
- xdg_data = os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
117
- return Path(xdg_data) / "Supervertaler"
75
+ if sys.platform == 'win32':
76
+ # Windows: %APPDATA% (Roaming, syncs across machines on domain)
77
+ appdata = os.environ.get('APPDATA', os.path.expanduser('~'))
78
+ return Path(appdata) / "Supervertaler" / "config.json"
79
+ elif sys.platform == 'darwin':
80
+ # macOS: ~/Library/Application Support/
81
+ return Path.home() / "Library" / "Application Support" / "Supervertaler" / "config.json"
118
82
  else:
119
- # =================================================================
120
- # DEVELOPMENT MODE: Use folder next to script
121
- # =================================================================
122
- return Path(__file__).parent / "user_data"
83
+ # Linux/BSD: ~/.config/ (XDG_CONFIG_HOME)
84
+ xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
85
+ return Path(xdg_config) / "Supervertaler" / "config.json"
123
86
 
124
87
 
125
- def migrate_user_data_if_needed(new_path: Path) -> bool:
88
+ def get_default_user_data_path() -> Path:
126
89
  """
127
- Migrate user data from old location (inside site-packages) to new location.
90
+ Get the DEFAULT user data path (visible in home folder).
128
91
 
129
- This handles the transition for existing pip users whose data was stored
130
- inside the package directory (which gets wiped on upgrade).
92
+ - Windows: C:/Users/Username/Supervertaler/
93
+ - macOS: ~/Supervertaler/
94
+ - Linux: ~/Supervertaler/
131
95
 
132
- Returns True if migration was performed, False otherwise.
96
+ This is intentionally a visible, easily-accessible location.
133
97
  """
134
- import shutil
98
+ return Path.home() / "Supervertaler"
99
+
100
+
101
+ def save_user_data_path(path: Path) -> None:
102
+ """Save the user's chosen data path to the config pointer file."""
103
+ pointer_path = get_config_pointer_path()
104
+ pointer_path.parent.mkdir(parents=True, exist_ok=True)
135
105
 
136
- # Only migrate for pip installs (frozen builds keep data next to EXE)
137
- if getattr(sys, 'frozen', False):
138
- return False
106
+ config = {"user_data_path": str(path)}
107
+ with open(pointer_path, 'w', encoding='utf-8') as f:
108
+ json.dump(config, f, indent=2)
109
+
110
+
111
+ def load_user_data_path_from_config() -> Optional[Path]:
112
+ """
113
+ Load the user's chosen data path from config pointer file.
114
+ Returns None if no config exists or path is invalid.
115
+ """
116
+ pointer_path = get_config_pointer_path()
139
117
 
140
- # Check if we're in a pip install
141
- script_path = Path(__file__).resolve()
142
- if "site-packages" not in str(script_path):
143
- return False # Development mode, no migration needed
118
+ if not pointer_path.exists():
119
+ return None
144
120
 
145
- # Old location: user_data folder inside the package in site-packages
146
- old_path = script_path.parent / "user_data"
121
+ try:
122
+ with open(pointer_path, 'r', encoding='utf-8') as f:
123
+ config = json.load(f)
124
+
125
+ path_str = config.get('user_data_path')
126
+ if path_str:
127
+ return Path(path_str)
128
+ except (json.JSONDecodeError, OSError):
129
+ pass
147
130
 
148
- # If old location doesn't exist or is empty, no migration needed
149
- if not old_path.exists():
150
- return False
131
+ return None
132
+
133
+
134
+ def get_user_data_path() -> Path:
135
+ """
136
+ Get the path to user data directory.
151
137
 
152
- # Check if old location has actual content (not just empty dirs)
153
- old_has_content = False
154
- for item in old_path.rglob('*'):
155
- if item.is_file():
156
- old_has_content = True
157
- break
138
+ Resolution order:
139
+ 1. Check config pointer file for user's saved preference
140
+ 2. If pointer missing, check if default location has data (auto-recover)
141
+ 3. If nothing found, return default (first-run dialog will be shown later)
158
142
 
159
- if not old_has_content:
160
- return False
143
+ All users (EXE, pip, dev) use the same unified system:
144
+ - Default: ~/Supervertaler/ (visible in home folder)
145
+ - User can choose custom location on first run
146
+ - Config pointer remembers their choice
161
147
 
162
- # If new location already has content, don't overwrite (user may have set up fresh)
163
- new_has_content = False
164
- if new_path.exists():
165
- for item in new_path.rglob('*'):
166
- if item.is_file():
167
- new_has_content = True
168
- break
148
+ This ensures data survives pip upgrades, EXE updates, and is easy to find/backup.
149
+ """
150
+ # 1. Check if user has a saved preference
151
+ saved_path = load_user_data_path_from_config()
152
+ if saved_path and saved_path.exists():
153
+ return saved_path
154
+
155
+ # 2. Check if default location has data (auto-recovery if pointer deleted)
156
+ default_path = get_default_user_data_path()
157
+ if default_path.exists():
158
+ # Check if it has actual content
159
+ has_content = False
160
+ try:
161
+ for item in default_path.iterdir():
162
+ if item.is_file() or (item.is_dir() and any(item.iterdir())):
163
+ has_content = True
164
+ break
165
+ except OSError:
166
+ pass
167
+
168
+ if has_content:
169
+ # Auto-recover: recreate the pointer file
170
+ save_user_data_path(default_path)
171
+ print(f"[Data Paths] Auto-recovered config pointer to: {default_path}")
172
+ return default_path
169
173
 
170
- if new_has_content:
171
- # Both have content - log but don't migrate (user may have set up manually)
172
- print(f"[Migration] Both old and new data locations have content:")
173
- print(f" Old: {old_path}")
174
- print(f" New: {new_path}")
175
- print(f"[Migration] Keeping new location. Old data preserved in site-packages.")
176
- return False
174
+ # 3. Return default path (first-run dialog will be shown by MainWindow)
175
+ # Don't create folder yet - let the dialog handle that
176
+ return default_path
177
+
178
+
179
+ def needs_first_run_data_dialog() -> bool:
180
+ """
181
+ Check if we need to show the first-run data location dialog.
177
182
 
178
- # Perform migration
179
- print(f"[Migration] Migrating user data from old location...")
180
- print(f" From: {old_path}")
181
- print(f" To: {new_path}")
183
+ Returns True if:
184
+ - No config pointer file exists, AND
185
+ - Default data location doesn't have existing content
182
186
 
183
- try:
184
- # Ensure new path parent exists
185
- new_path.parent.mkdir(parents=True, exist_ok=True)
186
-
187
- # Copy entire directory tree (use copy, not move, for safety)
188
- if new_path.exists():
189
- # Merge into existing empty directory
190
- for item in old_path.iterdir():
191
- dest = new_path / item.name
192
- if item.is_dir():
193
- shutil.copytree(item, dest, dirs_exist_ok=True)
194
- else:
195
- shutil.copy2(item, dest)
196
- else:
197
- shutil.copytree(old_path, new_path)
198
-
199
- print(f"[Migration] ✅ Successfully migrated user data!")
200
- print(f"[Migration] Your API keys, TMs, glossaries, and prompts are now in:")
201
- print(f" {new_path}")
202
-
203
- # Leave a marker file in the old location so users know what happened
204
- marker = old_path / "_MIGRATED_TO_NEW_LOCATION.txt"
205
- marker.write_text(
206
- f"Your Supervertaler data has been migrated to a persistent location:\n"
207
- f"{new_path}\n\n"
208
- f"This location survives pip upgrades.\n"
209
- f"You can safely delete this old folder.\n"
210
- f"Migration date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
211
- )
212
-
213
- return True
214
-
215
- except Exception as e:
216
- print(f"[Migration] ⚠️ Error during migration: {e}")
217
- print(f"[Migration] Your data is still in: {old_path}")
187
+ This means it's truly a fresh install.
188
+ """
189
+ # If config pointer exists, user has already chosen a location
190
+ pointer_path = get_config_pointer_path()
191
+ if pointer_path.exists():
218
192
  return False
193
+
194
+ # If default location has content, we auto-recovered (no dialog needed)
195
+ default_path = get_default_user_data_path()
196
+ if default_path.exists():
197
+ try:
198
+ for item in default_path.iterdir():
199
+ if item.is_file() or (item.is_dir() and any(item.iterdir())):
200
+ return False # Has content, no dialog needed
201
+ except OSError:
202
+ pass
203
+
204
+ # Truly a fresh install - need to show dialog
205
+ return True
219
206
 
220
207
  import threading
221
208
  import time # For delays in Superlookup
@@ -5307,43 +5294,48 @@ class SupervertalerQt(QMainWindow):
5307
5294
  # ============================================================================
5308
5295
  # USER DATA PATH INITIALIZATION
5309
5296
  # ============================================================================
5310
- # User data is stored in a PERSISTENT location that survives pip upgrades:
5311
- # - Windows: %LOCALAPPDATA%\Supervertaler\
5312
- # - macOS: ~/Library/Application Support/Supervertaler/
5313
- # - Linux: ~/.local/share/Supervertaler/
5314
- # - EXE: user_data/ folder next to executable (portable mode)
5315
- # - Dev: user_data_private/ or user_data/ next to script
5297
+ # ALL users (EXE, pip, dev) now use a unified system:
5298
+ # - Default location: ~/Supervertaler/ (visible in home folder)
5299
+ # - User can choose custom location on first run
5300
+ # - Config pointer stores their choice at standard config location
5301
+ # - Auto-recovery if pointer is deleted but data exists at default location
5316
5302
  from modules.database_manager import DatabaseManager
5317
5303
 
5304
+ # Check if this is first run BEFORE getting the path
5305
+ # (so we know whether to show the data location dialog later)
5306
+ self._needs_data_location_dialog = needs_first_run_data_dialog()
5307
+
5318
5308
  if ENABLE_PRIVATE_FEATURES:
5319
5309
  # Developer mode: use private folder (git-ignored)
5320
5310
  self.user_data_path = Path(__file__).parent / "user_data_private"
5311
+ self._needs_data_location_dialog = False # Dev mode doesn't need dialog
5321
5312
  else:
5322
- # Normal mode: use the helper function (handles pip vs EXE vs dev)
5313
+ # Normal mode: use unified system (same for pip, EXE, etc.)
5323
5314
  self.user_data_path = get_user_data_path()
5324
5315
 
5325
- # Migrate old data from site-packages to new persistent location (pip users)
5326
- # This is a one-time migration for users upgrading from older versions
5327
- migrate_user_data_if_needed(self.user_data_path)
5328
-
5329
5316
  # Ensure user_data directory exists (creates empty folder if missing)
5330
- self.user_data_path.mkdir(parents=True, exist_ok=True)
5317
+ # BUT only if we're not going to show the dialog (which will create the chosen folder)
5318
+ if not self._needs_data_location_dialog:
5319
+ self.user_data_path.mkdir(parents=True, exist_ok=True)
5331
5320
 
5332
5321
  print(f"[Data Paths] User data: {self.user_data_path}")
5333
5322
 
5334
5323
  # Database Manager for Termbases
5335
5324
  self.db_manager = DatabaseManager(
5336
- db_path=str(self.user_data_path / "Translation_Resources" / "supervertaler.db"),
5325
+ db_path=str(self.user_data_path / "resources" / "supervertaler.db"),
5337
5326
  log_callback=self.log
5338
5327
  )
5339
- self.db_manager.connect()
5328
+ # Only connect if we're not showing the dialog (which will create the folder)
5329
+ # If dialog is needed, we'll connect after user chooses location
5330
+ if not self._needs_data_location_dialog:
5331
+ self.db_manager.connect()
5340
5332
 
5341
5333
  # TM Database - Initialize early so Superlookup works without a project loaded
5342
5334
  from modules.translation_memory import TMDatabase
5343
5335
  self.tm_database = TMDatabase(
5344
5336
  source_lang=None, # Will be set when project is loaded
5345
5337
  target_lang=None, # Will be set when project is loaded
5346
- db_path=str(self.user_data_path / "Translation_Resources" / "supervertaler.db"),
5338
+ db_path=str(self.user_data_path / "resources" / "supervertaler.db"),
5347
5339
  log_callback=self.log
5348
5340
  )
5349
5341
 
@@ -5460,11 +5452,184 @@ class SupervertalerQt(QMainWindow):
5460
5452
  from PyQt6.QtCore import QTimer
5461
5453
  QTimer.singleShot(2000, lambda: self._check_for_new_models(force=False)) # 2 second delay
5462
5454
 
5463
- # First-run check - show Features tab to new users
5464
- if not general_settings.get('first_run_completed', False):
5455
+ # First-run check - show data location dialog, then Features tab
5456
+ if self._needs_data_location_dialog:
5457
+ from PyQt6.QtCore import QTimer
5458
+ QTimer.singleShot(300, self._show_data_location_dialog)
5459
+ elif not general_settings.get('first_run_completed', False):
5465
5460
  from PyQt6.QtCore import QTimer
5466
5461
  QTimer.singleShot(500, self._show_first_run_welcome)
5467
5462
 
5463
+ def _show_data_location_dialog(self):
5464
+ """Show dialog to let user choose their data folder location on first run."""
5465
+ try:
5466
+ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
5467
+ QPushButton, QLineEdit, QFileDialog, QDialogButtonBox)
5468
+
5469
+ dialog = QDialog(self)
5470
+ dialog.setWindowTitle("Choose Data Folder Location")
5471
+ dialog.setMinimumWidth(550)
5472
+ dialog.setModal(True)
5473
+
5474
+ layout = QVBoxLayout(dialog)
5475
+ layout.setSpacing(15)
5476
+
5477
+ # Title
5478
+ title_label = QLabel("<h2>📁 Choose Your Data Folder</h2>")
5479
+ layout.addWidget(title_label)
5480
+
5481
+ # Explanation
5482
+ msg_label = QLabel(
5483
+ "Supervertaler stores your data in a folder of your choice, which contains things like:<br><br>"
5484
+ "• API keys<br>"
5485
+ "• Translation memories<br>"
5486
+ "• Glossaries<br>"
5487
+ "• Prompts<br>"
5488
+ "• Settings<br><br>"
5489
+ "Choose a location that's easy to find and backup.<br>"
5490
+ "You can change this later in Settings → General."
5491
+ )
5492
+ msg_label.setWordWrap(True)
5493
+ layout.addWidget(msg_label)
5494
+
5495
+ # Path input with browse button
5496
+ path_layout = QHBoxLayout()
5497
+
5498
+ path_edit = QLineEdit()
5499
+ default_path = get_default_user_data_path()
5500
+ path_edit.setText(str(default_path))
5501
+ path_edit.setMinimumWidth(350)
5502
+ path_layout.addWidget(path_edit)
5503
+
5504
+ browse_btn = QPushButton("Browse...")
5505
+ def browse_folder():
5506
+ folder = QFileDialog.getExistingDirectory(
5507
+ dialog,
5508
+ "Choose Data Folder",
5509
+ str(Path.home())
5510
+ )
5511
+ if folder:
5512
+ # Append "Supervertaler" if user didn't include it
5513
+ folder_path = Path(folder)
5514
+ if folder_path.name != "Supervertaler":
5515
+ folder_path = folder_path / "Supervertaler"
5516
+ path_edit.setText(str(folder_path))
5517
+
5518
+ browse_btn.clicked.connect(browse_folder)
5519
+ path_layout.addWidget(browse_btn)
5520
+
5521
+ layout.addLayout(path_layout)
5522
+
5523
+ # Tip
5524
+ tip_label = QLabel(
5525
+ "💡 <b>Tip:</b> The default location is in your home folder, "
5526
+ "making it easy to find and backup."
5527
+ )
5528
+ tip_label.setWordWrap(True)
5529
+ tip_label.setStyleSheet("color: #666;")
5530
+ layout.addWidget(tip_label)
5531
+
5532
+ # Buttons
5533
+ button_layout = QHBoxLayout()
5534
+
5535
+ default_btn = QPushButton("Use Default")
5536
+ default_btn.clicked.connect(lambda: path_edit.setText(str(default_path)))
5537
+ button_layout.addWidget(default_btn)
5538
+
5539
+ button_layout.addStretch()
5540
+
5541
+ ok_btn = QPushButton("OK")
5542
+ ok_btn.setDefault(True)
5543
+ ok_btn.clicked.connect(dialog.accept)
5544
+ button_layout.addWidget(ok_btn)
5545
+
5546
+ layout.addLayout(button_layout)
5547
+
5548
+ # Show dialog
5549
+ if dialog.exec() == QDialog.DialogCode.Accepted:
5550
+ chosen_path = Path(path_edit.text())
5551
+
5552
+ # Create the folder
5553
+ chosen_path.mkdir(parents=True, exist_ok=True)
5554
+
5555
+ # Save the choice to config pointer
5556
+ save_user_data_path(chosen_path)
5557
+
5558
+ # Update our path if different from what we initialized with
5559
+ if chosen_path != self.user_data_path:
5560
+ self.user_data_path = chosen_path
5561
+ # Re-initialize managers with new path
5562
+ self._reinitialize_with_new_data_path()
5563
+ else:
5564
+ # Same path, but we still need to connect database (it was deferred)
5565
+ if hasattr(self, 'db_manager') and self.db_manager and not self.db_manager.connection:
5566
+ self.db_manager.connect()
5567
+
5568
+ self.log(f"📁 Data folder set to: {chosen_path}")
5569
+
5570
+ # Now show the features welcome dialog
5571
+ from PyQt6.QtCore import QTimer
5572
+ QTimer.singleShot(300, self._show_first_run_welcome)
5573
+ else:
5574
+ # User cancelled - use default anyway
5575
+ default_path.mkdir(parents=True, exist_ok=True)
5576
+ save_user_data_path(default_path)
5577
+ self.log(f"📁 Using default data folder: {default_path}")
5578
+
5579
+ except Exception as e:
5580
+ self.log(f"⚠️ Data location dialog error: {e}")
5581
+ import traceback
5582
+ traceback.print_exc()
5583
+
5584
+ def _reinitialize_with_new_data_path(self):
5585
+ """Re-initialize managers after user changes data path."""
5586
+ try:
5587
+ # Close existing database connection
5588
+ if hasattr(self, 'db_manager') and self.db_manager:
5589
+ self.db_manager.close()
5590
+
5591
+ # Re-initialize database manager
5592
+ from modules.database_manager import DatabaseManager
5593
+ self.db_manager = DatabaseManager(
5594
+ db_path=str(self.user_data_path / "resources" / "supervertaler.db"),
5595
+ log_callback=self.log
5596
+ )
5597
+ self.db_manager.connect()
5598
+
5599
+ # Re-initialize TM database
5600
+ from modules.translation_memory import TMDatabase
5601
+ self.tm_database = TMDatabase(
5602
+ source_lang=None,
5603
+ target_lang=None,
5604
+ db_path=str(self.user_data_path / "resources" / "supervertaler.db"),
5605
+ log_callback=self.log
5606
+ )
5607
+
5608
+ # Re-initialize TM metadata manager
5609
+ from modules.tm_metadata_manager import TMMetadataManager
5610
+ self.tm_metadata_mgr = TMMetadataManager(self.db_manager, self.log)
5611
+
5612
+ # Update other managers
5613
+ self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
5614
+ self.fr_history = FindReplaceHistory(str(self.user_data_path))
5615
+ self.shortcut_manager = ShortcutManager(Path(self.user_data_path) / "shortcuts.json")
5616
+ self.voice_command_manager = VoiceCommandManager(self.user_data_path, main_window=self)
5617
+
5618
+ # Update theme manager
5619
+ from modules.theme_manager import ThemeManager
5620
+ self.theme_manager = ThemeManager(self.user_data_path)
5621
+ self.theme_manager.apply_theme(QApplication.instance())
5622
+
5623
+ # Update recent projects file path
5624
+ self.recent_projects_file = self.user_data_path / "recent_projects.json"
5625
+
5626
+ self.log(f"✅ Re-initialized all managers with new data path")
5627
+
5628
+ except Exception as e:
5629
+ self.log(f"⚠️ Error re-initializing managers: {e}")
5630
+ import traceback
5631
+ traceback.print_exc()
5632
+
5468
5633
  def _show_first_run_welcome(self):
5469
5634
  """Show welcome message and Features tab on first run."""
5470
5635
  try:
@@ -5886,7 +6051,7 @@ class SupervertalerQt(QMainWindow):
5886
6051
  quickmenu_items = lib.get_quickmenu_grid_prompts() or []
5887
6052
 
5888
6053
  if not quickmenu_items:
5889
- self.log("⚠️ No QuickMenu prompts available. Add prompts with 'Show in Supervertaler QuickMenu' enabled.")
6054
+ self.log("⚠️ No QuickMenu prompts available. Add prompts with 'Show in QuickMenu' enabled.")
5890
6055
  return
5891
6056
 
5892
6057
  # Find the currently focused widget (source or target cell)
@@ -6567,7 +6732,7 @@ class SupervertalerQt(QMainWindow):
6567
6732
  go_resources_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(1) if hasattr(self, 'main_tabs') else None)
6568
6733
  nav_menu.addAction(go_resources_action)
6569
6734
 
6570
- go_prompt_manager_action = QAction("🤖 &Prompt Manager", self)
6735
+ go_prompt_manager_action = QAction(" &QuickMenu", self)
6571
6736
  go_prompt_manager_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(2) if hasattr(self, 'main_tabs') else None)
6572
6737
  nav_menu.addAction(go_prompt_manager_action)
6573
6738
 
@@ -7046,13 +7211,13 @@ class SupervertalerQt(QMainWindow):
7046
7211
  resources_tab = self.create_resources_tab()
7047
7212
  self.main_tabs.addTab(resources_tab, "🗂️ Resources")
7048
7213
 
7049
- # ===== 3. PROMPT MANAGER TAB =====
7050
- # Unified Prompt Library + AI Assistant
7214
+ # ===== 3. QUICKMENU TAB =====
7215
+ # Unified QuickMenu system: folder structure = menu hierarchy for in-app + global access
7051
7216
  from modules.unified_prompt_manager_qt import UnifiedPromptManagerQt
7052
7217
  prompt_widget = QWidget()
7053
7218
  self.prompt_manager_qt = UnifiedPromptManagerQt(self, standalone=False)
7054
7219
  self.prompt_manager_qt.create_tab(prompt_widget)
7055
- self.main_tabs.addTab(prompt_widget, "🤖 Prompt Manager")
7220
+ self.main_tabs.addTab(prompt_widget, " QuickMenu")
7056
7221
 
7057
7222
  # Keep backward compatibility reference
7058
7223
  self.document_views_widget = self.main_tabs
@@ -7112,7 +7277,7 @@ class SupervertalerQt(QMainWindow):
7112
7277
  layout.addWidget(desc)
7113
7278
 
7114
7279
  # Initialize NT manager
7115
- nt_path = self.user_data_path / "Translation_Resources" / "Non-translatables"
7280
+ nt_path = self.user_data_path / "resources" / "non_translatables"
7116
7281
  nt_path.mkdir(parents=True, exist_ok=True)
7117
7282
  self.nt_manager = NonTranslatablesManager(str(nt_path), self.log)
7118
7283
 
@@ -8456,7 +8621,7 @@ class SupervertalerQt(QMainWindow):
8456
8621
  from modules.superbrowser import SuperbrowserWidget
8457
8622
 
8458
8623
  # Create and return the Superbrowser widget
8459
- superbrowser_widget = SuperbrowserWidget(parent=self)
8624
+ superbrowser_widget = SuperbrowserWidget(parent=self, user_data_path=self.user_data_path)
8460
8625
 
8461
8626
  return superbrowser_widget
8462
8627
 
@@ -8743,7 +8908,7 @@ class SupervertalerQt(QMainWindow):
8743
8908
 
8744
8909
  # Create new Superlookup instance for detached window
8745
8910
  # Or move the existing one - better to create new to avoid widget parenting issues
8746
- detached_lookup = SuperlookupTab(self.lookup_detached_window)
8911
+ detached_lookup = SuperlookupTab(self.lookup_detached_window, user_data_path=self.user_data_path)
8747
8912
 
8748
8913
  # Explicitly copy theme_manager reference
8749
8914
  detached_lookup.theme_manager = self.theme_manager
@@ -8880,7 +9045,7 @@ class SupervertalerQt(QMainWindow):
8880
9045
  # Superdocs removed (online GitBook will be used instead)
8881
9046
 
8882
9047
  print("[DEBUG] About to create SuperlookupTab...")
8883
- lookup_tab = SuperlookupTab(self)
9048
+ lookup_tab = SuperlookupTab(self, user_data_path=self.user_data_path)
8884
9049
  print("[DEBUG] SuperlookupTab created successfully")
8885
9050
  self.lookup_tab = lookup_tab # Store reference for later use
8886
9051
  modules_tabs.addTab(lookup_tab, "🔍 Superlookup")
@@ -9324,58 +9489,139 @@ class SupervertalerQt(QMainWindow):
9324
9489
  # Show progress dialog
9325
9490
  from PyQt6.QtWidgets import QProgressDialog
9326
9491
  from PyQt6.QtCore import Qt
9492
+ import time
9327
9493
 
9328
- progress = QProgressDialog("Importing TMX file...", None, 0, 0, self)
9494
+ # Create progress dialog with cancel button
9495
+ progress = QProgressDialog("Counting translation units...", "Cancel", 0, 100, self)
9329
9496
  progress.setWindowTitle("TMX Import")
9330
9497
  progress.setWindowModality(Qt.WindowModality.WindowModal)
9331
9498
  progress.setMinimumDuration(0)
9499
+ progress.setAutoClose(False)
9500
+ progress.setAutoReset(False)
9332
9501
  progress.setValue(0)
9333
9502
  progress.show()
9334
9503
  QApplication.processEvents()
9335
9504
 
9505
+ # Track if user cancelled
9506
+ cancelled = False
9507
+ start_time = time.time()
9508
+
9509
+ def update_progress(current, total, message):
9510
+ nonlocal cancelled
9511
+ if progress.wasCanceled():
9512
+ cancelled = True
9513
+ return
9514
+
9515
+ if total > 0:
9516
+ percentage = int((current / total) * 100)
9517
+ progress.setValue(percentage)
9518
+
9519
+ # Calculate time stats
9520
+ elapsed = time.time() - start_time
9521
+ if current > 0:
9522
+ rate = current / elapsed
9523
+ remaining = (total - current) / rate if rate > 0 else 0
9524
+
9525
+ # Format message with stats
9526
+ time_info = f" (~{int(remaining/60)}m {int(remaining%60)}s remaining)" if remaining > 10 else ""
9527
+ progress.setLabelText(f"{message}\nRate: {int(rate):,} entries/sec{time_info}")
9528
+ else:
9529
+ progress.setLabelText(message)
9530
+ else:
9531
+ progress.setLabelText(message)
9532
+
9533
+ QApplication.processEvents()
9534
+
9336
9535
  if choice == 'import_strip':
9337
9536
  # Import with variant stripping
9338
9537
  tm_id, count = self.tm_database.load_tmx_file(
9339
9538
  file_path, source_lang, target_lang,
9340
- tm_name=None, read_only=False
9539
+ tm_name=None, read_only=False, progress_callback=update_progress
9341
9540
  )
9342
9541
  progress.close()
9343
- self.log(f"Imported {count} entries (variants stripped: {compat['tmx_source']}, {compat['tmx_target']} → {source_lang}, {target_lang})")
9344
- QMessageBox.information(
9345
- self, "Import Complete",
9346
- f"TMX imported successfully!\n\nEntries: {count}\n"
9347
- f"Mapped: {compat['tmx_source']}, {compat['tmx_target']} → {source_lang}, {target_lang}"
9348
- )
9542
+
9543
+ if not cancelled:
9544
+ self.log(f"Imported {count} entries (variants stripped: {compat['tmx_source']}, {compat['tmx_target']} → {source_lang}, {target_lang})")
9545
+ QMessageBox.information(
9546
+ self, "Import Complete",
9547
+ f"TMX imported successfully!\n\nEntries: {count:,}\n"
9548
+ f"Mapped: {compat['tmx_source']}, {compat['tmx_target']} → {source_lang}, {target_lang}"
9549
+ )
9550
+ else:
9551
+ self.log("TMX import cancelled by user")
9552
+ QMessageBox.information(self, "Import Cancelled", f"Import stopped.\n\nPartial entries may have been imported: {count:,}")
9349
9553
  elif choice == 'create_new':
9350
9554
  # Create new TM with variant languages
9351
9555
  tm_id, count = self.tm_database.load_tmx_file(
9352
9556
  file_path, compat['tmx_source'], compat['tmx_target'],
9353
- tm_name=None, read_only=False
9557
+ tm_name=None, read_only=False, progress_callback=update_progress
9354
9558
  )
9355
9559
  progress.close()
9356
- self.log(f"Created new TM with variants: {compat['tmx_source']}, {compat['tmx_target']}")
9357
- QMessageBox.information(
9358
- self, "Import Complete",
9359
- f"New TM created!\n\nTM ID: {tm_id}\nEntries: {count}\n"
9360
- f"Languages: {compat['tmx_source']}, {compat['tmx_target']}"
9361
- )
9560
+
9561
+ if not cancelled:
9562
+ self.log(f"Created new TM with variants: {compat['tmx_source']}, {compat['tmx_target']}")
9563
+ QMessageBox.information(
9564
+ self, "Import Complete",
9565
+ f"New TM created!\n\nTM ID: {tm_id}\nEntries: {count:,}\n"
9566
+ f"Languages: {compat['tmx_source']}, {compat['tmx_target']}"
9567
+ )
9568
+ else:
9569
+ self.log("TMX import cancelled by user")
9570
+ QMessageBox.information(self, "Import Cancelled", f"Import stopped.\n\nPartial entries may have been imported: {count:,}")
9362
9571
  else:
9363
- # Exact match - proceed normally with progress
9572
+ # Exact match - proceed normally with real-time progress
9364
9573
  from PyQt6.QtWidgets import QProgressDialog
9365
9574
  from PyQt6.QtCore import Qt
9575
+ import time
9366
9576
 
9367
- progress = QProgressDialog("Importing TMX file...", None, 0, 0, self)
9577
+ progress = QProgressDialog("Counting translation units...", "Cancel", 0, 100, self)
9368
9578
  progress.setWindowTitle("TMX Import")
9369
9579
  progress.setWindowModality(Qt.WindowModality.WindowModal)
9370
9580
  progress.setMinimumDuration(0)
9581
+ progress.setAutoClose(False)
9582
+ progress.setAutoReset(False)
9371
9583
  progress.setValue(0)
9372
9584
  progress.show()
9373
9585
  QApplication.processEvents()
9374
9586
 
9375
- tm_id, count = self.tm_database.load_tmx_file(file_path, source_lang, target_lang)
9587
+ cancelled = False
9588
+ start_time = time.time()
9589
+
9590
+ def update_progress(current, total, message):
9591
+ nonlocal cancelled
9592
+ if progress.wasCanceled():
9593
+ cancelled = True
9594
+ return
9595
+
9596
+ if total > 0:
9597
+ percentage = int((current / total) * 100)
9598
+ progress.setValue(percentage)
9599
+
9600
+ # Calculate time stats
9601
+ elapsed = time.time() - start_time
9602
+ if current > 0:
9603
+ rate = current / elapsed
9604
+ remaining = (total - current) / rate if rate > 0 else 0
9605
+
9606
+ # Format message with stats
9607
+ time_info = f" (~{int(remaining/60)}m {int(remaining%60)}s remaining)" if remaining > 10 else ""
9608
+ progress.setLabelText(f"{message}\nRate: {int(rate):,} entries/sec{time_info}")
9609
+ else:
9610
+ progress.setLabelText(message)
9611
+ else:
9612
+ progress.setLabelText(message)
9613
+
9614
+ QApplication.processEvents()
9615
+
9616
+ tm_id, count = self.tm_database.load_tmx_file(file_path, source_lang, target_lang, progress_callback=update_progress)
9376
9617
  progress.close()
9377
- self.log(f"Successfully imported {count} entries from TMX file")
9378
- QMessageBox.information(self, "Import Complete", f"TMX imported!\n\nEntries: {count}\nTM ID: {tm_id}")
9618
+
9619
+ if not cancelled:
9620
+ self.log(f"Successfully imported {count} entries from TMX file")
9621
+ QMessageBox.information(self, "Import Complete", f"TMX imported!\n\nEntries: {count:,}\nTM ID: {tm_id}")
9622
+ else:
9623
+ self.log("TMX import cancelled by user")
9624
+ QMessageBox.information(self, "Import Cancelled", f"Import stopped.\n\nPartial entries may have been imported: {count:,}")
9379
9625
 
9380
9626
  except Exception as e:
9381
9627
  self.log(f"Error importing TMX file: {e}")
@@ -10507,7 +10753,7 @@ class SupervertalerQt(QMainWindow):
10507
10753
  if reply2 == QMessageBox.StandardButton.Yes and self.tm_database:
10508
10754
  # Clear TM entries
10509
10755
  import sqlite3
10510
- db_path = self.user_data_path / "Translation_Resources" / "supervertaler.db"
10756
+ db_path = self.user_data_path / "resources" / "supervertaler.db"
10511
10757
  conn = sqlite3.connect(str(db_path))
10512
10758
  cursor = conn.cursor()
10513
10759
  cursor.execute("DELETE FROM translation_units")
@@ -13606,29 +13852,66 @@ class SupervertalerQt(QMainWindow):
13606
13852
  if reply != QMessageBox.StandardButton.Yes:
13607
13853
  return
13608
13854
 
13609
- # Import into existing TM with progress dialog
13855
+ # Import into existing TM with real-time progress dialog
13610
13856
  from PyQt6.QtWidgets import QProgressDialog
13611
13857
  from PyQt6.QtCore import Qt
13858
+ import time
13612
13859
 
13613
- progress = QProgressDialog("Importing TMX file...", None, 0, 0, self)
13860
+ progress = QProgressDialog("Counting translation units...", "Cancel", 0, 100, self)
13614
13861
  progress.setWindowTitle("TMX Import")
13615
13862
  progress.setWindowModality(Qt.WindowModality.WindowModal)
13616
13863
  progress.setMinimumDuration(0)
13864
+ progress.setAutoClose(False)
13865
+ progress.setAutoReset(False)
13617
13866
  progress.setValue(0)
13618
13867
  progress.show()
13619
13868
  QApplication.processEvents()
13620
13869
 
13870
+ cancelled = False
13871
+ start_time = time.time()
13872
+
13873
+ def update_progress(current, total, message):
13874
+ nonlocal cancelled
13875
+ if progress.wasCanceled():
13876
+ cancelled = True
13877
+ return
13878
+
13879
+ if total > 0:
13880
+ percentage = int((current / total) * 100)
13881
+ progress.setValue(percentage)
13882
+
13883
+ # Calculate time stats
13884
+ elapsed = time.time() - start_time
13885
+ if current > 0:
13886
+ rate = current / elapsed
13887
+ remaining = (total - current) / rate if rate > 0 else 0
13888
+
13889
+ # Format message with stats
13890
+ time_info = f" (~{int(remaining/60)}m {int(remaining%60)}s remaining)" if remaining > 10 else ""
13891
+ progress.setLabelText(f"{message}\nRate: {int(rate):,} entries/sec{time_info}")
13892
+ else:
13893
+ progress.setLabelText(message)
13894
+ else:
13895
+ progress.setLabelText(message)
13896
+
13897
+ QApplication.processEvents()
13898
+
13621
13899
  count = self.tm_database._load_tmx_into_db(
13622
- filepath, tm_src_lang, tm_tgt_lang, target_tm_id, strip_variants=strip_variants
13900
+ filepath, tm_src_lang, tm_tgt_lang, target_tm_id,
13901
+ strip_variants=strip_variants, progress_callback=update_progress
13623
13902
  )
13624
13903
 
13625
13904
  progress.close()
13626
13905
 
13627
- # Update entry count
13628
- tm_metadata_mgr.update_entry_count(target_tm_id)
13629
- self.log(f"Imported {count} entries into TM '{target_tm_id}'")
13630
-
13631
- QMessageBox.information(self, "Success", f"TMX imported successfully into TM '{target_tm_id}'!\n\nEntries imported: {count}")
13906
+ if not cancelled:
13907
+ # Update entry count
13908
+ tm_metadata_mgr.update_entry_count(target_tm_id)
13909
+ self.log(f"Imported {count} entries into TM '{target_tm_id}'")
13910
+
13911
+ QMessageBox.information(self, "Success", f"TMX imported successfully into TM '{target_tm_id}'!\n\nEntries imported: {count:,}")
13912
+ else:
13913
+ self.log(f"TMX import cancelled by user. Partial import: {count} entries")
13914
+ QMessageBox.information(self, "Import Cancelled", f"Import stopped.\n\nPartial entries imported: {count:,}")
13632
13915
  refresh_callback()
13633
13916
  else:
13634
13917
  QMessageBox.critical(self, "Error", "TM database not available")
@@ -14408,7 +14691,7 @@ class SupervertalerQt(QMainWindow):
14408
14691
  auto_markdown_cb.setChecked(general_settings.get('auto_generate_markdown', False))
14409
14692
  auto_markdown_cb.setToolTip(
14410
14693
  "Automatically convert imported documents to markdown format\n"
14411
- "for AI Assistant access in user_data_private/AI_Assistant/current_document/"
14694
+ "for AI Assistant access in user_data_private/ai_assistant/current_document/"
14412
14695
  )
14413
14696
  behavior_layout.addWidget(auto_markdown_cb)
14414
14697
 
@@ -14620,7 +14903,7 @@ class SupervertalerQt(QMainWindow):
14620
14903
 
14621
14904
  def _create_general_settings_tab(self):
14622
14905
  """Create General Settings tab content"""
14623
- from PyQt6.QtWidgets import QCheckBox, QGroupBox, QPushButton
14906
+ from PyQt6.QtWidgets import QCheckBox, QGroupBox, QPushButton, QLineEdit, QFileDialog
14624
14907
 
14625
14908
  tab = QWidget()
14626
14909
  layout = QVBoxLayout(tab)
@@ -14650,6 +14933,119 @@ class SupervertalerQt(QMainWindow):
14650
14933
  startup_group.setLayout(startup_layout)
14651
14934
  layout.addWidget(startup_group)
14652
14935
 
14936
+ # Data Folder Location group
14937
+ data_folder_group = QGroupBox("📁 Data Folder Location")
14938
+ data_folder_layout = QVBoxLayout()
14939
+
14940
+ data_folder_info = QLabel(
14941
+ "Your translation memories, glossaries, prompts, and settings are stored here:"
14942
+ )
14943
+ data_folder_info.setWordWrap(True)
14944
+ data_folder_layout.addWidget(data_folder_info)
14945
+
14946
+ # Path display with browse button
14947
+ path_row = QHBoxLayout()
14948
+
14949
+ data_path_edit = QLineEdit()
14950
+ data_path_edit.setText(str(self.user_data_path))
14951
+ data_path_edit.setReadOnly(True)
14952
+ data_path_edit.setToolTip("Current data folder location")
14953
+ path_row.addWidget(data_path_edit)
14954
+
14955
+ change_path_btn = QPushButton("Change...")
14956
+ change_path_btn.setToolTip("Choose a different location for your data folder")
14957
+
14958
+ def change_data_path():
14959
+ """Let user change their data folder location."""
14960
+ from PyQt6.QtWidgets import QMessageBox
14961
+
14962
+ folder = QFileDialog.getExistingDirectory(
14963
+ self,
14964
+ "Choose New Data Folder Location",
14965
+ str(Path.home())
14966
+ )
14967
+ if folder:
14968
+ folder_path = Path(folder)
14969
+ # Append "Supervertaler" if user didn't include it
14970
+ if folder_path.name != "Supervertaler":
14971
+ folder_path = folder_path / "Supervertaler"
14972
+
14973
+ # Warn if switching to a folder with existing data
14974
+ if folder_path.exists() and folder_path != self.user_data_path:
14975
+ has_content = any(folder_path.iterdir())
14976
+ if has_content:
14977
+ reply = QMessageBox.question(
14978
+ self,
14979
+ "Existing Data Found",
14980
+ f"The folder already contains data:\n{folder_path}\n\n"
14981
+ "Do you want to use this existing data?",
14982
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel
14983
+ )
14984
+ if reply != QMessageBox.StandardButton.Yes:
14985
+ return
14986
+
14987
+ # Create the folder
14988
+ folder_path.mkdir(parents=True, exist_ok=True)
14989
+
14990
+ # Ask if they want to copy existing data
14991
+ if self.user_data_path.exists() and self.user_data_path != folder_path:
14992
+ reply = QMessageBox.question(
14993
+ self,
14994
+ "Copy Existing Data?",
14995
+ f"Would you like to copy your existing data to the new location?\n\n"
14996
+ f"From: {self.user_data_path}\n"
14997
+ f"To: {folder_path}\n\n"
14998
+ "This will NOT delete the original data.",
14999
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
15000
+ )
15001
+ if reply == QMessageBox.StandardButton.Yes:
15002
+ import shutil
15003
+ try:
15004
+ for item in self.user_data_path.iterdir():
15005
+ dest = folder_path / item.name
15006
+ if item.is_dir():
15007
+ shutil.copytree(item, dest, dirs_exist_ok=True)
15008
+ else:
15009
+ shutil.copy2(item, dest)
15010
+ self.log(f"✅ Copied data to new location")
15011
+ except Exception as e:
15012
+ QMessageBox.warning(self, "Copy Error", f"Failed to copy data: {e}")
15013
+
15014
+ # Save the new path
15015
+ save_user_data_path(folder_path)
15016
+ data_path_edit.setText(str(folder_path))
15017
+
15018
+ # Update and reinitialize
15019
+ self.user_data_path = folder_path
15020
+ self._reinitialize_with_new_data_path()
15021
+
15022
+ QMessageBox.information(
15023
+ self,
15024
+ "Data Folder Changed",
15025
+ f"Data folder changed to:\n{folder_path}\n\n"
15026
+ "All managers have been reinitialized."
15027
+ )
15028
+
15029
+ change_path_btn.clicked.connect(change_data_path)
15030
+ path_row.addWidget(change_path_btn)
15031
+
15032
+ open_folder_btn = QPushButton("Open")
15033
+ open_folder_btn.setToolTip("Open this folder in your file manager")
15034
+ open_folder_btn.clicked.connect(lambda: os.startfile(str(self.user_data_path)) if sys.platform == 'win32'
15035
+ else subprocess.run(['xdg-open' if sys.platform == 'linux' else 'open', str(self.user_data_path)]))
15036
+ path_row.addWidget(open_folder_btn)
15037
+
15038
+ data_folder_layout.addLayout(path_row)
15039
+
15040
+ data_folder_tip = QLabel(
15041
+ "💡 This folder persists across updates. Back it up regularly!"
15042
+ )
15043
+ data_folder_tip.setStyleSheet("color: #666; font-size: 9pt;")
15044
+ data_folder_layout.addWidget(data_folder_tip)
15045
+
15046
+ data_folder_group.setLayout(data_folder_layout)
15047
+ layout.addWidget(data_folder_group)
15048
+
14653
15049
  # Sound Effects group
14654
15050
  sound_group = QGroupBox("🔊 Sound Effects")
14655
15051
  sound_layout = QVBoxLayout()
@@ -16685,7 +17081,7 @@ class SupervertalerQt(QMainWindow):
16685
17081
  prompt_text = self.prompt_manager_qt.get_system_template(mode_key)
16686
17082
  else:
16687
17083
  # Fallback: load from JSON file
16688
- system_prompts_file = self.user_data_path / "Prompt_Library" / "system_prompts_layer1.json"
17084
+ system_prompts_file = self.user_data_path / "prompt_library" / "system_prompts_layer1.json"
16689
17085
  if system_prompts_file.exists():
16690
17086
  import json
16691
17087
  with open(system_prompts_file, 'r', encoding='utf-8') as f:
@@ -16714,7 +17110,7 @@ class SupervertalerQt(QMainWindow):
16714
17110
 
16715
17111
  # Always save to JSON file
16716
17112
  import json
16717
- system_prompts_file = self.user_data_path / "Prompt_Library" / "system_prompts_layer1.json"
17113
+ system_prompts_file = self.user_data_path / "prompt_library" / "system_prompts_layer1.json"
16718
17114
  system_prompts_file.parent.mkdir(parents=True, exist_ok=True)
16719
17115
 
16720
17116
  # Load existing prompts
@@ -29775,7 +30171,7 @@ class SupervertalerQt(QMainWindow):
29775
30171
  layout = QVBoxLayout(dialog)
29776
30172
 
29777
30173
  # Header
29778
- header = QLabel("<h3>✅ AI-Powered Translation Proofreading</h3>")
30174
+ header = QLabel("<h3>✅ AI-Enhanced Translation Proofreading</h3>")
29779
30175
  layout.addWidget(header)
29780
30176
 
29781
30177
  info_label = QLabel(
@@ -36493,7 +36889,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36493
36889
  subtab_name: Name of the sub-tab to navigate to (e.g., "AI Settings")
36494
36890
  """
36495
36891
  if hasattr(self, 'main_tabs'):
36496
- # Main tabs: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
36892
+ # Main tabs: Grid=0, Resources=1, QuickMenu=2, Tools=3, Settings=4
36497
36893
  self.main_tabs.setCurrentIndex(4)
36498
36894
 
36499
36895
  # Navigate to specific sub-tab if requested
@@ -36514,7 +36910,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36514
36910
  def _go_to_superlookup(self):
36515
36911
  """Navigate to Superlookup in Tools tab"""
36516
36912
  if hasattr(self, 'main_tabs'):
36517
- # Main tabs: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
36913
+ # Main tabs: Grid=0, Resources=1, QuickMenu=2, Tools=3, Settings=4
36518
36914
  self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
36519
36915
  # Then switch to Superlookup sub-tab
36520
36916
  if hasattr(self, 'modules_tabs'):
@@ -36527,7 +36923,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36527
36923
  def _navigate_to_tool(self, tool_name: str):
36528
36924
  """Navigate to a specific tool in the Tools tab"""
36529
36925
  if hasattr(self, 'main_tabs'):
36530
- # Main tabs: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
36926
+ # Main tabs: Grid=0, Resources=1, QuickMenu=2, Tools=3, Settings=4
36531
36927
  self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
36532
36928
  # Then switch to the specific tool sub-tab
36533
36929
  if hasattr(self, 'modules_tabs'):
@@ -36760,7 +37156,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36760
37156
  file_path, _ = QFileDialog.getOpenFileName(
36761
37157
  dialog,
36762
37158
  "Select TMX File",
36763
- str(self.user_data_path / "Translation_Resources"),
37159
+ str(self.user_data_path / "resources"),
36764
37160
  "TMX Files (*.tmx);;All Files (*.*)"
36765
37161
  )
36766
37162
  if file_path:
@@ -37546,7 +37942,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37546
37942
  layout.addWidget(title)
37547
37943
 
37548
37944
  # Description
37549
- desc = QLabel("<p><b>AI-powered tool for translators & writers</b></p>")
37945
+ desc = QLabel("<p><b>AI-enhanced tool for translators & writers</b></p>")
37550
37946
  desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
37551
37947
  layout.addWidget(desc)
37552
37948
 
@@ -41333,9 +41729,10 @@ class SuperlookupTab(QWidget):
41333
41729
  Works anywhere on your computer: in CAT tools, browsers, Word, any text box
41334
41730
  """
41335
41731
 
41336
- def __init__(self, parent=None):
41732
+ def __init__(self, parent=None, user_data_path=None):
41337
41733
  super().__init__(parent)
41338
41734
  self.main_window = parent # Store reference to main window for database access
41735
+ self.user_data_path = user_data_path # Store user data path for web cache
41339
41736
 
41340
41737
  print("[Superlookup] SuperlookupTab.__init__ called")
41341
41738
 
@@ -41507,31 +41904,10 @@ class SuperlookupTab(QWidget):
41507
41904
 
41508
41905
  layout.addLayout(search_row)
41509
41906
 
41510
- # Options row (direction + language filters) - compact single line
41907
+ # Options row (language filters only) - compact single line
41511
41908
  options_layout = QHBoxLayout()
41512
41909
  options_layout.setSpacing(8)
41513
41910
 
41514
- # Search direction selector
41515
- direction_label = QLabel("Direction:")
41516
- direction_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
41517
- options_layout.addWidget(direction_label)
41518
-
41519
- self.direction_both = CheckmarkRadioButton("Both")
41520
- self.direction_both.setChecked(True) # Default
41521
- self.direction_both.setToolTip("Search in both source and target text (bidirectional)")
41522
- options_layout.addWidget(self.direction_both)
41523
-
41524
- self.direction_source = CheckmarkRadioButton("Source")
41525
- self.direction_source.setToolTip("Search only in source language text")
41526
- options_layout.addWidget(self.direction_source)
41527
-
41528
- self.direction_target = CheckmarkRadioButton("Target")
41529
- self.direction_target.setToolTip("Search only in target language text")
41530
- options_layout.addWidget(self.direction_target)
41531
-
41532
- # Spacer
41533
- options_layout.addSpacing(15)
41534
-
41535
41911
  # Language filter dropdowns
41536
41912
  from_label = QLabel("From:")
41537
41913
  from_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
@@ -42043,7 +42419,11 @@ class SuperlookupTab(QWidget):
42043
42419
  self.web_engine_available = True
42044
42420
 
42045
42421
  # Create persistent profile for login/cookie storage
42046
- storage_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_data', 'web_cache')
42422
+ if self.user_data_path:
42423
+ storage_path = os.path.join(str(self.user_data_path), 'web_cache')
42424
+ else:
42425
+ # Fallback to script directory if user_data_path not provided
42426
+ storage_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_data', 'web_cache')
42047
42427
  os.makedirs(storage_path, exist_ok=True)
42048
42428
  self.web_profile = QWebEngineProfile("SuperlookupProfile", self)
42049
42429
  self.web_profile.setPersistentStoragePath(storage_path)
@@ -43307,17 +43687,10 @@ class SuperlookupTab(QWidget):
43307
43687
  def get_search_direction(self):
43308
43688
  """Get the current search direction setting.
43309
43689
 
43310
- Returns:
43311
- 'both' - bidirectional search (source and target)
43312
- 'source' - search source text only
43313
- 'target' - search target text only
43690
+ Always returns 'both' for maximum search coverage.
43691
+ Smart language swapping ensures results are always shown in correct order.
43314
43692
  """
43315
- if hasattr(self, 'direction_source') and self.direction_source.isChecked():
43316
- return 'source'
43317
- elif hasattr(self, 'direction_target') and self.direction_target.isChecked():
43318
- return 'target'
43319
- else:
43320
- return 'both'
43693
+ return 'both'
43321
43694
 
43322
43695
  def _on_language_changed(self):
43323
43696
  """Handle language dropdown change - update Web Resources info"""
@@ -45074,7 +45447,7 @@ class SuperlookupTab(QWidget):
45074
45447
  print(f"[Superlookup] Has main_tabs: {hasattr(main_window, 'main_tabs')}")
45075
45448
 
45076
45449
  # Switch to Tools tab (main_tabs index 3)
45077
- # Tab structure: Grid=0, Resources=1, Prompt Manager=2, Tools=3, Settings=4
45450
+ # Tab structure: Grid=0, Resources=1, QuickMenu=2, Tools=3, Settings=4
45078
45451
  if hasattr(main_window, 'main_tabs'):
45079
45452
  print(f"[Superlookup] Current main_tab index: {main_window.main_tabs.currentIndex()}")
45080
45453
  main_window.main_tabs.setCurrentIndex(3) # Tools tab