supervertaler 1.9.146__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.146"
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,21 +61,148 @@ def get_resource_path(relative_path: str) -> Path:
61
61
  return base_path / relative_path
62
62
 
63
63
 
64
+ def get_config_pointer_path() -> Path:
65
+ """
66
+ Get path to the config pointer file that stores the user's chosen data location.
67
+
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
72
+
73
+ This small file just contains a pointer to where the real data lives.
74
+ """
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"
82
+ else:
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"
86
+
87
+
88
+ def get_default_user_data_path() -> Path:
89
+ """
90
+ Get the DEFAULT user data path (visible in home folder).
91
+
92
+ - Windows: C:/Users/Username/Supervertaler/
93
+ - macOS: ~/Supervertaler/
94
+ - Linux: ~/Supervertaler/
95
+
96
+ This is intentionally a visible, easily-accessible location.
97
+ """
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)
105
+
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()
117
+
118
+ if not pointer_path.exists():
119
+ return None
120
+
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
130
+
131
+ return None
132
+
133
+
64
134
  def get_user_data_path() -> Path:
65
135
  """
66
136
  Get the path to user data directory.
67
137
 
68
- In frozen builds (EXE): 'user_data' folder next to the EXE
69
- In development: 'user_data' folder next to the script
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)
142
+
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
70
147
 
71
- The build process copies user_data directly next to the EXE for easy access.
148
+ This ensures data survives pip upgrades, EXE updates, and is easy to find/backup.
72
149
  """
73
- if getattr(sys, 'frozen', False):
74
- # Frozen build: user_data is next to the EXE (copied by build script)
75
- return Path(sys.executable).parent / "user_data"
76
- else:
77
- # Development: user_data next to script
78
- return Path(__file__).parent / "user_data"
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
173
+
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.
182
+
183
+ Returns True if:
184
+ - No config pointer file exists, AND
185
+ - Default data location doesn't have existing content
186
+
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():
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
79
206
 
80
207
  import threading
81
208
  import time # For delays in Superlookup
@@ -5167,35 +5294,48 @@ class SupervertalerQt(QMainWindow):
5167
5294
  # ============================================================================
5168
5295
  # USER DATA PATH INITIALIZATION
5169
5296
  # ============================================================================
5170
- # Set up user data paths - handles both development and frozen (EXE) builds
5171
- # In frozen builds, user_data is copied directly next to the EXE by the build 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
5172
5302
  from modules.database_manager import DatabaseManager
5173
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
+
5174
5308
  if ENABLE_PRIVATE_FEATURES:
5175
5309
  # Developer mode: use private folder (git-ignored)
5176
5310
  self.user_data_path = Path(__file__).parent / "user_data_private"
5311
+ self._needs_data_location_dialog = False # Dev mode doesn't need dialog
5177
5312
  else:
5178
- # Normal mode: use the helper function
5313
+ # Normal mode: use unified system (same for pip, EXE, etc.)
5179
5314
  self.user_data_path = get_user_data_path()
5180
5315
 
5181
5316
  # Ensure user_data directory exists (creates empty folder if missing)
5182
- 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)
5183
5320
 
5184
5321
  print(f"[Data Paths] User data: {self.user_data_path}")
5185
5322
 
5186
5323
  # Database Manager for Termbases
5187
5324
  self.db_manager = DatabaseManager(
5188
- db_path=str(self.user_data_path / "Translation_Resources" / "supervertaler.db"),
5325
+ db_path=str(self.user_data_path / "resources" / "supervertaler.db"),
5189
5326
  log_callback=self.log
5190
5327
  )
5191
- 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()
5192
5332
 
5193
5333
  # TM Database - Initialize early so Superlookup works without a project loaded
5194
5334
  from modules.translation_memory import TMDatabase
5195
5335
  self.tm_database = TMDatabase(
5196
5336
  source_lang=None, # Will be set when project is loaded
5197
5337
  target_lang=None, # Will be set when project is loaded
5198
- db_path=str(self.user_data_path / "Translation_Resources" / "supervertaler.db"),
5338
+ db_path=str(self.user_data_path / "resources" / "supervertaler.db"),
5199
5339
  log_callback=self.log
5200
5340
  )
5201
5341
 
@@ -5312,11 +5452,184 @@ class SupervertalerQt(QMainWindow):
5312
5452
  from PyQt6.QtCore import QTimer
5313
5453
  QTimer.singleShot(2000, lambda: self._check_for_new_models(force=False)) # 2 second delay
5314
5454
 
5315
- # First-run check - show Features tab to new users
5316
- 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):
5317
5460
  from PyQt6.QtCore import QTimer
5318
5461
  QTimer.singleShot(500, self._show_first_run_welcome)
5319
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
+
5320
5633
  def _show_first_run_welcome(self):
5321
5634
  """Show welcome message and Features tab on first run."""
5322
5635
  try:
@@ -5738,7 +6051,7 @@ class SupervertalerQt(QMainWindow):
5738
6051
  quickmenu_items = lib.get_quickmenu_grid_prompts() or []
5739
6052
 
5740
6053
  if not quickmenu_items:
5741
- 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.")
5742
6055
  return
5743
6056
 
5744
6057
  # Find the currently focused widget (source or target cell)
@@ -6419,7 +6732,7 @@ class SupervertalerQt(QMainWindow):
6419
6732
  go_resources_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(1) if hasattr(self, 'main_tabs') else None)
6420
6733
  nav_menu.addAction(go_resources_action)
6421
6734
 
6422
- go_prompt_manager_action = QAction("🤖 &Prompt Manager", self)
6735
+ go_prompt_manager_action = QAction(" &QuickMenu", self)
6423
6736
  go_prompt_manager_action.triggered.connect(lambda: self.main_tabs.setCurrentIndex(2) if hasattr(self, 'main_tabs') else None)
6424
6737
  nav_menu.addAction(go_prompt_manager_action)
6425
6738
 
@@ -6898,13 +7211,13 @@ class SupervertalerQt(QMainWindow):
6898
7211
  resources_tab = self.create_resources_tab()
6899
7212
  self.main_tabs.addTab(resources_tab, "🗂️ Resources")
6900
7213
 
6901
- # ===== 3. PROMPT MANAGER TAB =====
6902
- # Unified Prompt Library + AI Assistant
7214
+ # ===== 3. QUICKMENU TAB =====
7215
+ # Unified QuickMenu system: folder structure = menu hierarchy for in-app + global access
6903
7216
  from modules.unified_prompt_manager_qt import UnifiedPromptManagerQt
6904
7217
  prompt_widget = QWidget()
6905
7218
  self.prompt_manager_qt = UnifiedPromptManagerQt(self, standalone=False)
6906
7219
  self.prompt_manager_qt.create_tab(prompt_widget)
6907
- self.main_tabs.addTab(prompt_widget, "🤖 Prompt Manager")
7220
+ self.main_tabs.addTab(prompt_widget, " QuickMenu")
6908
7221
 
6909
7222
  # Keep backward compatibility reference
6910
7223
  self.document_views_widget = self.main_tabs
@@ -6964,7 +7277,7 @@ class SupervertalerQt(QMainWindow):
6964
7277
  layout.addWidget(desc)
6965
7278
 
6966
7279
  # Initialize NT manager
6967
- nt_path = self.user_data_path / "Translation_Resources" / "Non-translatables"
7280
+ nt_path = self.user_data_path / "resources" / "non_translatables"
6968
7281
  nt_path.mkdir(parents=True, exist_ok=True)
6969
7282
  self.nt_manager = NonTranslatablesManager(str(nt_path), self.log)
6970
7283
 
@@ -8308,7 +8621,7 @@ class SupervertalerQt(QMainWindow):
8308
8621
  from modules.superbrowser import SuperbrowserWidget
8309
8622
 
8310
8623
  # Create and return the Superbrowser widget
