summaryrefslogtreecommitdiff
path: root/examples/triage
diff options
context:
space:
mode:
authorJordan Wiens <github@psifertex.com>2025-12-19 14:18:19 -0500
committerJordan Wiens <github@psifertex.com>2025-12-19 15:20:22 -0500
commitdbdf3381e2dd32527cffaca6081fc5a9913f1cc7 (patch)
tree4f10dc3101700a63f7e83a087e69b5b1659fb61a /examples/triage
parent1cabd2def9da8ee106be26308b96a2a91cd92a2f (diff)
triage view responsive layout
Diffstat (limited to 'examples/triage')
-rw-r--r--examples/triage/headers.cpp134
-rw-r--r--examples/triage/headers.h29
-rw-r--r--examples/triage/strings.cpp30
-rw-r--r--examples/triage/strings.h3
-rw-r--r--examples/triage/view.cpp43
-rw-r--r--examples/triage/view.h6
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();