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:
+97
-1
@@ -16,7 +16,7 @@ from PyQt6.QtWidgets import (
|
||||
QMenu, QApplication,
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
Qt, QAbstractTableModel, QModelIndex, pyqtSignal,
|
||||
Qt, QAbstractTableModel, QModelIndex, pyqtSignal, QEvent,
|
||||
)
|
||||
from PyQt6.QtGui import QColor, QBrush, QFont, QKeySequence, QShortcut
|
||||
|
||||
@@ -267,8 +267,15 @@ class RowDialog(QDialog):
|
||||
)
|
||||
bbox.accepted.connect(self.accept)
|
||||
bbox.rejected.connect(self.reject)
|
||||
ok_btn = bbox.button(QDialogButtonBox.StandardButton.Ok)
|
||||
if ok_btn:
|
||||
ok_btn.setDefault(True)
|
||||
root.addWidget(bbox)
|
||||
|
||||
# Focus the first input field so the user can start typing immediately
|
||||
if self._fields:
|
||||
next(iter(self._fields.values())).setFocus()
|
||||
|
||||
@property
|
||||
def values(self) -> dict:
|
||||
"""Return {col: value_or_None} for all fields.
|
||||
@@ -380,6 +387,13 @@ class TableViewer(QWidget):
|
||||
tb.addWidget(self._edit_btn)
|
||||
tb.addWidget(self._delete_btn)
|
||||
tb.addStretch()
|
||||
|
||||
self._freeze_btn = QPushButton("📌")
|
||||
self._freeze_btn.setFixedWidth(34)
|
||||
self._freeze_btn.setToolTip("Freeze first column (keep it visible while scrolling)")
|
||||
self._freeze_btn.setCheckable(True)
|
||||
self._freeze_btn.clicked.connect(self._toggle_freeze)
|
||||
tb.addWidget(self._freeze_btn)
|
||||
root.addLayout(tb)
|
||||
|
||||
# ── Table view ─────────────────────────────────────────────────────────
|
||||
@@ -395,6 +409,7 @@ class TableViewer(QWidget):
|
||||
QHeaderView.ResizeMode.Interactive)
|
||||
self._table_view.horizontalHeader().setStretchLastSection(True)
|
||||
self._table_view.verticalHeader().setDefaultSectionSize(24)
|
||||
self._table_view.verticalHeader().setFixedWidth(48)
|
||||
self._table_view.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self._table_view.customContextMenuRequested.connect(self._context_menu)
|
||||
@@ -415,6 +430,40 @@ class TableViewer(QWidget):
|
||||
self._delete_selected)
|
||||
QShortcut(QKeySequence("Ins"), self._table_view, self._add_row)
|
||||
|
||||
# ── Frozen first-column overlay ───────────────────────────────────────
|
||||
self._freeze_active = False
|
||||
self._frozen_view = QTableView(self._table_view)
|
||||
self._frozen_view.setModel(self._model)
|
||||
self._frozen_view.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self._frozen_view.setVerticalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self._frozen_view.setHorizontalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self._frozen_view.setAlternatingRowColors(True)
|
||||
self._frozen_view.verticalHeader().hide()
|
||||
self._frozen_view.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Fixed)
|
||||
self._frozen_view.setStyleSheet(
|
||||
"QTableView { border: none; border-right: 2px solid #8caaee; }")
|
||||
self._frozen_view.hide()
|
||||
|
||||
# Sync vertical scroll between the two views
|
||||
self._table_view.verticalScrollBar().valueChanged.connect(
|
||||
self._frozen_view.verticalScrollBar().setValue)
|
||||
self._frozen_view.verticalScrollBar().valueChanged.connect(
|
||||
self._table_view.verticalScrollBar().setValue)
|
||||
|
||||
# Sync row heights
|
||||
self._table_view.verticalHeader().sectionResized.connect(
|
||||
lambda row, _, h: self._frozen_view.setRowHeight(row, h))
|
||||
|
||||
# Update frozen geometry when column 0 is resized
|
||||
self._table_view.horizontalHeader().sectionResized.connect(
|
||||
lambda col, _old, _new: self._update_frozen_geometry() if col == 0 else None)
|
||||
|
||||
# Event filter to handle main view resize
|
||||
self._table_view.installEventFilter(self)
|
||||
|
||||
root.addWidget(self._table_view, 1)
|
||||
|
||||
# ── Pagination bar (prominent, always visible) ─────────────────────────
|
||||
@@ -505,11 +554,58 @@ class TableViewer(QWidget):
|
||||
self._refresh_action_states()
|
||||
self._table_view.resizeColumnsToContents()
|
||||
self._table_view.horizontalHeader().setStretchLastSection(True)
|
||||
if self._freeze_active and self._model.columnCount() > 0:
|
||||
self._apply_freeze()
|
||||
|
||||
def _on_error(self, msg: str):
|
||||
self._total_lbl.setText(f"Error: {msg[:80]}")
|
||||
self.status_message.emit(f"Error: {msg}")
|
||||
|
||||
# ── Frozen column ──────────────────────────────────────────────────────────
|
||||
|
||||
def _toggle_freeze(self, checked: bool):
|
||||
self._freeze_active = checked
|
||||
if checked and self._model.columnCount() > 0:
|
||||
self._apply_freeze()
|
||||
elif not checked:
|
||||
self._unapply_freeze()
|
||||
|
||||
def _apply_freeze(self):
|
||||
col_count = self._model.columnCount()
|
||||
if col_count == 0:
|
||||
return
|
||||
self._frozen_view.setSelectionModel(self._table_view.selectionModel())
|
||||
for c in range(col_count):
|
||||
self._frozen_view.setColumnHidden(c, c != 0)
|
||||
self._frozen_view.resizeColumnToContents(0)
|
||||
self._table_view.setColumnHidden(0, True)
|
||||
self._update_frozen_geometry()
|
||||
self._frozen_view.show()
|
||||
self._frozen_view.raise_()
|
||||
|
||||
def _unapply_freeze(self):
|
||||
self._frozen_view.hide()
|
||||
if self._model.columnCount() > 0:
|
||||
self._table_view.setColumnHidden(0, False)
|
||||
|
||||
def _update_frozen_geometry(self):
|
||||
if not self._freeze_active:
|
||||
return
|
||||
fw = self._table_view.frameWidth()
|
||||
vh_w = self._table_view.verticalHeader().width()
|
||||
col0_w = self._frozen_view.columnWidth(0)
|
||||
self._frozen_view.setGeometry(
|
||||
fw + vh_w,
|
||||
fw,
|
||||
col0_w,
|
||||
self._table_view.height() - 2 * fw,
|
||||
)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if obj is self._table_view and event.type() == QEvent.Type.Resize:
|
||||
self._update_frozen_geometry()
|
||||
return False
|
||||
|
||||
def _refresh_pagination(self):
|
||||
ps = self._page_size if self._page_size > 0 else max(self._total, 1)
|
||||
rows_loaded = self._model.rowCount()
|
||||
|
||||
Reference in New Issue
Block a user