""" Main application window. On startup: • All saved connection profiles are loaded from ~/.dbclient/connections.json and shown in the sidebar in "disconnected" state. • Double-clicking or right-click → Connect instantly re-connects. • New Connection dialog saves the profile AND connects immediately. • Edit / Delete work on both connected and saved-only profiles. """ from PyQt6.QtWidgets import ( QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QSplitter, QTabWidget, QStatusBar, QLabel, QMessageBox, QDockWidget, QPushButton, QApplication, ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QAction, QKeySequence from app.ui.schema_browser import SchemaBrowser from app.ui.sql_editor import SQLEditorWidget from app.ui.table_viewer import TableViewer from app.ui.table_structure import TableStructureView from app.ui.query_history import QueryHistoryPanel from app.ui.process_list import ProcessListPanel from app.ui.import_dialog import ImportDialog from app.ui.dump_dialog import DumpDialog from app.ui.explain_view import ExplainPanel from app.ui.user_manager import UserManagerPanel from app.ui.log_viewer import LogViewer from app.ui.connection_dialog import ConnectionDialog from app.config.connections import load_profiles, delete_profile from app.models.connection_model import ConnectionProfile from app.drivers import get_driver from app.utils.logger import get_logger _log = get_logger(__name__) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("DBClient") self.resize(1400, 860) self.setMinimumSize(1024, 640) # profile_id → driver (only currently connected ones) self._active_drivers: dict = {} # profile_id → ConnectionProfile (all loaded profiles, connected or not) self._all_profiles: dict = {} self._build_ui() self._build_menus() self._build_status_bar() # Load saved profiles after the window is shown QTimer.singleShot(0, self._load_saved_profiles) # ── Layout ──────────────────────────────────────────────────────────────── def _build_ui(self): central = QWidget() self.setCentralWidget(central) h = QHBoxLayout(central) h.setContentsMargins(0, 0, 0, 0) h.setSpacing(0) self._h_splitter = QSplitter(Qt.Orientation.Horizontal) self._h_splitter.setHandleWidth(2) # ── Left sidebar ────────────────────────────────────────────────────── left = QWidget() left.setMinimumWidth(220) left.setMaximumWidth(420) ll = QVBoxLayout(left) ll.setContentsMargins(0, 0, 0, 0) ll.setSpacing(0) hdr = QWidget() hdr.setObjectName("sidebarHeader") hdr_lay = QHBoxLayout(hdr) hdr_lay.setContentsMargins(8, 6, 8, 6) hdr_lay.setSpacing(4) title = QLabel(" Connections") title.setObjectName("sidebarTitle") f = title.font() f.setBold(True) title.setFont(f) self._new_conn_btn = QPushButton("+") self._new_conn_btn.setObjectName("newConnBtn") self._new_conn_btn.setFixedSize(28, 28) self._new_conn_btn.setToolTip("New Connection (Ctrl+N)") self._new_conn_btn.clicked.connect(self._new_connection) hdr_lay.addWidget(title, 1) hdr_lay.addWidget(self._new_conn_btn) ll.addWidget(hdr) self._schema_browser = SchemaBrowser() # Tree → workspace wiring self._schema_browser.open_table_viewer.connect(self._open_table_viewer) self._schema_browser.open_table_structure.connect(self._open_table_structure) self._schema_browser.open_sql_editor.connect(self._open_sql_editor) self._schema_browser.run_query_requested.connect(self._paste_query) # Saved-profile management signals self._schema_browser.connect_requested.connect(self._connect_by_id) self._schema_browser.edit_requested.connect(self._edit_connection) self._schema_browser.delete_requested.connect(self._delete_connection) ll.addWidget(self._schema_browser, 1) # ── Right workspace ─────────────────────────────────────────────────── right = QWidget() rl = QVBoxLayout(right) rl.setContentsMargins(0, 0, 0, 0) rl.setSpacing(0) self._workspace = QTabWidget() self._workspace.setTabsClosable(True) self._workspace.setMovable(True) self._workspace.tabCloseRequested.connect(self._close_tab) self._workspace.setObjectName("workspace") self._empty_label = QLabel( "🔌 Double-click a saved connection to connect\n\n" "Use + to add a new connection profile." ) self._empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._empty_label.setObjectName("emptyLabel") rl.addWidget(self._empty_label) rl.addWidget(self._workspace) self._workspace.setVisible(False) # ── History dock ────────────────────────────────────────────────────── self._history_panel = QueryHistoryPanel() self._history_panel.run_query.connect(self._paste_query) history_dock = QDockWidget("Query History", self) history_dock.setWidget(self._history_panel) history_dock.setMinimumHeight(120) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, history_dock) history_dock.setVisible(False) self._history_dock = history_dock self._h_splitter.addWidget(left) self._h_splitter.addWidget(right) self._h_splitter.setSizes([260, 1100]) h.addWidget(self._h_splitter) # ── Menus ───────────────────────────────────────────────────────────────── def _build_menus(self): mb = self.menuBar() file_menu = mb.addMenu("&File") file_menu.addAction(self._act("+ New Connection…", self._new_connection, "Ctrl+N")) file_menu.addSeparator() file_menu.addAction(self._act("Exit", QApplication.quit, "Ctrl+Q")) view_menu = mb.addMenu("&View") view_menu.addAction(self._act("Toggle Query History", self._toggle_history, "Ctrl+H")) view_menu.addAction(self._act("New SQL Tab", self._new_sql_tab, "Ctrl+T")) tools_menu = mb.addMenu("&Tools") tools_menu.addAction(self._act("Process List…", self._open_process_list, "Ctrl+P")) tools_menu.addSeparator() tools_menu.addAction(self._act("Import CSV / JSON…", self._open_import_dialog)) tools_menu.addAction(self._act("Export Database Dump…", self._open_dump_dialog)) tools_menu.addSeparator() tools_menu.addAction(self._act("User & Privilege Management…", self._open_user_manager, "Ctrl+U")) help_menu = mb.addMenu("&Help") help_menu.addAction(self._act("Keyboard Shortcuts", self._show_shortcuts)) help_menu.addAction(self._act("View App Logs", self._open_log_viewer, "Ctrl+L")) help_menu.addSeparator() help_menu.addAction(self._act("About DBClient", self._show_about)) def _act(self, text: str, slot, shortcut: str = None) -> QAction: """Create a QAction parented to this window (prevents GC from killing it).""" a = QAction(text, self) a.triggered.connect(slot) if shortcut: a.setShortcut(QKeySequence(shortcut)) return a # ── Status bar ──────────────────────────────────────────────────────────── def _build_status_bar(self): sb = QStatusBar() self.setStatusBar(sb) self._status_lbl = QLabel("Ready") sb.addWidget(self._status_lbl, 1) def _set_status(self, msg: str): self._status_lbl.setText(msg) # ── Startup: load saved profiles ────────────────────────────────────────── def _load_saved_profiles(self): """ Load all saved connection profiles from disk and add them to the sidebar as disconnected nodes. Called once after the window shows. """ profiles = load_profiles() for profile in profiles: self._all_profiles[profile.id] = profile self._schema_browser.add_saved_profile(profile) if profiles: self._set_status( f"Loaded {len(profiles)} saved connection(s). " "Double-click to connect." ) # ── Connection management ───────────────────────────────────────────────── def _new_connection(self): """Open the New Connection dialog, save the profile, and connect.""" dlg = ConnectionDialog(parent=self) if dlg.exec(): profile = dlg.profile self._all_profiles[profile.id] = profile # Dialog already persisted it via save_profile() # Show it as saved first, then auto-connect self._schema_browser.add_saved_profile(profile) self._do_connect(profile) def _connect_by_id(self, profile_id: str): """Called when user double-clicks / right-clicks Connect on a saved node.""" if self._schema_browser.is_connected(profile_id): self._set_status("Already connected.") return profile = self._all_profiles.get(profile_id) if not profile: return self._do_connect(profile) def _do_connect(self, profile: ConnectionProfile): """Build the driver, connect, and upgrade the sidebar node.""" pid = profile.id config = { "host": profile.host, "port": profile.port, "database": profile.database, "user": profile.username, "password": profile.password, "connection_timeout": profile.connection_timeout, } _log.info("Connecting to '%s' type=%s host=%s", profile.name, profile.db_type, profile.host) try: driver = get_driver(profile.db_type, config) driver.connect() self._active_drivers[pid] = driver self._schema_browser.add_connection(profile, driver) self._show_workspace() self._set_status(f"✅ Connected: {profile.name}") _log.info("Connected to '%s' successfully", profile.name) except Exception as e: _log.error("Connection failed for '%s': %s", profile.name, e, exc_info=True) QMessageBox.critical(self, "Connection Error", f"Could not connect to '{profile.name}':\n\n{e}") def _edit_connection(self, profile_id: str): """Open edit dialog for a saved or connected profile.""" profile = self._all_profiles.get(profile_id) if not profile: return was_connected = self._schema_browser.is_connected(profile_id) dlg = ConnectionDialog(profile=profile, parent=self) if not dlg.exec(): return updated = dlg.profile self._all_profiles[profile_id] = updated # save_profile() was already called inside the dialog if was_connected: # Disconnect first, then reconnect with new credentials self._schema_browser.remove_connection(profile_id, keep_saved=True) self._active_drivers.pop(profile_id, None) self._do_connect(updated) else: self._schema_browser.update_saved_profile(updated) self._set_status(f"Connection '{updated.name}' updated.") def _delete_connection(self, profile_id: str): """Delete a profile entirely from memory and disk.""" profile = self._all_profiles.get(profile_id) if not profile: return btn = QMessageBox.warning( self, "Delete Connection", f"Delete the connection profile '{profile.name}'?\n\n" "This removes it from the saved list. " "The database itself will NOT be affected.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if btn != QMessageBox.StandardButton.Yes: return # Disconnect if active if self._schema_browser.is_connected(profile_id): self._schema_browser.remove_connection(profile_id, keep_saved=False) self._active_drivers.pop(profile_id, None) else: self._schema_browser.remove_connection(profile_id, keep_saved=False) self._all_profiles.pop(profile_id, None) delete_profile(profile_id) self._set_status(f"Connection '{profile.name}' deleted.") # ── Workspace helpers ───────────────────────────────────────────────────── def _show_workspace(self): self._empty_label.setVisible(False) self._workspace.setVisible(True) def _open_table_viewer(self, driver, database: str, table: str): tab = TableViewer(driver, database, table) tab.status_message.connect(self._set_status) idx = self._workspace.addTab(tab, f"📋 {table}") self._workspace.setCurrentIndex(idx) def _open_table_structure(self, driver, database: str, table: str): tab = TableStructureView(driver, database, table) tab.status_message.connect(self._set_status) idx = self._workspace.addTab(tab, f"🏗️ {table}") self._workspace.setCurrentIndex(idx) def _open_sql_editor(self, driver, database: str): sql_widget = SQLEditorWidget() sql_widget.status_message.connect(self._set_status) sql_widget.new_tab(driver, database) label = f"✏️ SQL — {database}" if database else "✏️ SQL" idx = self._workspace.addTab(sql_widget, label) self._workspace.setCurrentIndex(idx) def _new_sql_tab(self): sql_widget = SQLEditorWidget() sql_widget.status_message.connect(self._set_status) sql_widget.new_tab() idx = self._workspace.addTab(sql_widget, "✏️ SQL") self._workspace.setCurrentIndex(idx) def _paste_query(self, sql: str): current = self._workspace.currentWidget() if isinstance(current, SQLEditorWidget): tab = current.current_tab() if tab: tab.set_sql(sql) return self._new_sql_tab() self._paste_query(sql) def _close_tab(self, idx: int): self._workspace.removeTab(idx) if self._workspace.count() == 0: self._workspace.setVisible(False) self._empty_label.setVisible(True) # ── Misc ────────────────────────────────────────────────────────────────── def _toggle_history(self): self._history_dock.setVisible(not self._history_dock.isVisible()) def _show_about(self): QMessageBox.about(self, "About DBClient", "DBClient v1.0.0

" "Cross-platform desktop database client.
" "MySQL · PostgreSQL · SQLite · SQL Server

" "Built with Python + PyQt6.") def _show_shortcuts(self): QMessageBox.information(self, "Keyboard Shortcuts", "F5 / Ctrl+Enter — Run query\n" "Ctrl+/ — Toggle comment\n" "Ctrl+N — New connection\n" "Ctrl+T — New SQL tab\n" "Ctrl+H — Toggle query history\n" "Ctrl+U — User management\n" "Ctrl+L — View app logs\n" "Delete — Delete selected row (table viewer)\n" "Ins — Add new row (table viewer)\n" "Ctrl+Q — Quit") def _open_log_viewer(self): """Open the application log viewer as a workspace tab.""" viewer = LogViewer(parent=self) idx = self._workspace.addTab(viewer, "📋 App Logs") self._workspace.setCurrentIndex(idx) self._show_workspace() def _open_process_list(self): """Open a Process List tab for the currently active connection.""" driver, name = self._active_driver_for_tools() if driver is None: return panel = ProcessListPanel(driver, connection_name=name) panel.status_message.connect(self._set_status) idx = self._workspace.addTab(panel, f"⚙️ Processes — {name}") self._workspace.setCurrentIndex(idx) self._show_workspace() def _open_import_dialog(self): """Open the Import CSV/JSON dialog targeting the active connection.""" driver, name = self._active_driver_for_tools() if driver is None: return # Determine current database from the active workspace tab if possible database = "" current = self._workspace.currentWidget() if hasattr(current, "_database"): database = current._database or "" dlg = ImportDialog(driver, database=database, table="", parent=self) dlg.exec() def _open_dump_dialog(self): """Open the Export Database Dump dialog targeting the active connection.""" driver, name = self._active_driver_for_tools() if driver is None: return # Pre-select the database visible in the current workspace tab database = "" current = self._workspace.currentWidget() if hasattr(current, "_database"): database = current._database or "" dlg = DumpDialog(driver, database=database, parent=self) dlg.exec() def _open_user_manager(self): """Open the User & Privilege Management tab.""" driver, name = self._active_driver_for_tools() if driver is None: return panel = UserManagerPanel(driver, parent=self) panel.status_message.connect(self._set_status) idx = self._workspace.addTab(panel, f"👤 Users — {name}") self._workspace.setCurrentIndex(idx) self._show_workspace() def open_explain_tab(self, driver, database: str, sql: str): """Open an EXPLAIN plan tab (called from SQLEditorWidget).""" panel = ExplainPanel(driver, sql, parent=self) panel.status_message.connect(self._set_status) short_sql = sql[:40].replace("\n", " ") + ("…" if len(sql) > 40 else "") idx = self._workspace.addTab(panel, f"🔎 EXPLAIN") self._workspace.setTabToolTip(idx, short_sql) self._workspace.setCurrentIndex(idx) self._show_workspace() def _active_driver_for_tools(self): """Return (driver, connection_name) for the first active connection, or show a warning and return (None, '') if none are connected.""" if not self._active_drivers: QMessageBox.information( self, "No Active Connection", "Connect to a database first." ) return None, "" pid = next(iter(self._active_drivers)) driver = self._active_drivers[pid] profile = self._all_profiles.get(pid) name = profile.name if profile else pid return driver, name def closeEvent(self, event): for driver in self._active_drivers.values(): try: driver.disconnect() except Exception: pass event.accept()