diff options
| author | Jordan Wiens <github@psifertex.com> | 2025-12-19 14:18:19 -0500 |
|---|---|---|
| committer | Jordan Wiens <github@psifertex.com> | 2025-12-19 15:20:22 -0500 |
| commit | dbdf3381e2dd32527cffaca6081fc5a9913f1cc7 (patch) | |
| tree | 4f10dc3101700a63f7e83a087e69b5b1659fb61a /examples | |
| parent | 1cabd2def9da8ee106be26308b96a2a91cd92a2f (diff) | |
triage view responsive layout
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/triage/headers.cpp | 134 | ||||
| -rw-r--r-- | examples/triage/headers.h | 29 | ||||
| -rw-r--r-- | examples/triage/strings.cpp | 30 | ||||
| -rw-r--r-- | examples/triage/strings.h | 3 | ||||
| -rw-r--r-- | examples/triage/view.cpp | 43 | ||||
| -rw-r--r-- | examples/triage/view.h | 6 |
6 files changed, 226 insertions, 19 deletions
diff --git a/examples/triage/headers.cpp b/examples/triage/headers.cpp index 8e0d4df3..f2165de0 100644 --- a/examples/triage/headers.cpp +++ b/examples/triage/headers.cpp @@ -361,16 +361,111 @@ QString PEHeaders::GetNameOfEnumerationMember(BinaryViewRef data, const std::str } -HeaderWidget::HeaderWidget(QWidget* parent, const Headers& header) : QWidget(parent) +HeaderWidget::HeaderWidget(QWidget* parent, const Headers& header) : QWidget(parent), m_headers(header) { - QGridLayout* layout = new QGridLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->setVerticalSpacing(1); + m_layout = new QGridLayout(); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setVerticalSpacing(1); + m_layout->setHorizontalSpacing(2); + setLayout(m_layout); + m_currentColumns = (int)header.GetColumns(); + m_pendingWidth = -1; + + // Create timer for debouncing resize events + m_resizeTimer = new QTimer(this); + m_resizeTimer->setSingleShot(true); + m_resizeTimer->setInterval(50); // 50ms delay after resize stops + connect(m_resizeTimer, &QTimer::timeout, this, &HeaderWidget::performDelayedResize); + + rebuildLayout(); +} + + +void HeaderWidget::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + updateColumns(this->width()); +} + + +void HeaderWidget::updateColumns(int width) +{ + m_pendingWidth = width; + m_resizeTimer->start(); +} + + +void HeaderWidget::performDelayedResize() +{ + if (m_pendingWidth < 0) + return; + + int width = m_pendingWidth; + m_pendingWidth = -1; + + int desiredColumns; + + // Add hysteresis to prevent thrashing when width oscillates near breakpoints + if (m_currentColumns == 1) + { + // Growing from 1 column: need to exceed threshold to switch + if (width >= TriageBreakpoints::NARROW + 40) + desiredColumns = (width >= TriageBreakpoints::MEDIUM + 40) ? (int)m_headers.GetColumns() : 2; + else + desiredColumns = 1; + } + else if (m_currentColumns == 2) + { + // From 2 columns: wider hysteresis band + if (width < TriageBreakpoints::NARROW - 40) + desiredColumns = 1; + else if (width >= TriageBreakpoints::MEDIUM + 40) + desiredColumns = (int)m_headers.GetColumns(); + else + desiredColumns = 2; + } + else + { + // Shrinking from 3 columns: need to fall below threshold to switch + if (width < TriageBreakpoints::NARROW - 40) + desiredColumns = 1; + else if (width < TriageBreakpoints::MEDIUM - 40) + desiredColumns = 2; + else + desiredColumns = (int)m_headers.GetColumns(); + } + + if (desiredColumns != m_currentColumns) + { + m_currentColumns = desiredColumns; + rebuildLayout(); + } +} + + +void HeaderWidget::rebuildLayout() +{ + // Disable updates during rebuild to prevent flickering + setUpdatesEnabled(false); + + // Clear existing layout + QLayoutItem* item; + while ((item = m_layout->takeAt(0)) != nullptr) + { + if (item->widget()) + { + item->widget()->hide(); // Hide before deletion to reduce flicker + item->widget()->deleteLater(); // Use deleteLater() to safely delete during events + } + delete item; + } + + // Rebuild with current column count int row = 0; int col = 0; - for (auto& field : header.GetFields()) + for (auto& field : m_headers.GetFields()) { - layout->addWidget(new QLabel(field.title + ": "), row, col * 3); + m_layout->addWidget(new QLabel(field.title + ": "), row, col * 3); // For text fields with multiple values, join them with newlines for copying QString copyText; @@ -401,18 +496,31 @@ HeaderWidget::HeaderWidget(QWidget* parent, const Headers& header) : QWidget(par copyLabel->setCopyText(copyText); label = copyLabel; } - layout->addWidget(label, row, col * 3 + 1); + m_layout->addWidget(label, row, col * 3 + 1); row++; } - if ((header.GetColumns() > 1) && (row >= (int)header.GetRowsPerColumn()) - && ((col + 1) < (int)header.GetColumns())) + if ((m_currentColumns > 1) && (row >= (int)m_headers.GetRowsPerColumn()) + && ((col + 1) < m_currentColumns)) { row = 0; col++; } } - for (col = 1; col < (int)header.GetColumns(); col++) - layout->setColumnMinimumWidth(col * 3 - 1, UIContext::getScaledWindowSize(20, 20).width()); - layout->setColumnStretch((int)header.GetColumns() * 3 - 1, 1); - setLayout(layout); + + // Clear all column stretches and minimum widths first + for (col = 0; col < 9; col++) // Max 3 columns * 3 grid columns each + { + m_layout->setColumnStretch(col, 0); + m_layout->setColumnMinimumWidth(col, 0); + } + + // Set spacing columns to minimum width + for (col = 1; col < m_currentColumns; col++) + m_layout->setColumnMinimumWidth(col * 3 - 1, UIContext::getScaledWindowSize(20, 20).width()); + + // Set last column to stretch + m_layout->setColumnStretch(m_currentColumns * 3 - 1, 1); + + // Re-enable updates and force a single repaint + setUpdatesEnabled(true); } diff --git a/examples/triage/headers.h b/examples/triage/headers.h index de14165a..84bfa018 100644 --- a/examples/triage/headers.h +++ b/examples/triage/headers.h @@ -2,9 +2,21 @@ #include <QtWidgets/QLabel> #include <QtWidgets/QWidget> +#include <QtWidgets/QGridLayout> +#include <QtCore/QTimer> +#include <QtGui/QScreen> +#include <QtGui/QWindow> +#include <QtGui/QGuiApplication> #include <functional> #include "uitypes.h" #include "copyablelabel.h" +#include "uicontext.h" + +// Responsive layout breakpoints (logical pixels) +namespace TriageBreakpoints { + constexpr int NARROW = 1000; + constexpr int MEDIUM = 1400; +} class NavigationLabel : public QLabel @@ -91,6 +103,23 @@ class PEHeaders : public Headers class HeaderWidget : public QWidget { + Q_OBJECT + + Headers m_headers; + QGridLayout* m_layout; + int m_currentColumns; + int m_pendingWidth; + QTimer* m_resizeTimer; + + void rebuildLayout(); + public: HeaderWidget(QWidget* parent, const Headers& headers); + void updateColumns(int width); + + protected: + virtual void resizeEvent(QResizeEvent* event) override; + + private slots: + void performDelayedResize(); }; diff --git a/examples/triage/strings.cpp b/examples/triage/strings.cpp index 269f747a..a5661139 100644 --- a/examples/triage/strings.cpp +++ b/examples/triage/strings.cpp @@ -3,6 +3,9 @@ #include <QtGui/QClipboard> #include <QtGui/QGuiApplication> #include <QtCore/QStringList> +#include <QtCore/QEvent> +#include <QtCore/QTimer> +#include <QtWidgets/QHeaderView> #include "strings.h" #include "view.h" #include "fontsettings.h" @@ -203,12 +206,39 @@ StringsTreeView::StringsTreeView(StringsWidget* parent, TriageView* view, Binary setFont(getMonospaceFont(this)); + // Set column resize modes - use Interactive to avoid O(n) recalculation on every update + header()->setSectionResizeMode(QHeaderView::Interactive); + header()->setSectionResizeMode(2, QHeaderView::Stretch); // String column stretches to fill + + updateColumnWidths(); + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &StringsTreeView::stringSelected); connect(this, &QTreeView::doubleClicked, this, &StringsTreeView::stringDoubleClicked); m_actionHandler.bindAction("Copy", UIAction([this]() { copySelection(); }, [this]() { return canCopySelection(); })); } + +void StringsTreeView::updateColumnWidths() +{ + // Size address and length columns based on their headers, not contents + header()->resizeSection(0, header()->sectionSizeHint(0) + 20); + header()->resizeSection(1, header()->sectionSizeHint(1) + 20); +} + + +bool StringsTreeView::event(QEvent* event) +{ + // Update column widths when font or style changes (e.g., UI scale change) + if (event->type() == QEvent::FontChange || event->type() == QEvent::StyleChange) + { + // Defer update until after Qt recalculates font metrics + QTimer::singleShot(0, this, &StringsTreeView::updateColumnWidths); + } + return QTreeView::event(event); +} + + void StringsTreeView::copySelection() { if (!model() || !selectionModel()) diff --git a/examples/triage/strings.h b/examples/triage/strings.h index 25de66cd..14762b34 100644 --- a/examples/triage/strings.h +++ b/examples/triage/strings.h @@ -42,6 +42,8 @@ class StringsTreeView : public QTreeView, public FilterTarget UIActionHandler m_actionHandler; GenericStringsModel* m_model; + void updateColumnWidths(); + public: StringsTreeView(StringsWidget* parent, TriageView* view, BinaryViewRef data); void copySelection(); @@ -56,6 +58,7 @@ class StringsTreeView : public QTreeView, public FilterTarget protected: virtual void keyPressEvent(QKeyEvent* event) override; + virtual bool event(QEvent* event) override; private Q_SLOTS: void stringSelected(const QModelIndex& cur, const QModelIndex& prev); diff --git a/examples/triage/view.cpp b/examples/triage/view.cpp index 8981a99b..1da79c1d 100644 --- a/examples/triage/view.cpp +++ b/examples/triage/view.cpp @@ -47,8 +47,8 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent { QGroupBox* headerGroup = new QGroupBox("Headers", container); QVBoxLayout* headerLayout = new QVBoxLayout(); - HeaderWidget* headerWidget = new HeaderWidget(headerGroup, *hdr); - headerLayout->addWidget(headerWidget); + m_headerWidget = new HeaderWidget(headerGroup, *hdr); + headerLayout->addWidget(m_headerWidget); headerGroup->setLayout(headerLayout); layout->addWidget(headerGroup); delete hdr; @@ -72,13 +72,13 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent if (m_data->IsExecutable()) { - QSplitter* importExportSplitter = new QSplitter(Qt::Horizontal); + m_importExportSplitter = new QSplitter(Qt::Horizontal); QGroupBox* importGroup = new QGroupBox("Imports", container); QVBoxLayout* importLayout = new QVBoxLayout(); importLayout->addWidget(new ImportsWidget(importGroup, this, m_data)); importGroup->setLayout(importLayout); - importExportSplitter->addWidget(importGroup); + m_importExportSplitter->addWidget(importGroup); QSplitter* exportEntrySplitter = new QSplitter(Qt::Vertical); @@ -94,8 +94,8 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent entryGroup->setLayout(entryLayout); exportEntrySplitter->addWidget(entryGroup); - importExportSplitter->addWidget(exportEntrySplitter); - layout->addWidget(importExportSplitter); + m_importExportSplitter->addWidget(exportEntrySplitter); + layout->addWidget(m_importExportSplitter); if (m_data->GetTypeName() != "PE") { @@ -290,6 +290,37 @@ void TriageView::focusInEvent(QFocusEvent*) } +void TriageView::resizeEvent(QResizeEvent* event) +{ + QScrollArea::resizeEvent(event); + updateImportExportLayout(); +} + + +void TriageView::updateImportExportLayout() +{ + if (!m_importExportSplitter) + return; + + int width = viewport()->width(); + Qt::Orientation currentOrientation = m_importExportSplitter->orientation(); + Qt::Orientation desiredOrientation; + + // Add hysteresis: use different thresholds for shrinking vs growing + if (currentOrientation == Qt::Horizontal) + { + desiredOrientation = (width < TriageBreakpoints::NARROW - 20) ? Qt::Vertical : Qt::Horizontal; + } + else + { + desiredOrientation = (width >= TriageBreakpoints::NARROW + 20) ? Qt::Horizontal : Qt::Vertical; + } + + if (currentOrientation != desiredOrientation) + m_importExportSplitter->setOrientation(desiredOrientation); +} + + TriageViewType::TriageViewType() : ViewType("Triage", "Triage Summary") {} diff --git a/examples/triage/view.h b/examples/triage/view.h index e691b951..ab4f1024 100644 --- a/examples/triage/view.h +++ b/examples/triage/view.h @@ -6,12 +6,16 @@ #include "byte.h" +class HeaderWidget; + class TriageView : public QScrollArea, public View { BinaryViewRef m_data; uint64_t m_currentOffset = 0; ByteView* m_byteView = nullptr; QPushButton* m_fullAnalysisButton = nullptr; + QSplitter* m_importExportSplitter = nullptr; + HeaderWidget* m_headerWidget = nullptr; public: TriageView(QWidget* parent, BinaryViewRef data); @@ -28,9 +32,11 @@ class TriageView : public QScrollArea, public View protected: virtual void focusInEvent(QFocusEvent* event) override; + virtual void resizeEvent(QResizeEvent* event) override; private: void goToAddress(); + void updateImportExportLayout(); private Q_SLOTS: void startFullAnalysis(); |