8311
- superbrowser_widget = SuperbrowserWidget(parent=self)
8624
+ superbrowser_widget = SuperbrowserWidget(parent=self, user_data_path=self.user_data_path)
8312
8625
 
8313
8626
  return superbrowser_widget
8314
8627
 
@@ -8595,7 +8908,7 @@ class SupervertalerQt(QMainWindow):
8595
8908
 
8596
8909
  # Create new Superlookup instance for detached window
8597
8910
  # Or move the existing one - better to create new to avoid widget parenting issues
8598
- detached_lookup = SuperlookupTab(self.lookup_detached_window)
8911
+ detached_lookup = SuperlookupTab(self.lookup_detached_window, user_data_path=self.user_data_path)
8599
8912
 
8600
8913
  # Explicitly copy theme_manager reference
8601
8914
  detached_lookup.theme_manager = self.theme_manager
@@ -8732,7 +9045,7 @@ class SupervertalerQt(QMainWindow):
8732
9045
  # Superdocs removed (online GitBook will be used instead)
8733
9046
 
8734
9047
  print("[DEBUG] About to create SuperlookupTab...")
8735
- lookup_tab = SuperlookupTab(self)
9048
+ lookup_tab = SuperlookupTab(self, user_data_path=self.user_data_path)
8736
9049
  print("[DEBUG] SuperlookupTab created successfully")
8737
9050
  self.lookup_tab = lookup_tab # Store reference for later use
8738
9051
  modules_tabs.addTab(lookup_tab, "🔍 Superlookup")
@@ -9176,58 +9489,139 @@ class SupervertalerQt(QMainWindow):
9176
9489
  # Show progress dialog
9177
9490
  from PyQt6.QtWidgets import QProgressDialog
9178
9491
  from PyQt6.QtCore import Qt
9492
+ import time
9179
9493
 
9180
- 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)
9181
9496
  progress.setWindowTitle("TMX Import")
9182
9497
  progress.setWindowModality(Qt.WindowModality.WindowModal)
9183
9498
  progress.setMinimumDuration(0)
9499
+ progress.setAutoClose(False)
9500
+ progress.setAutoReset(False)
9184
9501
  progress.setValue(0)
9185
9502
  progress.show()
9186
9503
  QApplication.processEvents()
9187
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
+
9188
9535
  if choice == 'import_strip':
9189
9536
  # Import with variant stripping
9190
9537
  tm_id, count = self.tm_database.load_tmx_file(
9191
9538
  file_path, source_lang, target_lang,
9192
- tm_name=None, read_only=False
9539
+ tm_name=None, read_only=False, progress_callback=update_progress
9193
9540
  )
9194
9541
  progress.close()
9195
- self.log(f"Imported {count} entries (variants stripped: {compat['tmx_source']}, {compat['tmx_target']} → {source_lang}, {target_lang})")
9196
- QMessageBox.information(
9197
- self, "Import Complete",
9198
- f"TMX imported successfully!\n\nEntries: {count}\n"
9199
- f"Mapped: {compat['tmx_source']}, {compat['tmx_target']} → {source_lang}, {target_lang}"
9200
- )
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:,}")
9201
9553
  elif choice == 'create_new':
9202
9554
  # Create new TM with variant languages
9203
9555
  tm_id, count = self.tm_database.load_tmx_file(
9204
9556
  file_path, compat['tmx_source'], compat['tmx_target'],
9205
- tm_name=None, read_only=False
9557
+ tm_name=None, read_only=False, progress_callback=update_progress
9206
9558
  )
9207
9559
  progress.close()
9208
- self.log(f"Created new TM with variants: {compat['tmx_source']}, {compat['tmx_target']}")
9209
- QMessageBox.information(
9210
- self, "Import Complete",
9211
- f"New TM created!\n\nTM ID: {tm_id}\nEntries: {count}\n"
9212
- f"Languages: {compat['tmx_source']}, {compat['tmx_target']}"
9213
- )
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:,}")
9214
9571
  else:
9215
- # Exact match - proceed normally with progress
9572
+ # Exact match - proceed normally with real-time progress
9216
9573
  from PyQt6.QtWidgets import QProgressDialog
9217
9574
  from PyQt6.QtCore import Qt
9575
+ import time
9218
9576
 
9219
- progress = QProgressDialog("Importing TMX file...", None, 0, 0, self)
9577
+ progress = QProgressDialog("Counting translation units...", "Cancel", 0, 100, self)
9220
9578
  progress.setWindowTitle("TMX Import")
9221
9579
  progress.setWindowModality(Qt.WindowModality.WindowModal)
9222
9580
  progress.setMinimumDuration(0)
9581
+ progress.setAutoClose(False)
9582
+ progress.setAutoReset(False)
9223
9583
  progress.setValue(0)
9224
9584
  progress.show()
9225
9585
  QApplication.processEvents()
9226
9586
 
9227
- 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)
9228
9617
  progress.close()
9229
- self.log(f"Successfully imported {count} entries from TMX file")
9230
- 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:,}")
9231
9625
 
9232
9626
  except Exception as e:
9233
9627
  self.log(f"Error importing TMX file: {e}")
@@ -10359,7 +10753,7 @@ class SupervertalerQt(QMainWindow):
10359
10753
  if reply2 == QMessageBox.StandardButton.Yes and self.tm_database:
10360
10754
  # Clear TM entries
10361
10755
  import sqlite3
10362
- db_path = self.user_data_path / "Translation_Resources" / "supervertaler.db"
10756
+ db_path = self.user_data_path / "resources" / "supervertaler.db"
10363
10757
  conn = sqlite3.connect(str(db_path))
10364
10758
  cursor = conn.cursor()
10365
10759
  cursor.execute("DELETE FROM translation_units")
@@ -13458,29 +13852,66 @@ class SupervertalerQt(QMainWindow):
13458
13852
  if reply != QMessageBox.StandardButton.Yes:
13459
13853
  return
13460
13854
 
13461
- # Import into existing TM with progress dialog
13855
+ # Import into existing TM with real-time progress dialog
13462
13856
  from PyQt6.QtWidgets import QProgressDialog
13463
13857
  from PyQt6.QtCore import Qt
13858
+ import time
13464
13859
 
13465
- progress = QProgressDialog("Importing TMX file...", None, 0, 0, self)
13860
+ progress = QProgressDialog("Counting translation units...", "Cancel", 0, 100, self)
13466
13861
  progress.setWindowTitle("TMX Import")
13467
13862
  progress.setWindowModality(Qt.WindowModality.WindowModal)
13468
13863
  progress.setMinimumDuration(0)
13864
+ progress.setAutoClose(False)
13865
+ progress.setAutoReset(False)
13469
13866
  progress.setValue(0)
13470
13867
  progress.show()
13471
13868
  QApplication.processEvents()
13472
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
+
13473
13899
  count = self.tm_database._load_tmx_into_db(
13474
- 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
13475
13902
  )
13476
13903
 
13477
13904
  progress.close()
13478
13905
 
13479
- # Update entry count
13480
- tm_metadata_mgr.update_entry_count(target_tm_id)
13481
- self.log(f"Imported {count} entries into TM '{target_tm_id}'")
13482
-
13483
- 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:,}")
13484
13915
  refresh_callback()
13485
13916
  else:
13486
13917
  QMessageBox.critical(self, "Error", "TM database not available")
@@ -14260,7 +14691,7 @@ class SupervertalerQt(QMainWindow):
14260
14691
  auto_markdown_cb.setChecked(general_settings.get('auto_generate_markdown', False))
14261
14692
  auto_markdown_cb.setToolTip(
14262
14693
  "Automatically convert imported documents to markdown format\n"
14263
- "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/"
14264
14695
  )
14265
14696
  behavior_layout.addWidget(auto_markdown_cb)
14266
14697
 
@@ -14472,7 +14903,7 @@ class SupervertalerQt(QMainWindow):
14472
14903
 
