• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

anthonypdawson / vector-inspector / 22584832749

02 Mar 2026 04:16PM UTC coverage: 79.791% (-0.2%) from 79.983%
22584832749

push

github

anthonypdawson
feat: update release notes for version 0.5.4 and improve WebEngine object disposal to prevent shutdown warnings

40 of 74 new or added lines in 3 files covered. (54.05%)

26 existing lines in 1 file now uncovered.

11474 of 14380 relevant lines covered (79.79%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.64
/src/vector_inspector/ui/main_window.py
1
"""Updated main window with multi-database support."""
2

3
from typing import Optional
1✔
4

5
from PySide6.QtCore import QByteArray, Qt, QTimer
1✔
6
from PySide6.QtGui import QAction
1✔
7
from PySide6.QtWidgets import (
1✔
8
    QDialog,
9
    QLabel,
10
    QMessageBox,
11
    QStatusBar,
12
    QToolBar,
13
)
14

15
from vector_inspector.core.connection_manager import ConnectionInstance, ConnectionManager
1✔
16
from vector_inspector.core.logging import log_error
1✔
17
from vector_inspector.services.profile_service import ProfileService
1✔
18
from vector_inspector.services.settings_service import SettingsService
1✔
19
from vector_inspector.services.task_runner import ThreadedTaskRunner
1✔
20
from vector_inspector.state import AppState
1✔
21
from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
1✔
22
from vector_inspector.ui.components.profile_manager_panel import ProfileManagerPanel
1✔
23
from vector_inspector.ui.controllers.connection_controller import ConnectionController
1✔
24
from vector_inspector.ui.main_window_shell import InspectorShell
1✔
25
from vector_inspector.ui.services.dialog_service import DialogService
1✔
26
from vector_inspector.ui.tabs import InspectorTabs
1✔
27

28

29
class MainWindow(InspectorShell):
1✔
30
    """Main application window with multi-database support."""
31

32
    connection_manager: ConnectionManager
1✔
33
    profile_service: ProfileService
1✔
34
    settings_service: SettingsService
1✔
35
    connection_controller: ConnectionController
1✔
36
    visualization_view: object
1✔
37
    info_panel: object
1✔
38
    metadata_view: object
1✔
39
    search_view: object
1✔
40
    connection_panel: ConnectionManagerPanel
1✔
41
    profile_panel: ProfileManagerPanel
1✔
42

43
    def __init__(self):
1✔
44
        super().__init__()
1✔
45

46
        # Shared application state and task runner
47
        self.app_state = AppState()
1✔
48
        self.task_runner = ThreadedTaskRunner()
1✔
49

50
        # Core services
51
        self.connection_manager = ConnectionManager()
1✔
52
        self.profile_service = ProfileService()
1✔
53
        self.settings_service = SettingsService()
1✔
54

55
        # Controller for connection operations
56
        self.connection_controller = ConnectionController(self.connection_manager, self.profile_service, self)
1✔
57

58
        # State
59
        self.visualization_view = None
1✔
60

61
        # View references (will be set in _setup_ui)
62
        self.info_panel = None
1✔
63
        self.metadata_view = None
1✔
64
        self.search_view = None
1✔
65
        self.connection_panel = None
1✔
66
        self.profile_panel = None
1✔
67

68
        self.setWindowTitle("Vector Inspector")
1✔
69
        self.setGeometry(100, 100, 1600, 900)
1✔
70

71
        self._setup_ui()
1✔
72
        self._setup_menu_bar()
1✔
73
        self._setup_toolbar()
1✔
74
        self._setup_statusbar()
1✔
75
        self._connect_signals()
1✔
76
        self._restore_session()
1✔
77
        # Listen for settings changes so updates apply immediately
78
        try:
1✔
79
            self.settings_service.signals.setting_changed.connect(self._on_setting_changed)
1✔
80
        except Exception:
×
81
            pass
×
82
        # Restore window geometry if present
83
        try:
1✔
84
            geom = self.settings_service.get_window_geometry()
1✔
85
            if geom and self.settings_service.get_window_restore_geometry():
1✔
86
                try:
1✔
87
                    # restoreGeometry accepts QByteArray; wrap bytes accordingly
88
                    if isinstance(geom, (bytes, bytearray)):
1✔
89
                        self.restoreGeometry(QByteArray(geom))
1✔
90
                    else:
91
                        self.restoreGeometry(geom)
×
92
                except Exception:
×
93
                    # fallback: try passing raw bytes
94
                    try:
×
95
                        self.restoreGeometry(geom)
×
96
                    except Exception:
×
97
                        pass
×
98
        except Exception:
×
99
            pass
×
100
        # Show splash after main window is visible
101
        QTimer.singleShot(0, self._maybe_show_splash)
1✔
102

103
    def _maybe_show_splash(self):
1✔
104
        # Only show splash if not hidden in settings
105
        if not self.settings_service.get("hide_splash_window", False):
1✔
106
            try:
×
107
                from vector_inspector.ui.components.splash_window import SplashWindow
×
108

109
                splash = SplashWindow(self)
×
110
                splash.setWindowModality(Qt.ApplicationModal)
×
111
                splash.raise_()
×
112
                splash.activateWindow()
×
113
                if splash.exec() == QDialog.DialogCode.Accepted and splash.should_hide():
×
114
                    self.settings_service.set("hide_splash_window", True)
×
115
            except Exception as e:
×
116
                print(f"[SplashWindow] Failed to show splash: {e}")
×
117

118
    def _setup_ui(self):
1✔
119
        """Setup the main UI layout using InspectorShell."""
120
        # Left panels - Connections and Profiles
121
        self.connection_panel = ConnectionManagerPanel(self.connection_manager)
1✔
122
        self.add_left_panel(self.connection_panel, "Active")
1✔
123

124
        self.profile_panel = ProfileManagerPanel(self.profile_service)
1✔
125
        self.add_left_panel(self.profile_panel, "Profiles")
1✔
126

127
        # Refresh info panel when switching left tabs (e.g., back to Active)
128
        try:
1✔
129
            self.left_tabs.currentChanged.connect(self._on_left_panel_changed)
1✔
130
        except Exception:
×
131
            pass
×
132

133
        # Default to Profiles tab on launch (index 1) so saved profiles are
134
        # shown to the user first instead of the Active panel.
135
        try:
1✔
136
            # Only switch if there are at least two tabs
137
            if self.left_tabs.count() > 1:
1✔
138
                self.set_left_panel_active(1)
1✔
139
        except Exception:
×
140
            pass
×
141

142
        # Main content tabs using TabRegistry
143
        tab_defs = InspectorTabs.get_standard_tabs()
1✔
144

145
        for i, tab_def in enumerate(tab_defs):
1✔
146
            widget = InspectorTabs.create_tab_widget(tab_def, app_state=self.app_state, task_runner=self.task_runner)
1✔
147
            self.add_main_tab(widget, tab_def.title)
1✔
148

149
            # Store references to views (except placeholder)
150
            if i == InspectorTabs.INFO_TAB:
1✔
151
                self.info_panel = widget
1✔
152
            elif i == InspectorTabs.DATA_TAB:
1✔
153
                self.metadata_view = widget
1✔
154
            elif i == InspectorTabs.SEARCH_TAB:
1✔
155
                self.search_view = widget
1✔
156
            # Visualization is lazy-loaded, so it's a placeholder for now
157

158
        # Set Info tab as default
159
        self.set_main_tab_active(InspectorTabs.INFO_TAB)
1✔
160

161
        # Connect to tab change to lazy load visualization
162
        self.tab_widget.currentChanged.connect(self._on_tab_changed)
1✔
163

164
    def _setup_menu_bar(self):
1✔
165
        """Setup application menu bar."""
166
        menubar = self.menuBar()
1✔
167

168
        # File menu
169
        file_menu = menubar.addMenu("&File")
1✔
170

171
        new_connection_action = QAction("&New Connection...", self)
1✔
172
        new_connection_action.setShortcut("Ctrl+N")
1✔
173
        new_connection_action.triggered.connect(self._new_connection_from_profile)
1✔
174
        file_menu.addAction(new_connection_action)
1✔
175

176
        file_menu.addSeparator()
1✔
177

178
        prefs_action = QAction("Preferences...", self)
1✔
179
        prefs_action.setShortcut("Ctrl+,")
1✔
180
        prefs_action.triggered.connect(self._show_preferences_dialog)
1✔
181
        file_menu.addAction(prefs_action)
1✔
182

183
        file_menu.addSeparator()
1✔
184

185
        exit_action = QAction("E&xit", self)
1✔
186
        exit_action.setShortcut("Ctrl+Q")
1✔
187
        exit_action.triggered.connect(self.close)
1✔
188
        file_menu.addAction(exit_action)
1✔
189

190
        # Connection menu
191
        connection_menu = menubar.addMenu("&Connection")
1✔
192

193
        new_profile_action = QAction("New &Profile...", self)
1✔
194
        new_profile_action.triggered.connect(self._show_profile_editor)
1✔
195
        connection_menu.addAction(new_profile_action)
1✔
196

197
        connection_menu.addSeparator()
1✔
198

199
        refresh_action = QAction("&Refresh Collections", self)
1✔
200
        refresh_action.setShortcut("F5")
1✔
201
        refresh_action.triggered.connect(self._refresh_active_connection)
1✔
202
        connection_menu.addAction(refresh_action)
1✔
203

204
        connection_menu.addSeparator()
1✔
205

206
        backup_action = QAction("&Backup/Restore...", self)
1✔
207
        backup_action.triggered.connect(self._show_backup_restore_dialog)
1✔
208
        connection_menu.addAction(backup_action)
1✔
209

210
        migrate_action = QAction("&Migrate Data...", self)
1✔
211
        migrate_action.triggered.connect(self._show_migration_dialog)
1✔
212
        connection_menu.addAction(migrate_action)
1✔
213

214
        # Tools menu
215
        tools_menu = menubar.addMenu("&Tools")
1✔
216

217
        create_collection_action = QAction("Create &Test Collection...", self)
1✔
218
        create_collection_action.setShortcut("Ctrl+T")
1✔
219
        create_collection_action.triggered.connect(self._create_test_collection)
1✔
220
        tools_menu.addAction(create_collection_action)
1✔
221

222
        # View menu
223
        view_menu = menubar.addMenu("&View")
1✔
224

225
        self.cache_action = QAction("Enable &Caching", self)
1✔
226
        self.cache_action.setCheckable(True)
1✔
227
        self.cache_action.setChecked(self.settings_service.get_cache_enabled())
1✔
228
        self.cache_action.triggered.connect(self._toggle_cache)
1✔
229
        view_menu.addAction(self.cache_action)
1✔
230

231
        # Help menu
232
        help_menu = menubar.addMenu("&Help")
1✔
233
        about_action = QAction("&About", self)
1✔
234
        about_action.triggered.connect(self._show_about)
1✔
235
        help_menu.addAction(about_action)
1✔
236
        check_update_action = QAction("Check for Update", self)
1✔
237
        check_update_action.triggered.connect(self._check_for_update_from_menu)
1✔
238
        help_menu.addAction(check_update_action)
1✔
239

240
    def _check_for_update_from_menu(self):
1✔
241
        from PySide6.QtWidgets import QMessageBox
×
242

243
        from vector_inspector.services.update_service import UpdateService
×
244
        from vector_inspector.utils.version import get_app_version
×
245

246
        latest = UpdateService.get_latest_release(force_refresh=True)
×
247
        if latest:
×
248
            current_version = get_app_version()
×
249
            latest_version = latest.get("tag_name")
×
250
            if latest_version and UpdateService.compare_versions(current_version, latest_version):
×
251
                # Show update modal
252
                self._latest_release = latest
×
253
                self._on_update_indicator_clicked(None)
×
254
                return
×
255
        QMessageBox.information(self, "Check for Update", "No update available.")
×
256

257
    def _setup_toolbar(self):
1✔
258
        """Setup application toolbar."""
259
        toolbar = QToolBar("Main Toolbar")
1✔
260
        toolbar.setMovable(False)
1✔
261
        self.addToolBar(toolbar)
1✔
262

263
        new_connection_action = QAction("New Connection", self)
1✔
264
        new_connection_action.triggered.connect(self._new_connection_from_profile)
1✔
265
        toolbar.addAction(new_connection_action)
1✔
266

267
        toolbar.addSeparator()
1✔
268

269
        refresh_action = QAction("Refresh", self)
1✔
270
        refresh_action.triggered.connect(self._refresh_active_connection)
1✔
271
        toolbar.addAction(refresh_action)
1✔
272

273
    def _setup_statusbar(self):
1✔
274
        """Setup status bar with connection breadcrumb and update indicator."""
275
        status_bar = QStatusBar()
1✔
276
        self.setStatusBar(status_bar)
1✔
277

278
        # Breadcrumb label
279
        self.breadcrumb_label = QLabel("No active connection")
1✔
280
        self.statusBar().addPermanentWidget(self.breadcrumb_label)
1✔
281

282
        # Update indicator label (hidden by default)
283
        self.update_indicator = QLabel()
1✔
284
        self.update_indicator.setText("")
1✔
285
        self.update_indicator.setStyleSheet("color: #2980b9; font-weight: bold; text-decoration: underline;")
1✔
286
        self.update_indicator.setVisible(False)
1✔
287
        self.update_indicator.setCursor(Qt.PointingHandCursor)
1✔
288
        self.statusBar().addPermanentWidget(self.update_indicator)
1✔
289

290
        self.statusBar().showMessage("Ready")
1✔
291

292
        # Connect click event
293
        self.update_indicator.mousePressEvent = self._on_update_indicator_clicked
1✔
294

295
        # Check for updates on launch
296
        import threading
1✔
297

298
        from PySide6.QtCore import QTimer
1✔
299

300
        from vector_inspector.services.update_service import UpdateService
1✔
301
        from vector_inspector.utils.version import get_app_version
1✔
302

303
        def check_updates():
1✔
304
            latest = UpdateService.get_latest_release()
1✔
305
            if latest:
1✔
306
                current_version = get_app_version()
1✔
307
                latest_version = latest.get("tag_name")
1✔
308
                if latest_version and UpdateService.compare_versions(current_version, latest_version):
1✔
309

310
                    def show_update():
×
311
                        self._latest_release = latest
×
312
                        self.update_indicator.setText(f"Update available: v{latest_version}")
×
313
                        self.update_indicator.setVisible(True)
×
314

315
                    QTimer.singleShot(0, show_update)
×
316

317
        threading.Thread(target=check_updates, daemon=True).start()
1✔
318

319
    def _show_preferences_dialog(self):
1✔
320
        try:
×
321
            from vector_inspector.ui.dialogs.settings_dialog import SettingsDialog
×
322

323
            dlg = SettingsDialog(self.settings_service, self)
×
324
            if dlg.exec() == QDialog.DialogCode.Accepted:
×
325
                self._apply_settings_to_views()
×
326
        except Exception as e:
×
327
            print(f"Failed to open preferences: {e}")
×
328

329
    def _apply_settings_to_views(self):
1✔
330
        """Apply relevant settings to existing views."""
331
        try:
1✔
332
            # Breadcrumb visibility
333
            enabled = self.settings_service.get_breadcrumb_enabled()
1✔
334
            if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
1✔
335
                self.search_view.breadcrumb_label.setVisible(enabled)
1✔
336
                # also set elide mode
337
                mode = self.settings_service.get_breadcrumb_elide_mode()
1✔
338
                try:
1✔
339
                    self.search_view.set_elide_mode(mode)
1✔
340
                except Exception:
×
341
                    pass
×
342

343
            # Default results
344
            default_n = self.settings_service.get_default_n_results()
1✔
345
            if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
1✔
346
                try:
1✔
347
                    self.search_view.n_results_spin.setValue(int(default_n))
1✔
348
                except Exception:
×
349
                    pass
×
350

351
        except Exception:
×
352
            pass
×
353

354
    def _on_setting_changed(self, key: str, value: object):
1✔
355
        """Handle granular setting change events."""
356
        try:
1✔
357
            if key == "breadcrumb.enabled":
1✔
358
                enabled = bool(value)
×
359
                if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
×
360
                    self.search_view.breadcrumb_label.setVisible(enabled)
×
361
            elif key == "breadcrumb.elide_mode":
1✔
362
                mode = str(value)
×
363
                if self.search_view is not None and hasattr(self.search_view, "set_elide_mode"):
×
364
                    self.search_view.set_elide_mode(mode)
×
365
            elif key == "search.default_n_results":
1✔
366
                try:
×
367
                    n = int(value)
×
368
                    if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
×
369
                        self.search_view.n_results_spin.setValue(n)
×
370
                except Exception:
×
371
                    pass
×
372
        except Exception:
×
373
            pass
×
374

375
    def _on_update_indicator_clicked(self, event):
1✔
376
        # Show update details dialog
377
        if not hasattr(self, "_latest_release"):
×
378
            return
×
379

380
        # Track that user clicked on update available
381
        try:
×
382
            from vector_inspector.services.telemetry_service import TelemetryService
×
383
            from vector_inspector.utils.version import get_app_version
×
384

385
            telemetry = TelemetryService(self.settings_service)
×
386
            if telemetry.is_enabled():
×
387
                latest_version = self._latest_release.get("tag_name", "unknown")
×
388
                event_data = {
×
389
                    "hwid": telemetry.get_hwid(),
390
                    "event_name": "update_clicked",
391
                    "app_version": get_app_version(),
392
                    "client_type": "vector-inspector",
393
                    "metadata": {"latest_version": latest_version},
394
                }
395
                telemetry.queue_event(event_data)
×
396
                telemetry.send_batch()
×
397
        except Exception as e:
×
398
            # Don't let telemetry errors break the update flow
399
            log_error(f"Telemetry error: {e}")
×
400

401
        DialogService.show_update_details(self._latest_release, self)
×
402

403
    def _connect_signals(self):
1✔
404
        """Connect signals between components."""
405
        # Connection manager signals
406
        self.connection_manager.active_connection_changed.connect(self._on_active_connection_changed)
1✔
407
        self.connection_manager.active_collection_changed.connect(self._on_active_collection_changed)
1✔
408
        self.connection_manager.collections_updated.connect(self._on_collections_updated)
1✔
409
        self.connection_manager.connection_opened.connect(self._on_connection_opened)
1✔
410

411
        # Connection controller signals
412
        self.connection_controller.connection_completed.connect(self._on_connection_completed)
1✔
413

414
        # Connection panel signals
415
        self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
1✔
416
        self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
1✔
417

418
        # Profile panel signals
419
        self.profile_panel.connect_profile.connect(self._connect_to_profile)
1✔
420
        # Emit profile selection so InfoPanel can preview profile details
421
        try:
1✔
422
            self.profile_panel.profile_selected.connect(self._on_profile_selected_from_profiles)
1✔
423
        except Exception:
×
424
            pass
×
425

426
    def _on_connection_completed(self, connection_id: str, success: bool, collections: list, error: str):
1✔
427
        """Handle connection completed event from controller."""
428
        if success:
×
429
            # Switch to Active connections tab
430
            self.set_left_panel_active(0)
×
431
            self.statusBar().showMessage(f"Connected successfully ({len(collections)} collections)", 5000)
×
432

433
    def _on_tab_changed(self, index: int):
1✔
434
        """Handle tab change - lazy load visualization tab."""
435
        if index == InspectorTabs.VISUALIZATION_TAB and self.visualization_view is None:
1✔
436
            # Lazy load visualization view
437
            from vector_inspector.ui.views.visualization_view import VisualizationView
×
438

439
            self.visualization_view = VisualizationView(
×
440
                self.app_state, self.task_runner, connection_manager=self.connection_manager
441
            )
442

443
            # Connect signal to view point in data browser
444
            self.visualization_view.view_in_data_browser_requested.connect(self._on_view_in_data_browser_requested)
×
445

446
            # Replace placeholder with actual view
447
            self.remove_main_tab(InspectorTabs.VISUALIZATION_TAB)
×
448
            self.add_main_tab(self.visualization_view, "Visualization", InspectorTabs.VISUALIZATION_TAB)
×
449
            self.set_main_tab_active(InspectorTabs.VISUALIZATION_TAB)
×
450

451
            # Set collection if one is already selected (for initial state)
452
            # Future collection changes will be handled by app_state.collection_changed signal
453
            if self.app_state.collection:
×
454
                self.visualization_view.set_collection(self.app_state.collection)
×
455

456
    def _on_active_connection_changed(self, connection_id):
1✔
457
        """Handle active connection change."""
458
        if connection_id:
×
459
            instance = self.connection_manager.get_connection(connection_id)
×
460
            if instance:
×
461
                # Update breadcrumb
462
                self.breadcrumb_label.setText(instance.get_breadcrumb())
×
463

464
                # Update all views with new connection
465
                self._update_views_with_connection(instance)
×
466

467
                # If there's an active collection, update views with it
468
                if instance.active_collection:
×
469
                    self._update_views_for_collection(instance.active_collection)
×
470
            else:
471
                self.breadcrumb_label.setText("No active connection")
×
472
                self._update_views_with_connection(None)
×
473
        else:
474
            self.breadcrumb_label.setText("No active connection")
×
475
            self._update_views_with_connection(None)
×
476

477
    def _on_active_collection_changed(self, connection_id: str, collection_name: str):
1✔
478
        """Handle active collection change."""
479
        instance = self.connection_manager.get_connection(connection_id)
×
480
        if instance:
×
481
            # Update breadcrumb
482
            self.breadcrumb_label.setText(instance.get_breadcrumb())
×
483

484
            # Update views if this is the active connection
485
            if connection_id == self.connection_manager.get_active_connection_id():
×
486
                # Update views for collection (operations are threaded internally)
487
                if collection_name:
×
488
                    self._update_views_for_collection(collection_name)
×
489
                else:
490
                    # Clear collection from views
491
                    self._update_views_for_collection(None)
×
492

493
    def _on_collections_updated(self, connection_id: str, collections: list):
1✔
494
        """Handle collections list updated."""
495
        # UI automatically updates via connection_manager_panel
496
        pass
×
497

498
    def _on_connection_opened(self, connection_id: str):
1✔
499
        """Handle connection successfully opened."""
500
        # If this is the active connection, refresh the info panel
501
        if connection_id == self.connection_manager.get_active_connection_id():
×
502
            instance = self.connection_manager.get_connection(connection_id)
×
503
            if instance and instance.is_connected:
×
504
                self.info_panel.refresh_database_info()
×
505

506
    def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
1✔
507
        """Handle collection selection from connection panel."""
508
        # The connection manager already handled setting active collection
509
        # Just update the views (operations are threaded internally)
510
        self._update_views_for_collection(collection_name)
×
511

512
    def _update_views_with_connection(self, connection: Optional[ConnectionInstance]):
1✔
513
        """Update all views with a new connection."""
514
        # Update AppState (new pattern - triggers reactive views)
515
        # AppState exposes properties rather than setter methods.
516
        self.app_state.provider = connection
×
517

518
        # Clear current collection when switching connections (legacy pattern)
519
        self.info_panel.current_collection = None
×
520
        if hasattr(self.metadata_view, "current_collection"):
×
521
            self.metadata_view.current_collection = None
×
522
        if hasattr(self.search_view, "current_collection"):
×
523
            self.search_view.current_collection = None
×
524
        if self.visualization_view is not None:
×
525
            self.visualization_view.current_collection = None
×
526

527
        # Update connection references (legacy pattern)
528
        self.info_panel.connection = connection
×
529
        if hasattr(self.metadata_view, "connection"):
×
530
            self.metadata_view.connection = connection
×
531
        if hasattr(self.search_view, "connection"):
×
532
            self.search_view.connection = connection
×
533

534
        if self.visualization_view is not None:
×
535
            self.visualization_view.connection = connection
×
536

537
        # Refresh info panel (will show no collection selected)
538
        if connection:
×
539
            self.info_panel.refresh_database_info()
×
540

541
    def _update_views_for_collection(self, collection_name: str):
1✔
542
        """Update all views with the selected collection."""
543
        if collection_name:
×
544
            # Get active connection ID to use as database identifier
545
            active = self.connection_manager.get_active_connection()
×
546
            database_name = active.id if active else ""
×
547

548
            # Update AppState (new pattern - triggers reactive views)
549
            # AppState exposes properties rather than setter methods.
550
            self.app_state.collection = collection_name
×
551
            self.app_state.database = database_name
×
552

553
            # Update views (legacy pattern - for views not yet refactored)
554
            self.info_panel.set_collection(collection_name, database_name)
×
555
            if hasattr(self.metadata_view, "set_collection"):
×
556
                self.metadata_view.set_collection(collection_name, database_name)
×
557
            if hasattr(self.search_view, "set_collection"):
×
558
                self.search_view.set_collection(collection_name, database_name)
×
559

560
            if self.visualization_view is not None:
×
561
                self.visualization_view.set_collection(collection_name)
×
562

563
    def _new_connection_from_profile(self):
1✔
564
        """Show dialog to create new connection (switches to Profiles tab)."""
565
        self.set_left_panel_active(1)  # Switch to Profiles tab
×
566
        DialogService.show_profile_editor_prompt(self)
×
567

568
    def _show_profile_editor(self):
1✔
569
        """Show profile editor to create new profile."""
570
        self.set_left_panel_active(1)  # Switch to Profiles tab
×
571
        self.profile_panel._create_profile()
×
572

573
    def _connect_to_profile(self, profile_id: str):
1✔
574
        """Connect to a profile using the connection controller."""
575
        success = self.connection_controller.connect_to_profile(profile_id)
×
576
        if success:
×
577
            # Switch to Active connections tab after initiating connection
578
            self.set_left_panel_active(0)
×
579

580
    def _refresh_active_connection(self):
1✔
581
        """Refresh collections for the active connection."""
582
        active = self.connection_manager.get_active_connection()
×
583
        if not active or not active.is_connected:
×
584
            QMessageBox.information(self, "No Connection", "No active connection to refresh.")
×
585
            return
×
586

587
        try:
×
588
            collections = active.list_collections()
×
589
            self.connection_manager.update_collections(active.id, collections)
×
590
            self.statusBar().showMessage(f"Refreshed collections ({len(collections)} found)", 3000)
×
591

592
            # Also refresh info panel
593
            self.info_panel.refresh_database_info()
×
594
        except Exception as e:
×
595
            QMessageBox.warning(self, "Refresh Failed", f"Failed to refresh collections: {e}")
×
596

597
    def _restore_session(self):
1✔
598
        """Restore previously active connections on startup."""
599
        # TODO: Implement session restore
600
        # For now, we'll just show a message if there are saved profiles
601
        profiles = self.profile_service.get_all_profiles()
1✔
602
        if profiles:
1✔
603
            self.statusBar().showMessage(
×
604
                f"{len(profiles)} saved profile(s) available. Switch to Profiles tab to connect.",
605
                10000,
606
            )
607

608
        # Apply settings to views after UI is built
609
        self._apply_settings_to_views()
1✔
610

611
    def _show_about(self):
1✔
612
        """Show about dialog."""
613
        DialogService.show_about(self)
×
614

615
    def _toggle_cache(self, checked: bool):
1✔
616
        """Toggle caching on/off."""
617
        self.settings_service.set_cache_enabled(checked)
1✔
618
        # Update cache manager state (AppState coordination)
619
        if checked:
1✔
620
            self.app_state.cache_manager.enable()
1✔
621
        else:
622
            self.app_state.cache_manager.disable()
1✔
623
        status = "enabled" if checked else "disabled"
1✔
624
        self.statusBar().showMessage(f"Caching {status}", 3000)
1✔
625

626
    def _show_migration_dialog(self):
1✔
627
        """Show cross-database migration dialog."""
628
        DialogService.show_migration_dialog(self.connection_manager, self)
×
629

630
    def _show_backup_restore_dialog(self):
1✔
631
        """Show backup/restore dialog for the active collection."""
632
        # Get active connection and collection
633
        connection = self.connection_manager.get_active_connection()
×
634
        collection_name = self.connection_manager.get_active_collection()
×
635

636
        # Show dialog
637
        result = DialogService.show_backup_restore_dialog(connection, collection_name or "", self)
×
638

639
        if result == QDialog.DialogCode.Accepted:
×
640
            # Refresh collections after restore
641
            self._refresh_active_connection()
×
642

643
    def _create_test_collection(self):
1✔
644
        """Create a new collection with optional sample data."""
645
        # Get active connection
646
        active = self.connection_manager.get_active_connection()
×
647
        if not active or not active.is_connected:
×
648
            QMessageBox.information(self, "No Connection", "Please connect to a database first to create a collection.")
×
649
            return
×
650

651
        # Show dialog and create collection
652
        if self.connection_controller.create_collection_with_dialog(active.id):
×
653
            self.statusBar().showMessage("Collection created successfully", 3000)
×
654
            # Refresh the active connection to show the new collection
655
            self._refresh_active_connection()
×
656

657
    def show_search_results(self, collection_name: str, results: dict, context_info: str = ""):
1✔
658
        """Display search results in the Search tab.
659

660
        This is an extension point that allows external code (e.g., pro features)
661
        to programmatically display search results.
662

663
        Args:
664
            collection_name: Name of the collection
665
            results: Search results dictionary
666
            context_info: Optional context string (e.g., "Similar to: item_123")
667
        """
668
        # Switch to search tab
669
        self.set_main_tab_active(InspectorTabs.SEARCH_TAB)
×
670

671
        # Set the collection if needed
672
        if self.search_view.current_collection != collection_name:
×
673
            active = self.connection_manager.get_active_connection()
×
674
            database_name = active.id if active else ""
×
675
            self.search_view.set_collection(collection_name, database_name)
×
676

677
        # Display the results
678
        self.search_view.search_results = results
×
679
        self.search_view._display_results(results)
×
680

681
        # Update status with context if provided
682
        if context_info:
×
683
            num_results = len(results.get("ids", [[]])[0])
×
684
            self.search_view.results_status.setText(f"{context_info} - Found {num_results} results")
×
685

686
    def _on_view_in_data_browser_requested(self, item_id: str):
1✔
687
        """Handle request to view a specific item in the data browser.
688

689
        Args:
690
            item_id: ID of the item to view
691
        """
692
        # Switch to data browser tab
693
        self.set_main_tab_active(InspectorTabs.DATA_TAB)
1✔
694

695
        # Select the item in the metadata view
696
        if self.metadata_view:
1✔
697
            self.metadata_view.select_item_by_id(item_id)
1✔
698

699
    def _on_profile_selected_from_profiles(self, profile_id: str):
1✔
700
        """Handle single-click selection of a saved profile to preview its info."""
701
        try:
×
702
            profile_data = self.profile_service.get_profile_with_credentials(profile_id)
×
703
            # If profile_data is present, show preview in InfoPanel without connecting
704
            if profile_data and self.info_panel:
×
705
                self.info_panel.display_profile_info(profile_data)
×
706
            else:
707
                if self.info_panel:
×
708
                    self.info_panel.clear_profile_display()
×
709
        except Exception:
×
710
            # On error, clear preview to avoid stale info
711
            if self.info_panel:
×
712
                self.info_panel.clear_profile_display()
×
713

714
    def _on_left_panel_changed(self, index: int):
1✔
715
        """Handle when the left tab (Active/Profiles) changes.
716

717
        When switching to the Active tab, refresh the InfoPanel to show the
718
        active connection rather than any saved-profile preview.
719
        """
720
        # Index 0 is the Active connections tab by convention
721
        try:
1✔
722
            if index == 0 and self.info_panel:
1✔
723
                # Force refresh which will show active connection info
724
                self.info_panel.refresh_database_info()
×
725
        except Exception:
×
726
            pass
×
727

728
    def closeEvent(self, event):
1✔
729
        """Handle application close."""
730
        # Dispose visualization WebEngine objects first so pages/views are
731
        # deleted before any lower-level shutdown (profiles/connections).
732
        try:
1✔
733
            if self.visualization_view is not None:
1✔
NEW
734
                try:
×
NEW
735
                    self.visualization_view.cleanup_temp_html()
×
NEW
736
                except Exception:
×
NEW
737
                    pass
×
NEW
738
        except Exception:
×
NEW
739
            pass
×
740

741
        # Let Qt process pending deletion events to avoid profile-release races
742
        try:
1✔
743
            from PySide6.QtWidgets import QApplication
1✔
744

745
            try:
1✔
746
                QApplication.processEvents()
1✔
747
            except Exception:
×
748
                pass
×
NEW
749
        except Exception:
×
NEW
750
            pass
×
751

752
        # Clean up connection controller
753
        try:
1✔
754
            self.connection_controller.cleanup()
1✔
NEW
755
        except Exception:
×
NEW
756
            pass
×
757

758
        # Close all connections
759
        try:
1✔
760
            self.connection_manager.close_all_connections()
1✔
NEW
761
        except Exception:
×
NEW
762
            pass
×
763

764
        # Save window geometry if enabled
765
        try:
1✔
766
            if self.settings_service.get_window_restore_geometry():
1✔
767
                geom = self.saveGeometry()
1✔
768
                # geom may be a QByteArray; convert to raw bytes
769
                try:
1✔
770
                    if isinstance(geom, QByteArray):
1✔
771
                        b = bytes(geom)
1✔
772
                    else:
773
                        b = bytes(geom)
×
774
                    self.settings_service.set_window_geometry(b)
1✔
775
                except Exception:
×
776
                    try:
×
777
                        self.settings_service.set_window_geometry(bytes(geom))
×
778
                    except Exception:
×
779
                        pass
×
780
        except Exception:
1✔
781
            pass
1✔
782

783
        event.accept()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc