polish: row numbers, ping indicator, frozen column, keyboard nav
- ResultsPanel: _RowNumberProxy keeps vertical header numbers sequential in visual order (sort-stable); both views get a 48 px wide row-number header - MainWindow: _PingWorker pings each active connection every 30 s via test_connection(); status dot (green/red) shown in status bar with tooltip - TableViewer: 📌 toggle freezes first column using dual-view overlay (_frozen_view child widget) with synced scroll, row heights, and resize event-filter; column 0 is hidden in the main view while frozen - RowDialog: OK set as default button; first field focused on open - ConnectionDialog: OK set as default; Enter in password field submits Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+71
-1
@@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
|
||||
QTabWidget, QStatusBar, QLabel, QMessageBox, QDockWidget,
|
||||
QPushButton, QApplication, QMenu, QInputDialog, QFileDialog,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QAction, QColor, QKeySequence
|
||||
|
||||
from app.ui.schema_browser import SchemaBrowser
|
||||
@@ -41,6 +41,23 @@ from app.utils.logger import get_logger
|
||||
_log = get_logger(__name__)
|
||||
|
||||
|
||||
class _PingWorker(QThread):
|
||||
"""Background thread that pings one connection and emits the result."""
|
||||
pinged = pyqtSignal(str, bool) # (profile_id, ok)
|
||||
|
||||
def __init__(self, pid: str, driver, parent=None):
|
||||
super().__init__(parent)
|
||||
self._pid = pid
|
||||
self._driver = driver
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
ok, _ = self._driver.test_connection()
|
||||
self.pinged.emit(self._pid, ok)
|
||||
except Exception:
|
||||
self.pinged.emit(self._pid, False)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
@@ -53,11 +70,19 @@ class MainWindow(QMainWindow):
|
||||
self._active_drivers: dict = {}
|
||||
# profile_id → ConnectionProfile (all loaded profiles, connected or not)
|
||||
self._all_profiles: dict = {}
|
||||
# ping state
|
||||
self._ping_results: dict = {} # profile_id → bool
|
||||
self._ping_workers: list = [] # keep QThread refs alive
|
||||
|
||||
self._build_ui()
|
||||
self._build_menus()
|
||||
self._build_status_bar()
|
||||
|
||||
# Periodic connection heartbeat (every 30 s)
|
||||
self._ping_timer = QTimer(self)
|
||||
self._ping_timer.timeout.connect(self._ping_all_connections)
|
||||
self._ping_timer.start(30_000)
|
||||
|
||||
# Load saved profiles after the window is shown
|
||||
QTimer.singleShot(0, self._load_saved_profiles)
|
||||
|
||||
@@ -234,6 +259,12 @@ class MainWindow(QMainWindow):
|
||||
self._conn_lbl.setMinimumWidth(160)
|
||||
sb.addPermanentWidget(self._conn_lbl)
|
||||
|
||||
self._ping_lbl = QLabel("")
|
||||
self._ping_lbl.setFixedWidth(16)
|
||||
self._ping_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._ping_lbl.setToolTip("Connection heartbeat")
|
||||
sb.addPermanentWidget(self._ping_lbl)
|
||||
|
||||
def _set_status(self, msg: str):
|
||||
self._status_lbl.setText(msg)
|
||||
|
||||
@@ -404,6 +435,7 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self._schema_browser.remove_connection(profile_id, keep_saved=False)
|
||||
self._all_profiles.pop(profile_id, None)
|
||||
self._ping_results.pop(profile_id, None)
|
||||
delete_profile(profile_id)
|
||||
self._set_status(f"Connection '{profile.name}' deleted.")
|
||||
|
||||
@@ -484,6 +516,44 @@ class MainWindow(QMainWindow):
|
||||
preview = value[:60] + ("…" if len(value) > 60 else "")
|
||||
self._cell_lbl.setText(f" {preview}")
|
||||
|
||||
# ── Connection heartbeat ──────────────────────────────────────────────────
|
||||
|
||||
def _ping_all_connections(self):
|
||||
# Drop finished workers
|
||||
self._ping_workers = [w for w in self._ping_workers if w.isRunning()]
|
||||
if not self._active_drivers:
|
||||
self._ping_lbl.setText("")
|
||||
return
|
||||
for pid, driver in list(self._active_drivers.items()):
|
||||
# Skip if a ping for this connection is already in flight
|
||||
if any(getattr(w, "_pid", None) == pid for w in self._ping_workers):
|
||||
continue
|
||||
worker = _PingWorker(pid, driver)
|
||||
worker.pinged.connect(self._on_ping_result)
|
||||
worker.start()
|
||||
self._ping_workers.append(worker)
|
||||
|
||||
def _on_ping_result(self, pid: str, ok: bool):
|
||||
if pid not in self._active_drivers:
|
||||
return
|
||||
self._ping_results[pid] = ok
|
||||
all_ok = all(self._ping_results.get(p, True) for p in self._active_drivers)
|
||||
color = "#a6d189" if all_ok else "#e78284"
|
||||
self._ping_lbl.setText("●")
|
||||
self._ping_lbl.setStyleSheet(f"color: {color}; font-size: 11pt;")
|
||||
lines = []
|
||||
for p_id in self._active_drivers:
|
||||
profile = self._all_profiles.get(p_id)
|
||||
name = profile.name if profile else p_id
|
||||
icon = "✅" if self._ping_results.get(p_id, True) else "❌"
|
||||
lines.append(f"{icon} {name}")
|
||||
self._ping_lbl.setToolTip("Connections:\n" + "\n".join(lines))
|
||||
if not ok:
|
||||
profile = self._all_profiles.get(pid)
|
||||
name = profile.name if profile else pid
|
||||
_log.warning("Heartbeat failed for '%s'", name)
|
||||
self._set_status(f"⚠️ Connection heartbeat failed: {name}")
|
||||
|
||||
# ── File open / save ──────────────────────────────────────────────────────
|
||||
|
||||
def _open_sql_file(self, filepath: str = None):
|
||||
|
||||
Reference in New Issue
Block a user