""" Results panel — shows query result data, DML messages, errors, and export controls. """ import os from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableView, QLabel, QPushButton, QHeaderView, QAbstractItemView, QFileDialog, QMessageBox, QStackedWidget, QPlainTextEdit, QProgressBar, ) from PyQt6.QtCore import Qt, pyqtSignal, QSortFilterProxyModel from PyQt6.QtGui import QColor, QFont from app.models.result_table_model import ResultTableModel class ResultsPanel(QWidget): status_message = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._model = ResultTableModel() self._build_ui() # ── UI ──────────────────────────────────────────────────────────────────── def _build_ui(self): root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) # ── Status / toolbar ───────────────────────────────────────────────── self._toolbar = QHBoxLayout() self._toolbar.setContentsMargins(8, 4, 8, 4) self._toolbar.setSpacing(8) self._status_lbl = QLabel("Ready") self._status_lbl.setObjectName("statusLabel") self._export_csv = QPushButton("⬇ CSV") self._export_json = QPushButton("⬇ JSON") self._export_sql = QPushButton("⬇ SQL") self._export_csv.setFixedWidth(72) self._export_json.setFixedWidth(72) self._export_sql.setFixedWidth(72) self._export_csv.clicked.connect(self._do_export_csv) self._export_json.clicked.connect(self._do_export_json) self._export_sql.clicked.connect(self._do_export_sql) self._toolbar.addWidget(self._status_lbl, 1) self._toolbar.addWidget(self._export_csv) self._toolbar.addWidget(self._export_json) self._toolbar.addWidget(self._export_sql) root.addLayout(self._toolbar) # ── Stacked pages ───────────────────────────────────────────────────── self._stack = QStackedWidget() # Page 0 — table self._table = QTableView() self._proxy = QSortFilterProxyModel() self._proxy.setSourceModel(self._model) self._table.setModel(self._proxy) self._table.setSortingEnabled(True) self._table.setAlternatingRowColors(True) self._table.setSelectionBehavior( QAbstractItemView.SelectionBehavior.SelectItems) self._table.setSelectionMode( QAbstractItemView.SelectionMode.ExtendedSelection) self._table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeMode.Interactive) self._table.horizontalHeader().setStretchLastSection(True) self._table.verticalHeader().setDefaultSectionSize(24) self._table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._table.customContextMenuRequested.connect(self._table_context_menu) self._stack.addWidget(self._table) # idx 0 # Page 1 — message / log self._msg_view = QPlainTextEdit() self._msg_view.setReadOnly(True) self._msg_view.setFont(QFont("Consolas", 11)) self._stack.addWidget(self._msg_view) # idx 1 # Page 2 — loading spinner loading = QWidget() ll = QVBoxLayout(loading) ll.setAlignment(Qt.AlignmentFlag.AlignCenter) bar = QProgressBar() bar.setMaximum(0) bar.setFixedWidth(200) ll.addWidget(QLabel("Executing query…")) ll.addWidget(bar) self._stack.addWidget(loading) # idx 2 root.addWidget(self._stack, 1) self._set_export_visible(False) # ── Public API ──────────────────────────────────────────────────────────── def show_loading(self): self._model.clear() self._stack.setCurrentIndex(2) self._status_lbl.setText("Running…") self._set_export_visible(False) def show_data(self, cols: list, rows: list, count: int, elapsed: float): self._model.set_data(cols, rows) self._stack.setCurrentIndex(0) t = f"{elapsed:.3f}s" if elapsed else "" self._status_lbl.setText(f"{count:,} row(s) {t}") self.status_message.emit(f"Fetched {count:,} rows {t}") self._set_export_visible(bool(cols)) self._auto_resize() def show_message(self, msg: str): self._msg_view.appendPlainText(msg) self._stack.setCurrentIndex(1) self._status_lbl.setText(msg) self._set_export_visible(False) def show_error(self, msg: str): self._msg_view.setPlainText(f"❌ {msg}") self._stack.setCurrentIndex(1) self._status_lbl.setText(f"Error: {msg[:80]}") self.status_message.emit(f"Error: {msg[:80]}") self._set_export_visible(False) def export_dialog(self): self._do_export_csv() # ── Private helpers ─────────────────────────────────────────────────────── def _set_export_visible(self, v: bool): self._export_csv.setVisible(v) self._export_json.setVisible(v) self._export_sql.setVisible(v) def _auto_resize(self): header = self._table.horizontalHeader() for i in range(self._model.columnCount()): header.resizeSection( i, min(self._table.columnWidth(i) + 20, 300) ) self._table.resizeColumnsToContents() def _do_export_csv(self): path, _ = QFileDialog.getSaveFileName( self, "Export CSV", "results.csv", "CSV Files (*.csv)") if path: try: self._model.export_csv(path) QMessageBox.information(self, "Exported", f"Saved to {path}") except Exception as e: QMessageBox.critical(self, "Error", str(e)) def _do_export_json(self): path, _ = QFileDialog.getSaveFileName( self, "Export JSON", "results.json", "JSON Files (*.json)") if path: try: self._model.export_json(path) QMessageBox.information(self, "Exported", f"Saved to {path}") except Exception as e: QMessageBox.critical(self, "Error", str(e)) def _do_export_sql(self): path, _ = QFileDialog.getSaveFileName( self, "Export SQL", "results.sql", "SQL Files (*.sql)") if path: try: self._model.export_sql(path) QMessageBox.information(self, "Exported", f"Saved to {path}") except Exception as e: QMessageBox.critical(self, "Error", str(e)) def _table_context_menu(self, pos): from PyQt6.QtWidgets import QMenu from PyQt6.QtGui import QClipboard from PyQt6.QtWidgets import QApplication idx = self._table.indexAt(pos) if not idx.isValid(): return menu = QMenu(self) menu.addAction("📋 Copy cell", lambda: self._copy_cell(idx)) menu.addAction("📋 Copy row", lambda: self._copy_row(idx)) menu.addAction("📋 Copy all", lambda: self._copy_all()) menu.exec(self._table.viewport().mapToGlobal(pos)) def _copy_cell(self, idx): from PyQt6.QtWidgets import QApplication val = self._proxy.data(idx, Qt.ItemDataRole.DisplayRole) or "" QApplication.clipboard().setText(str(val)) def _copy_row(self, idx): from PyQt6.QtWidgets import QApplication row = self._proxy.mapToSource(idx).row() vals = [str(v or "") for v in self._model.get_row(row)] QApplication.clipboard().setText("\t".join(vals)) def _copy_all(self): from PyQt6.QtWidgets import QApplication import csv, io buf = io.StringIO() writer = csv.writer(buf) writer.writerow(self._model.column_names()) for r in range(self._model.rowCount()): writer.writerow(self._model.get_row(r)) QApplication.clipboard().setText(buf.getvalue())