Skip to content

Commit dbdf338

Browse files
committed
triage view responsive layout
1 parent 1cabd2d commit dbdf338

File tree

6 files changed

+226
-19
lines changed

6 files changed

+226
-19
lines changed

examples/triage/headers.cpp

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -361,16 +361,111 @@ QString PEHeaders::GetNameOfEnumerationMember(BinaryViewRef data, const std::str
361361
}
362362

363363

364-
HeaderWidget::HeaderWidget(QWidget* parent, const Headers& header) : QWidget(parent)
364+
HeaderWidget::HeaderWidget(QWidget* parent, const Headers& header) : QWidget(parent), m_headers(header)
365365
{
366-
QGridLayout* layout = new QGridLayout();
367-
layout->setContentsMargins(0, 0, 0, 0);
368-
layout->setVerticalSpacing(1);
366+
m_layout = new QGridLayout();
367+
m_layout->setContentsMargins(0, 0, 0, 0);
368+
m_layout->setVerticalSpacing(1);
369+
m_layout->setHorizontalSpacing(2);
370+
setLayout(m_layout);
371+
m_currentColumns = (int)header.GetColumns();
372+
m_pendingWidth = -1;
373+
374+
// Create timer for debouncing resize events
375+
m_resizeTimer = new QTimer(this);
376+
m_resizeTimer->setSingleShot(true);
377+
m_resizeTimer->setInterval(50); // 50ms delay after resize stops
378+
connect(m_resizeTimer, &QTimer::timeout, this, &HeaderWidget::performDelayedResize);
379+
380+
rebuildLayout();
381+
}
382+
383+
384+
void HeaderWidget::resizeEvent(QResizeEvent* event)
385+
{
386+
QWidget::resizeEvent(event);
387+
updateColumns(this->width());
388+
}
389+
390+
391+
void HeaderWidget::updateColumns(int width)
392+
{
393+
m_pendingWidth = width;
394+
m_resizeTimer->start();
395+
}
396+
397+
398+
void HeaderWidget::performDelayedResize()
399+
{
400+
if (m_pendingWidth < 0)
401+
return;
402+
403+
int width = m_pendingWidth;
404+
m_pendingWidth = -1;
405+
406+
int desiredColumns;
407+
408+
// Add hysteresis to prevent thrashing when width oscillates near breakpoints
409+
if (m_currentColumns == 1)
410+
{
411+
// Growing from 1 column: need to exceed threshold to switch
412+
if (width >= TriageBreakpoints::NARROW + 40)
413+
desiredColumns = (width >= TriageBreakpoints::MEDIUM + 40) ? (int)m_headers.GetColumns() : 2;
414+
else
415+
desiredColumns = 1;
416+
}
417+
else if (m_currentColumns == 2)
418+
{
419+
// From 2 columns: wider hysteresis band
420+
if (width < TriageBreakpoints::NARROW - 40)
421+
desiredColumns = 1;
422+
else if (width >= TriageBreakpoints::MEDIUM + 40)
423+
desiredColumns = (int)m_headers.GetColumns();
424+
else
425+
desiredColumns = 2;
426+
}
427+
else
428+
{
429+
// Shrinking from 3 columns: need to fall below threshold to switch
430+
if (width < TriageBreakpoints::NARROW - 40)
431+
desiredColumns = 1;
432+
else if (width < TriageBreakpoints::MEDIUM - 40)
433+
desiredColumns = 2;
434+
else
435+
desiredColumns = (int)m_headers.GetColumns();
436+
}
437+
438+
if (desiredColumns != m_currentColumns)
439+
{
440+
m_currentColumns = desiredColumns;
441+
rebuildLayout();
442+
}
443+
}
444+
445+
446+
void HeaderWidget::rebuildLayout()
447+
{
448+
// Disable updates during rebuild to prevent flickering
449+
setUpdatesEnabled(false);
450+
451+
// Clear existing layout
452+
QLayoutItem* item;
453+
while ((item = m_layout->takeAt(0)) != nullptr)
454+
{
455+
if (item->widget())
456+
{
457+
item->widget()->hide(); // Hide before deletion to reduce flicker
458+
item->widget()->deleteLater(); // Use deleteLater() to safely delete during events
459+
}
460+
delete item;
461+
}
462+
463+
// Rebuild with current column count
369464
int row = 0;
370465
int col = 0;
371-
for (auto& field : header.GetFields())
466+
for (auto& field : m_headers.GetFields())
372467
{
373-
layout->addWidget(new QLabel(field.title + ": "), row, col * 3);
468+
m_layout->addWidget(new QLabel(field.title + ": "), row, col * 3);
374469

375470
// For text fields with multiple values, join them with newlines for copying
376471
QString copyText;
@@ -401,18 +496,31 @@ HeaderWidget::HeaderWidget(QWidget* parent, const Headers& header) : QWidget(par
401496
copyLabel->setCopyText(copyText);
402497
label = copyLabel;
403498
}
404-
layout->addWidget(label, row, col * 3 + 1);
499+
m_layout->addWidget(label, row, col * 3 + 1);
405500
row++;
406501
}
407-
if ((header.GetColumns() > 1) && (row >= (int)header.GetRowsPerColumn())
408-
&& ((col + 1) < (int)header.GetColumns()))
502+
if ((m_currentColumns > 1) && (row >= (int)m_headers.GetRowsPerColumn())
503+
&& ((col + 1) < m_currentColumns))
409504
{
410505
row = 0;
411506
col++;
412507
}
413508
}
414-
for (col = 1; col < (int)header.GetColumns(); col++)
415-
layout->setColumnMinimumWidth(col * 3 - 1, UIContext::getScaledWindowSize(20, 20).width());
416-
layout->setColumnStretch((int)header.GetColumns() * 3 - 1, 1);
417-
setLayout(layout);
509+
510+
// Clear all column stretches and minimum widths first
511+
for (col = 0; col < 9; col++) // Max 3 columns * 3 grid columns each
512+
{
513+
m_layout->setColumnStretch(col, 0);
514+
m_layout->setColumnMinimumWidth(col, 0);
515+
}
516+
517+
// Set spacing columns to minimum width
518+
for (col = 1; col < m_currentColumns; col++)
519+
m_layout->setColumnMinimumWidth(col * 3 - 1, UIContext::getScaledWindowSize(20, 20).width());
520+
521+
// Set last column to stretch
522+
m_layout->setColumnStretch(m_currentColumns * 3 - 1, 1);
523+
524+
// Re-enable updates and force a single repaint
525+
setUpdatesEnabled(true);
418526
}

