summaryrefslogtreecommitdiff
path: root/ui/tableviewbase.h
diff options
context:
space:
mode:
authorAlexander Khosrowshahi <alexk@vector35.com>2025-08-28 16:03:23 -0400
committerJordan <github@psifertex.com>2025-10-24 11:48:53 -0400
commit4e3ef2ce073efe0cf1c7b744e720202afe7e294e (patch)
tree9ef816922564b6a5d917ca8f8b8cb9038526d57e /ui/tableviewbase.h
parent8ffd2aa208552bcfef8d5436b62826d17f95ae86 (diff)
Add TableViewBase.h and base class QTableView uses in stringsview, memory map, searches, and tag types
Diffstat (limited to 'ui/tableviewbase.h')
-rw-r--r--ui/tableviewbase.h169
1 files changed, 169 insertions, 0 deletions
diff --git a/ui/tableviewbase.h b/ui/tableviewbase.h
new file mode 100644
index 00000000..6a36505c
--- /dev/null
+++ b/ui/tableviewbase.h
@@ -0,0 +1,169 @@
+//
+// Created by Alexander Khosrowshahi on 8/22/25.
+//
+
+#pragma once
+
+#include <QtWidgets/QTableView>
+#include <QtWidgets/QHeaderView>
+#include <QtCore/QTimer>
+#include <QMenu>
+
+/// Base class for table views in Binary Ninja views
+/// - Moveable, resizeable columns with saved state
+/// - QSettings save to Tables/<viewName>/<Suffix>
+/// - Reset columns context menu action
+class TableViewBase: public QTableView {
+ Q_OBJECT
+
+public:
+ explicit TableViewBase(QWidget* parent = nullptr, const QString& viewName = {}): QTableView(parent), m_viewName(viewName) {
+ auto* hh = horizontalHeader();
+ hh->setStretchLastSection(true);
+ hh->setSectionResizeMode(QHeaderView::Interactive);
+ hh->setSectionsMovable(true);
+ hh->setSectionsClickable(true);
+ hh->setSortIndicatorShown(true);
+ hh->setSortIndicator(0, Qt::AscendingOrder);
+ hh->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(hh, &QHeaderView::customContextMenuRequested, this,
+ [this, hh](const QPoint& p) {
+ QMenu menu(hh);
+ QAction* reset = menu.addAction(tr("Reset Column Layout"));
+ connect(reset, &QAction::triggered, this, &TableViewBase::resetColumnLayout);
+ populateHeaderContextMenu(&menu, p);
+ menu.exec(hh->viewport()->mapToGlobal(p));
+ });
+
+ m_headerSaveDebounce.setSingleShot(true);
+ m_headerSaveDebounce.setInterval(150);
+
+ connect(&m_headerSaveDebounce, &QTimer::timeout, this, &TableViewBase::saveHeaderState);
+ connect(hh, &QHeaderView::sectionResized, this, &TableViewBase::scheduleSaveHeaderState);
+ connect(hh, &QHeaderView::sectionMoved, this, &TableViewBase::scheduleSaveHeaderState);
+
+ setShowGrid(false);
+ setSortingEnabled(true);
+
+ QMetaObject::invokeMethod(this, "restoreHeaderState", Qt::QueuedConnection);
+ }
+
+
+ void setModel(QAbstractItemModel* m) override
+ {
+ QTableView::setModel(m);
+ if (!m) return;
+ connect(m, &QAbstractItemModel::modelReset, this, &TableViewBase::restoreHeaderState);
+ connect(m, &QAbstractItemModel::columnsInserted, this, &TableViewBase::restoreHeaderState);
+ connect(m, &QAbstractItemModel::columnsRemoved, this, &TableViewBase::restoreHeaderState);
+
+ // Grab default header state
+ QTimer::singleShot(0, this, [this]{
+ captureDefaultHeaderState();
+ });
+
+ QMetaObject::invokeMethod(this, "restoreHeaderState", Qt::QueuedConnection);
+ }
+
+ // Save after debounce for repeated move/drag
+ void scheduleSaveHeaderState() { m_headerSaveDebounce.start(); }
+
+Q_SIGNALS:
+ // For owners/derived classes to add their own menu items
+ void populateHeaderContextMenu(QMenu*, const QPoint&);
+
+protected:
+ QString viewName() const {
+ if (!m_viewName.isEmpty()) return m_viewName;
+ if (!objectName().isEmpty()) return objectName();
+ return metaObject()->className();
+ }
+
+ QString settingsKey(const QString& suffix) const {
+ return QStringLiteral("tables/%1/%2").arg(viewName(), suffix);
+ }
+
+ void saveHeaderState() const {
+ auto* hh = horizontalHeader();
+ if (!hh) return;
+ QSettings s;
+ s.setValue(settingsKey("horizontalHeaderState"), hh->saveState());
+ }
+
+ void restoreHeaderState() const
+ {
+ auto* hh = horizontalHeader();
+ if (!hh) return;
+ QSettings s;
+ const QByteArray st = s.value(settingsKey("horizontalHeaderState")).toByteArray();
+ if (!st.isEmpty()) hh->restoreState(st);
+
+ const QByteArray def = s.value(settingsKey("horizontalHeaderDefaultState")).toByteArray();
+ if (!def.isEmpty()) hh->restoreState(def);
+ }
+
+ virtual int defaultSectionWidth(const int logicalIndex, const int charWidth) const
+ {
+ QString headerText;
+ if (model()) {
+ headerText = model()->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
+ }
+ constexpr int minChars = 8;
+ const int headerChars = qMax(minChars, headerText.size() + 2);
+ return headerChars * charWidth;
+ }
+
+ /// Grabs default header states on startup to save
+ /// Kind of a hacky fix, but many of our tables have manually set widths,
+ /// so compensating for them is a hassle.
+ void captureDefaultHeaderState() const
+ {
+ auto* hh = horizontalHeader();
+ if (!hh) return;
+
+ QSettings s;
+ const auto key = settingsKey("horizontalHeaderDefaultState");
+ if (!s.contains(key)) {
+ s.setValue(key, hh->saveState());
+ s.sync();
+ }
+ }
+
+
+ void resetColumnLayout() const
+ {
+ auto* hh = horizontalHeader();
+ if (!hh || !model()) return;
+
+ {
+ QSettings s;
+ s.remove(settingsKey("horizontalHeaderState"));
+ }
+
+ QSettings s;
+ const QByteArray def = s.value(settingsKey("horizontalHeaderDefaultState")).
+ toByteArray();
+ if (!def.isEmpty() && hh->restoreState(def))
+ {
+ return;
+ }
+
+ // If no default or set user layout, use size hints
+ for (int c = 0; c < model()->columnCount(); ++c)
+ {
+ constexpr int extra = 12;
+ int w = sizeHintForColumn(c);
+ QVariant head = model()->headerData(c, Qt::Horizontal, Qt::SizeHintRole);
+ int headerW = hh->sectionSizeHint(c);
+ if (head.canConvert<QSize>())
+ headerW = std::max(headerW, head.toSize().width());
+ hh->resizeSection(c, std::max(w, headerW) + extra);
+ }
+ }
+
+
+private:
+ QString m_viewName;
+ QTimer m_headerSaveDebounce;
+
+};