14473
14904
  def _create_general_settings_tab(self):
14474
14905
  """Create General Settings tab content"""
14475
- from PyQt6.QtWidgets import QCheckBox, QGroupBox, QPushButton
14906
+ from PyQt6.QtWidgets import QCheckBox, QGroupBox, QPushButton, QLineEdit, QFileDialog
14476
14907
 
14477
14908
  tab = QWidget()
14478
14909
  layout = QVBoxLayout(tab)
@@ -14502,6 +14933,119 @@ class SupervertalerQt(QMainWindow):
14502
14933
  startup_group.setLayout(startup_layout)
14503
14934
  layout.addWidget(startup_group)
14504
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
+
14505
15049
  # Sound Effects group
14506
15050
  sound_group = QGroupBox("🔊 Sound Effects")
14507
15051
  sound_layout = QVBoxLayout()
@@ -16537,7 +17081,7 @@ class SupervertalerQt(QMainWindow):
16537
17081
  prompt_text = self.prompt_manager_qt.get_system_template(mode_key)
16538
17082
  else:
16539
17083
  # Fallback: load from JSON file
16540
- 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"
16541
17085
  if system_prompts_file.exists():
16542
17086
  import json
16543
17087
  with open(system_prompts_file, 'r', encoding='utf-8') as f:
@@ -16566,7 +17110,7 @@ class SupervertalerQt(QMainWindow):
16566
17110
 
16567
17111
  # Always save to JSON file
16568
17112
  import json
16569
- 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"
16570
17114
  system_prompts_file.parent.mkdir(parents=True, exist_ok=True)
16571
17115
 
16572
17116
  # Load existing prompts
@@ -29627,7 +30171,7 @@ class SupervertalerQt(QMainWindow):
29627
30171
  layout = QVBoxLayout(dialog)
29628
30172
 
29629
30173
  # Header
29630
- header = QLabel("<h3>✅ AI-Powered Translation Proofreading</h3>")
30174
+ header = QLabel("<h3>✅ AI-Enhanced Translation Proofreading</h3>")
29631
30175
  layout.addWidget(header)
29632
30176
 
