""" Results panel — shows query result data, DML messages, errors, and export controls. Single query: one result shown directly (tab bar hidden). Script with multiple statements: each result set shown in its own named tab. """ import io import csv import os from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableView, QLabel, QPushButton, QHeaderView, QAbstractItemView, QFileDialog, QMessageBox, QStackedWidget, QPlainTextEdit, QProgressBar, QTabWidget, QApplication, QMenu, ) 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._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) # ── Stack: 0 = result tabs, 1 = loading ────────────────────────────── self._stack = QStackedWidget() self._result_tabs = QTabWidget() self._result_tabs.tabBar().setVisible(False) self._stack.addWidget(self._result_tabs) # idx 0 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 1 root.addWidget(self._stack, 1) self._set_export_visible(False) # ── Tab helpers ─────────────────────────────────────────────────────────── def _clear_tabs(self): while self._result_tabs.count(): self._result_tabs.removeTab(0) def _make_table_tab(self, cols: list, rows: list) -> tuple: model = ResultTableModel() model.set_data(cols, rows) proxy = QSortFilterProxyModel() proxy.setSourceModel(model) table = QTableView() table.setModel(proxy) table.setSortingEnabled(True) table.setAlternatingRowColors(True) table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeMode.Interactive) table.horizontalHeader().setStretchLastSection(True) table.verticalHeader().setDefaultSectionSize(24) table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) table.customContextMenuRequested.connect( lambda pos, t=table, p=proxy, m=model: self._table_context_menu(pos, t, p, m) ) return table, model, proxy def _make_message_tab(self, msg: str, is_error: bool = False) -> QPlainTextEdit: view = QPlainTextEdit() view.setReadOnly(True) view.setFont(QFont("Consolas", 11)) view.setPlainText(msg) if is_error: view.setStyleSheet("color: #e78284;") return view def _current_model(self) -> ResultTableModel | None: widget = self._result_tabs.currentWidget() if isinstance(widget, QTableView): proxy = widget.model() if isinstance(proxy, QSortFilterProxyModel): return proxy.sourceModel() return None def _auto_resize(self, table: QTableView): table.resizeColumnsToContents() h = table.horizontalHeader() for i in range(h.count()): if h.sectionSize(i) > 300: h.resizeSection(i, 300) # ── Public API ──────────────────────────────────────────────────────────── def show_loading(self): self._clear_tabs() self._stack.setCurrentIndex(1) self._status_lbl.setText("Running…") self._set_export_visible(False) def show_data(self, cols: list, rows: list, count: int, elapsed: float): table, model, _ = self._make_table_tab(cols, rows) self._clear_tabs() self._result_tabs.addTab(table, "Result") self._result_tabs.tabBar().setVisible(False) 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(table) def show_message(self, msg: str): view = self._make_message_tab(msg) self._clear_tabs() self._result_tabs.addTab(view, "Message") self._result_tabs.tabBar().setVisible(False) self._stack.setCurrentIndex(0) self._status_lbl.setText(msg[:100]) self._set_export_visible(False) def show_error(self, msg: str): view = self._make_message_tab(f"❌ {msg}", is_error=True) self._clear_tabs() self._result_tabs.addTab(view, "Error") self._result_tabs.tabBar().setVisible(False) self._stack.setCurrentIndex(0) short = msg[:80] self._status_lbl.setText(f"Error: {short}") self.status_message.emit(f"Error: {short}") self._set_export_visible(False) def show_script_results(self, results: list): """ Show each statement's result in a separate tab. results: list of (cols, rows, count, message_str) """ self._clear_tabs() last_table_idx = -1 has_data = False for i, (cols, rows, cnt, msg) in enumerate(results): if cols: table, _, _ = self._make_table_tab(cols, rows) label = f"Result {i + 1} ({cnt:,})" self._result_tabs.addTab(table, label) last_table_idx = self._result_tabs.count() - 1 has_data = True self._auto_resize(table) else: view = self._make_message_tab(msg or f"Statement {i + 1}: OK") label = f"Step {i + 1}" self._result_tabs.addTab(view, label) multi = self._result_tabs.count() > 1 self._result_tabs.tabBar().setVisible(multi) if last_table_idx >= 0: self._result_tabs.setCurrentIndex(last_table_idx) self._stack.setCurrentIndex(0) n_results = sum(1 for c, *_ in results if c) total_rows = sum(cnt for c, _, cnt, _ in results if c) self._status_lbl.setText( f"{len(results)} statement(s) — " f"{n_results} result set(s), {total_rows:,} total row(s)" ) self.status_message.emit( f"{len(results)} statements executed" ) self._set_export_visible(has_data) 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 _do_export_csv(self): model = self._current_model() if not model: return path, _ = QFileDialog.getSaveFileName( self, "Export CSV", "results.csv", "CSV Files (*.csv)") if path: try: 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): model = self._current_model() if not model: return path, _ = QFileDialog.getSaveFileName( self, "Export JSON", "results.json", "JSON Files (*.json)") if path: try: 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): model = self._current_model() if not model: return path, _ = QFileDialog.getSaveFileName( self, "Export SQL", "results.sql", "SQL Files (*.sql)") if path: try: 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, table: QTableView, proxy: QSortFilterProxyModel, model: ResultTableModel): idx = table.indexAt(pos) if not idx.isValid(): return menu = QMenu(self) menu.addAction("📋 Copy cell", lambda: self._copy_cell(idx, proxy)) menu.addAction("📋 Copy row", lambda: self._copy_row(idx, proxy, model)) menu.addAction("📋 Copy all", lambda: self._copy_all(model)) menu.exec(table.viewport().mapToGlobal(pos)) def _copy_cell(self, idx, proxy): val = proxy.data(idx, Qt.ItemDataRole.DisplayRole) or "" QApplication.clipboard().setText(str(val)) def _copy_row(self, idx, proxy, model): row = proxy.mapToSource(idx).row() vals = [str(v or "") for v in model.get_row(row)] QApplication.clipboard().setText("\t".join(vals)) def _copy_all(self, model): buf = io.StringIO() writer = csv.writer(buf) writer.writerow(model.column_names()) for r in range(model.rowCount()): writer.writerow(model.get_row(r)) QApplication.clipboard().setText(buf.getvalue())