diff --git a/ui/memorymap.h b/ui/memorymap.h index 35d79443a6..82b7f07a96 100644 --- a/ui/memorymap.h +++ b/ui/memorymap.h @@ -17,6 +17,7 @@ #include "uitypes.h" #include "fontsettings.h" #include "viewframe.h" +#include "tableviewbase.h" /*! @@ -172,14 +173,13 @@ class BINARYNINJAUIAPI SegmentWidget : public QWidget Q_OBJECT BinaryViewRef m_data; - QTableView* m_table; + TableViewBase* m_table; SegmentModel* m_model; QSortFilterProxyModel* m_proxyModel; std::mutex m_updateMutex; //void updateInfo(); void showContextMenu(const QPoint& point); - QMenu* createHeaderContextMenu(const QPoint& p); void restoreDefaults(); void addMemoryRegion(SegmentRef segment); @@ -251,7 +251,7 @@ class BINARYNINJAUIAPI SectionWidget : public QWidget Q_OBJECT BinaryViewRef m_data; - QTableView* m_table; + TableViewBase* m_table; SectionModel* m_model; QSortFilterProxyModel* m_proxyModel; std::mutex m_updateMutex; diff --git a/ui/searchresult.h b/ui/searchresult.h index 9fe28996a4..48c67de196 100644 --- a/ui/searchresult.h +++ b/ui/searchresult.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -173,7 +174,7 @@ class SearchResultWidget; /*! \ingroup searchresult */ -class BINARYNINJAUIAPI SearchResultTable : public QTableView +class BINARYNINJAUIAPI SearchResultTable : public TableViewBase { Q_OBJECT diff --git a/ui/stringsview.h b/ui/stringsview.h index 2e1359e024..c4c01b1c41 100644 --- a/ui/stringsview.h +++ b/ui/stringsview.h @@ -9,6 +9,7 @@ #include "render.h" #include "filter.h" #include "uicontext.h" +#include "tableviewbase.h" #define STRINGS_LIST_UPDATE_INTERVAL 250 @@ -133,7 +134,7 @@ class StringsViewSidebarWidget; \ingroup stringsview */ -class BINARYNINJAUIAPI StringsView : public QTableView, public View, public FilterTarget +class BINARYNINJAUIAPI StringsView : public TableViewBase, public View, public FilterTarget { Q_OBJECT @@ -148,6 +149,9 @@ class BINARYNINJAUIAPI StringsView : public QTableView, public View, public Filt uint64_t m_selectionBegin, m_selectionEnd; uint64_t m_currentlySelectedDataAddress; + QPointer m_horizontalHeader; + QPointer m_verticalHeader; + public: StringsView(BinaryViewRef data, StringsContainer* container); @@ -190,6 +194,7 @@ class BINARYNINJAUIAPI StringsView : public QTableView, public View, public Filt virtual void mousePressEvent(QMouseEvent* event) override; virtual void paintEvent(QPaintEvent* event) override; virtual bool event(QEvent* event) override; + int defaultSectionWidth(int logicalIndex, int charWidth) const override; private Q_SLOTS: void goToString(const QModelIndex& idx); diff --git a/ui/tableviewbase.h b/ui/tableviewbase.h new file mode 100644 index 0000000000..6a36505c26 --- /dev/null +++ b/ui/tableviewbase.h @@ -0,0 +1,169 @@ +// +// Created by Alexander Khosrowshahi on 8/22/25. +// + +#pragma once + +#include +#include +#include +#include + +/// Base class for table views in Binary Ninja views +/// - Moveable, resizeable columns with saved state +/// - QSettings save to Tables// +/// - 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()) + headerW = std::max(headerW, head.toSize().width()); + hh->resizeSection(c, std::max(w, headerW) + extra); + } + } + + +private: + QString m_viewName; + QTimer m_headerSaveDebounce; + +}; diff --git a/ui/tagtypelist.h b/ui/tagtypelist.h index 62950405cb..ee6793c344 100644 --- a/ui/tagtypelist.h +++ b/ui/tagtypelist.h @@ -8,6 +8,7 @@ #include #include "binaryninjaapi.h" #include "viewframe.h" +#include "tableviewbase.h" #define TAGS_UPDATE_CHECK_INTERVAL 200 @@ -88,7 +89,7 @@ class BINARYNINJAUIAPI TagTypeItemDelegate : public QItemDelegate \ingroup tagtypelist */ -class BINARYNINJAUIAPI TagTypeList : public QTableView, public BinaryNinja::BinaryDataNotification +class BINARYNINJAUIAPI TagTypeList : public TableViewBase, public BinaryNinja::BinaryDataNotification { Q_OBJECT