examples/triage/headers.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@
22

33
#include <QtWidgets/QLabel>
44
#include <QtWidgets/QWidget>
5+
#include <QtWidgets/QGridLayout>
6+
#include <QtCore/QTimer>
7+
#include <QtGui/QScreen>
8+
#include <QtGui/QWindow>
9+
#include <QtGui/QGuiApplication>
510
#include <functional>
611
#include "uitypes.h"
712
#include "copyablelabel.h"
13+
#include "uicontext.h"
14+
15+
// Responsive layout breakpoints (logical pixels)
16+
namespace TriageBreakpoints {
17+
constexpr int NARROW = 1000;
18+
constexpr int MEDIUM = 1400;
19+
}
820

921

1022
class NavigationLabel : public QLabel
@@ -91,6 +103,23 @@ class PEHeaders : public Headers
91103

92104
class HeaderWidget : public QWidget
93105
{
106+
Q_OBJECT
107+
108+
Headers m_headers;
109+
QGridLayout* m_layout;
110+
int m_currentColumns;
111+
int m_pendingWidth;
112+
QTimer* m_resizeTimer;
113+
114+
void rebuildLayout();
115+
94116
public:
95117
HeaderWidget(QWidget* parent, const Headers& headers);
118+
void updateColumns(int width);
119+
120+
protected:
121+
virtual void resizeEvent(QResizeEvent* event) override;
122+
123+
private slots:
124+
void performDelayedResize();
96125
};

examples/triage/strings.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
#include <QtGui/QClipboard>
44
#include <QtGui/QGuiApplication>
55
#include <QtCore/QStringList>
6+
#include <QtCore/QEvent>
7+
#include <QtCore/QTimer>
8+
#include <QtWidgets/QHeaderView>
69
#include "strings.h"
710
#include "view.h"
811
#include "fontsettings.h"
@@ -203,12 +206,39 @@ StringsTreeView::StringsTreeView(StringsWidget* parent, TriageView* view, Binary
203206

204207
setFont(getMonospaceFont(this));
205208

209+
// Set column resize modes - use Interactive to avoid O(n) recalculation on every update
210+
header()->setSectionResizeMode(QHeaderView::Interactive);
211+
header()->setSectionResizeMode(2, QHeaderView::Stretch); // String column stretches to fill
212+
213+
updateColumnWidths();
214+
206215
connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &StringsTreeView::stringSelected);
207216
connect(this, &QTreeView::doubleClicked, this, &StringsTreeView::stringDoubleClicked);
208217

209218
m_actionHandler.bindAction("Copy", UIAction([this]() { copySelection(); }, [this]() { return canCopySelection(); }));
210219
}
211220