29633
30177
  info_label = QLabel(
@@ -36345,7 +36889,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36345
36889
  subtab_name: Name of the sub-tab to navigate to (e.g., "AI Settings")
36346
36890
  """
36347
36891
  if hasattr(self, 'main_tabs'):
36348
- # 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
36349
36893
  self.main_tabs.setCurrentIndex(4)
36350
36894
 
36351
36895
  # Navigate to specific sub-tab if requested
@@ -36366,7 +36910,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36366
36910
  def _go_to_superlookup(self):
36367
36911
  """Navigate to Superlookup in Tools tab"""
36368
36912
  if hasattr(self, 'main_tabs'):
36369
- # 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
36370
36914
  self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
36371
36915
  # Then switch to Superlookup sub-tab
36372
36916
  if hasattr(self, 'modules_tabs'):
@@ -36379,7 +36923,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36379
36923
  def _navigate_to_tool(self, tool_name: str):
36380
36924
  """Navigate to a specific tool in the Tools tab"""
36381
36925
  if hasattr(self, 'main_tabs'):
36382
- # 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
36383
36927
  self.main_tabs.setCurrentIndex(3) # Switch to Tools tab
36384
36928
  # Then switch to the specific tool sub-tab
36385
36929
  if hasattr(self, 'modules_tabs'):
@@ -36612,7 +37156,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
36612
37156
  file_path, _ = QFileDialog.getOpenFileName(
36613
37157
  dialog,
36614
37158
  "Select TMX File",
36615
- str(self.user_data_path / "Translation_Resources"),
37159
+ str(self.user_data_path / "resources"),
36616
37160
  "TMX Files (*.tmx);;All Files (*.*)"
36617
37161
  )
36618
37162
  if file_path:
@@ -37398,7 +37942,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37398
37942
  layout.addWidget(title)
37399
37943
 
37400
37944
  # Description
37401
- 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>")
37402
37946
  desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
37403
37947
  layout.addWidget(desc)
37404
37948
 
@@ -41185,9 +41729,10 @@ class SuperlookupTab(QWidget):
41185
41729
  Works anywhere on your computer: in CAT tools, browsers, Word, any text box
41186
41730
  """
41187
41731
 
41188
- def __init__(self, parent=None):
41732
+ def __init__(self, parent=None, user_data_path=None):
41189
41733
  super().__init__(parent)
41190
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
41191
41736
 
41192
41737
  print("[Superlookup] SuperlookupTab.__init__ called")
41193
41738
 
@@ -41359,31 +41904,10 @@ class SuperlookupTab(QWidget):
41359
41904
 
41360
41905
  layout.addLayout(search_row)
41361
41906
 
41362
- # Options row (direction + language filters) - compact single line
41907
+ # Options row (language filters only) - compact single line
41363
41908
  options_layout = QHBoxLayout()
41364
41909
  options_layout.setSpacing(8)
41365
41910
 
41366
- # Search direction selector
41367
- direction_label = QLabel("Direction:")
41368
- direction_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
41369
- options_layout.addWidget(direction_label)
41370
-
41371
- self.direction_both = CheckmarkRadioButton("Both")
41372
- self.direction_both.setChecked(True) # Default
41373
- self.direction_both.setToolTip("Search in both source and target text (bidirectional)")
41374
- options_layout.addWidget(self.direction_both)
41375
-
41376
- self.direction_source = CheckmarkRadioButton("Source")
41377
- self.direction_source.setToolTip("Search only in source language text")
41378
- options_layout.addWidget(self.direction_source)
41379
-
41380
- self.direction_target = CheckmarkRadioButton("Target")
41381
- self.direction_target.setToolTip("Search only in target language text")
41382
- options_layout.addWidget(self.direction_target)
41383
-
41384
- # Spacer
41385
- options_layout.addSpacing(15)
41386
-
41387
41911
  # Language filter dropdowns
41388
41912
  from_label = QLabel("From:")
41389
41913
  from_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
@@ -41895,7 +42419,11 @@ class SuperlookupTab(QWidget):
41895
42419
  self.web_engine_available = True
41896
42420
 
41897
42421
  # Create persistent profile for login/cookie storage
41898
- 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')
41899
42427
  os.makedirs(storage_path, exist_ok=True)
41900
42428
  self.web_profile = QWebEngineProfile("SuperlookupProfile", self)
41901
42429
  self.web_profile.setPersistentStoragePath(storage_path)
@@ -43159,17 +43687,10 @@ class SuperlookupTab(QWidget):
43159
43687
  def get_search_direction(self):
43160
43688
  """Get the current search direction setting.
43161
43689
 
43162
- Returns:
43163
- 'both' - bidirectional search (source and target)
43164
- 'source' - search source text only
43165
- '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.
43166
43692
  """
43167
- if hasattr(self, 'direction_source') and self.direction_source.isChecked():
43168
- return 'source'
43169
- elif hasattr(self, 'direction_target') and self.direction_target.isChecked():
43170
- return 'target'
43171
- else:
43172
- return 'both'
43693
+ return 'both'
43173
43694
 
43174
43695
  def _on_language_changed(self):
43175
43696
  """Handle language dropdown change - update Web Resources info"""
@@ -44926,7 +45447,7 @@ class SuperlookupTab(QWidget):
44926
45447
  print(f"[Superlookup] Has main_tabs: {hasattr(main_window, 'main_tabs')}")
44927
45448
 
44928
45449
  # Switch to Tools tab (main_tabs index 3)
44929
- # 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
44930
45451
  if hasattr(main_window, 'main_tabs'):
44931
45452
  print(f"[Superlookup] Current main_tab index: {main_window.main_tabs.currentIndex()}")
44932
45453
  main_window.main_tabs.setCurrentIndex(3) # Tools tab