feat: preferences dialog, SQL formatter, and enhanced table context menu
- PreferencesDialog (Tools → Preferences, Ctrl+,): font family/size, word wrap, default page size, query timeout, max history, auto-commit; font changes apply live to all open editors via apply_settings() cascade - CodeEditor and TableViewer now read initial values from settings singleton - SQL formatter (≡ Format button): uses sqlparse to reindent and uppercase keywords; gracefully prompts to install sqlparse if missing - Table viewer context menu: "Filter by this value" (populates WHERE and reloads), "Copy row as SQL INSERT" (ready-to-paste INSERT statements), renamed "Copy row" → "Copy row as TSV" for clarity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+48
-2
@@ -21,6 +21,7 @@ from PyQt6.QtCore import (
|
||||
from PyQt6.QtGui import QColor, QBrush, QFont, QKeySequence, QShortcut
|
||||
|
||||
from app.utils.worker import TableDataWorker
|
||||
from app.config.settings import get_settings
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -310,7 +311,8 @@ class TableViewer(QWidget):
|
||||
self._table = table
|
||||
self._offset = 0
|
||||
self._total = 0
|
||||
self._page_size = 100 # default
|
||||
ps = get_settings().get("result_page_size", 100)
|
||||
self._page_size = ps if ps > 0 else 10_000_000 # 0 → All
|
||||
self._model = EditableTableModel()
|
||||
self._worker: TableDataWorker | None = None
|
||||
self._build_ui()
|
||||
@@ -609,14 +611,22 @@ class TableViewer(QWidget):
|
||||
|
||||
def _context_menu(self, pos):
|
||||
rows = self._selected_logical_rows()
|
||||
idx = self._table_view.indexAt(pos)
|
||||
menu = QMenu(self)
|
||||
menu.addAction("➕ Add Row", self._add_row)
|
||||
if rows:
|
||||
menu.addAction("✏️ Edit Row", self._edit_selected)
|
||||
menu.addAction("🗑️ Delete Row", self._delete_selected)
|
||||
menu.addSeparator()
|
||||
if idx.isValid():
|
||||
menu.addAction("🔍 Filter by this value",
|
||||
lambda: self._filter_by_cell(idx))
|
||||
menu.addSeparator()
|
||||
menu.addAction("📋 Copy cell value", lambda: self._copy_cell(pos))
|
||||
menu.addAction("📋 Copy row", self._copy_selected_rows)
|
||||
menu.addAction("📋 Copy row as TSV", self._copy_selected_rows)
|
||||
if rows:
|
||||
menu.addAction("📋 Copy row as SQL INSERT",
|
||||
self._copy_selected_as_insert)
|
||||
menu.exec(self._table_view.viewport().mapToGlobal(pos))
|
||||
|
||||
# ── Double-click: edit ────────────────────────────────────────────────────
|
||||
@@ -725,3 +735,39 @@ class TableViewer(QWidget):
|
||||
row_data = self._model.get_row_current(r)
|
||||
lines.append("\t".join("" if v is None else str(v) for v in row_data))
|
||||
QApplication.clipboard().setText("\n".join(lines))
|
||||
|
||||
def _copy_selected_as_insert(self):
|
||||
rows = self._selected_logical_rows()
|
||||
cols = self._model.column_names()
|
||||
col_list = ", ".join(f'"{c}"' for c in cols)
|
||||
statements = []
|
||||
for r in rows:
|
||||
row_data = self._model.get_row_current(r)
|
||||
values = []
|
||||
for v in row_data:
|
||||
if v is None:
|
||||
values.append("NULL")
|
||||
elif isinstance(v, (int, float)):
|
||||
values.append(str(v))
|
||||
else:
|
||||
escaped = str(v).replace("'", "''")
|
||||
values.append(f"'{escaped}'")
|
||||
statements.append(
|
||||
f'INSERT INTO "{self._table}" ({col_list}) VALUES ({", ".join(values)});'
|
||||
)
|
||||
QApplication.clipboard().setText("\n".join(statements))
|
||||
|
||||
def _filter_by_cell(self, index):
|
||||
if not index.isValid():
|
||||
return
|
||||
col_name = self._model.column_names()[index.column()]
|
||||
val = self._model.data(index, Qt.ItemDataRole.DisplayRole)
|
||||
if val is None:
|
||||
clause = f'"{col_name}" IS NULL'
|
||||
elif isinstance(val, str):
|
||||
escaped = val.replace("'", "''")
|
||||
clause = f'"{col_name}" = \'{escaped}\''
|
||||
else:
|
||||
clause = f'"{col_name}" = {val}'
|
||||
self._filter_input.setText(clause)
|
||||
self._apply_filter()
|
||||
|
||||
Reference in New Issue
Block a user