221+
222+
void StringsTreeView::updateColumnWidths()
223+
{
224+
// Size address and length columns based on their headers, not contents
225+
header()->resizeSection(0, header()->sectionSizeHint(0) + 20);
226+
header()->resizeSection(1, header()->sectionSizeHint(1) + 20);
227+
}
228+
229+
230+
bool StringsTreeView::event(QEvent* event)
231+
{
232+
// Update column widths when font or style changes (e.g., UI scale change)
233+
if (event->type() == QEvent::FontChange || event->type() == QEvent::StyleChange)
234+
{
235+
// Defer update until after Qt recalculates font metrics
236+
QTimer::singleShot(0, this, &StringsTreeView::updateColumnWidths);
237+
}
238+
return QTreeView::event(event);
239+
}
240+
241+
212242
void StringsTreeView::copySelection()
213243
{
214244
if (!model() || !selectionModel())

examples/triage/strings.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class StringsTreeView : public QTreeView, public FilterTarget
4242
UIActionHandler m_actionHandler;
4343
GenericStringsModel* m_model;
4444

45+
void updateColumnWidths();
46+
4547
public:
4648
StringsTreeView(StringsWidget* parent, TriageView* view, BinaryViewRef data);
4749
void copySelection();
@@ -56,6 +58,7 @@ class StringsTreeView : public QTreeView, public FilterTarget
5658

5759
protected:
5860
virtual void keyPressEvent(QKeyEvent* event) override;
61+
virtual bool event(QEvent* event) override;
5962

6063
private Q_SLOTS:
6164
void stringSelected(const QModelIndex& cur, const QModelIndex& prev);

examples/triage/view.cpp

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent
4747
{
4848
QGroupBox* headerGroup = new QGroupBox("Headers", container);
4949
QVBoxLayout* headerLayout = new QVBoxLayout();
50-
HeaderWidget* headerWidget = new HeaderWidget(headerGroup, *hdr);
51-
headerLayout->addWidget(headerWidget);
50+
m_headerWidget = new HeaderWidget(headerGroup, *hdr);
51+
headerLayout->addWidget(m_headerWidget);
5252
headerGroup->setLayout(headerLayout);
5353
layout->addWidget(headerGroup);
5454
delete hdr;
@@ -72,13 +72,13 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent
7272

7373
if (m_data->IsExecutable())
7474
{
75-
QSplitter* importExportSplitter = new QSplitter(Qt::Horizontal);
75+
m_importExportSplitter = new QSplitter(Qt::Horizontal);
7676

7777
QGroupBox* importGroup = new QGroupBox("Imports", container);
7878
QVBoxLayout* importLayout = new QVBoxLayout();
7979
importLayout->addWidget(new ImportsWidget(importGroup, this, m_data));
8080
importGroup->setLayout(importLayout);
81-
importExportSplitter->addWidget(importGroup);
81+
m_importExportSplitter->addWidget(importGroup);
8282

8383
QSplitter* exportEntrySplitter = new QSplitter(Qt::Vertical);
8484

@@ -94,8 +94,8 @@ TriageView::TriageView(QWidget* parent, BinaryViewRef data) : QScrollArea(parent
9494
entryGroup->setLayout(entryLayout);
9595
exportEntrySplitter->addWidget(entryGroup);
9696

97-
importExportSplitter->addWidget(exportEntrySplitter);
98-
layout->addWidget(importExportSplitter);
97+
m_importExportSplitter->addWidget(exportEntrySplitter);
98+
layout->addWidget(m_importExportSplitter);
9999

100100
if (m_data->GetTypeName() != "PE")
101101
{
@@ -290,6 +290,37 @@ void TriageView::focusInEvent(QFocusEvent*)
290290
}
291291

292292

293+
void TriageView::resizeEvent(QResizeEvent* event)
294+
{
295+
QScrollArea::resizeEvent(event);
296+
updateImportExportLayout();
297+
}
298+
299+
300+
void TriageView::updateImportExportLayout()
301+
{
302+
if (!m_importExportSplitter)
303+
return;
304+
305+
int width = viewport()->width();
306+
Qt::Orientation currentOrientation = m_importExportSplitter->orientation();
307+
Qt::Orientation desiredOrientation;
308+
309+
// Add hysteresis: use different thresholds for shrinking vs growing
310+
if (currentOrientation == Qt::Horizontal)
311+
{
312+
desiredOrientation = (width < TriageBreakpoints::NARROW - 20) ? Qt::Vertical : Qt::Horizontal;
313+
}
314+
else
315+
{
316+
desiredOrientation = (width >= TriageBreakpoints::NARROW + 20) ? Qt::Horizontal : Qt::Vertical;
317+
}
318+
319+
if (currentOrientation != desiredOrientation)
320+
m_importExportSplitter->setOrientation(desiredOrientation);
321+
}
322+
323+
293324
TriageViewType::TriageViewType() : ViewType("Triage", "Triage Summary") {}
294325

295326

examples/triage/view.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
#include "byte.h"
77

88

9+
class HeaderWidget;
10+
911
class TriageView : public QScrollArea, public View
1012
{
1113
BinaryViewRef m_data;
1214
uint64_t m_currentOffset = 0;
1315
ByteView* m_byteView = nullptr;
1416
QPushButton* m_fullAnalysisButton = nullptr;
17+
QSplitter* m_importExportSplitter = nullptr;
18+
HeaderWidget* m_headerWidget = nullptr;
1519

1620
public:
1721
TriageView(QWidget* parent, BinaryViewRef data);
@@ -28,9 +32,11 @@ class TriageView : public QScrollArea, public View
2832

2933
protected:
3034
virtual void focusInEvent(QFocusEvent* event) override;
35+
virtual void resizeEvent(QResizeEvent* event) override;
3136

3237
private:
3338
void goToAddress();
39+
void updateImportExportLayout();
3440

3541
private Q_SLOTS:
3642
void startFullAnalysis();

0 commit comments

Comments
 (0)