From 4ad2f3ee7d3a23cb2be812966931a074074851ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 19:01:37 +0000 Subject: [PATCH] Add "go to file offset" navigation A second box in the layout dock takes a file offset (0x-prefixed hex or decimal) and selects the most specific layout node whose byte range contains it, then scrolls it into view. The search walks the node tree lazily and is bounded so it stays responsive even on very large trees. https://claude.ai/code/session_013kBiVXftgoEsyGVyrvfGok --- src/src/dock/LayoutDockWidget.cpp | 94 +++++++++++++++++++++++++++++++ src/src/dock/LayoutDockWidget.h | 4 ++ 2 files changed, 98 insertions(+) diff --git a/src/src/dock/LayoutDockWidget.cpp b/src/src/dock/LayoutDockWidget.cpp index 8d9ae11..791d99c 100755 --- a/src/src/dock/LayoutDockWidget.cpp +++ b/src/src/dock/LayoutDockWidget.cpp @@ -28,6 +28,10 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) searchEdit->setPlaceholderText(tr("Search layout and opened tables...")); searchEdit->setClearButtonEnabled(true); + gotoEdit = new QLineEdit(this); + gotoEdit->setPlaceholderText(tr("Go to file offset (e.g. 0x4000)")); + gotoEdit->setClearButtonEnabled(true); + treeView = new LayoutTreeView(this); treeView->setMinimumWidth(200); treeView->setSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding); @@ -43,6 +47,7 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(2); layout->addWidget(searchEdit); + layout->addWidget(gotoEdit); layout->addWidget(treeView); setWidget(container); @@ -50,6 +55,8 @@ LayoutDockWidget::LayoutDockWidget(QWidget *parent) : QDockWidget(parent) this, &LayoutDockWidget::onSearchTextChanged); connect(searchEdit, &QLineEdit::returnPressed, this, &LayoutDockWidget::goToNextMatch); + connect(gotoEdit, &QLineEdit::returnPressed, + this, &LayoutDockWidget::goToOffset); connect(treeView, &QTreeView::clicked, this, &LayoutDockWidget::clickedTreeNode); connect(treeView, &QTreeView::activated, @@ -72,6 +79,7 @@ void LayoutDockWidget::openFile(const QString &filePath) // Clear the current tree while the new file parses in the background so the // UI thread stays responsive on large binaries / dyld shared caches. searchEdit->clear(); + gotoEdit->clear(); treeView->setModel(nullptr); parsing_ = true; WS()->addLog("Start parsing " + filePath); @@ -162,6 +170,92 @@ void LayoutDockWidget::goToNextMatch() treeView->scrollTo(target, QAbstractItemView::PositionAtCenter); } +moex::ViewNode *LayoutDockWidget::findNodeContainingOffset(moex::ViewNode *node, uint64_t offset, int &budget) const +{ + if (node == nullptr || budget <= 0) + return nullptr; + --budget; + + node->Init(); + + moex::ViewNode *best = nullptr; + uint64_t best_size = 0; + const auto &bin = node->binary(); + if (bin && !bin->IsEmpty() && bin->size > 0 && + offset >= bin->start_value && offset < bin->start_value + bin->size) { + best = node; + best_size = bin->size; + } + + node->ForEachChild([&](moex::ViewNode *child) { + moex::ViewNode *hit = findNodeContainingOffset(child, offset, budget); + if (hit != nullptr) { + const auto &cbin = hit->binary(); + const uint64_t csize = cbin ? cbin->size : 0; + // Prefer the most specific (smallest) containing range. + if (best == nullptr || (csize > 0 && csize <= best_size)) { + best = hit; + best_size = csize; + } + } + }); + + return best; +} + +QModelIndex LayoutDockWidget::findSourceIndexForNode(const QModelIndex &parent, moex::ViewNode *node) const +{ + QStandardItemModel *model = controller->model(); + const int rows = model->rowCount(parent); + for (int i = 0; i < rows; ++i) { + const QModelIndex idx = model->index(i, 0, parent); + QStandardItem *item = model->itemFromIndex(idx); + if (item != nullptr && + static_cast(item->data().value()) == node) { + return idx; + } + const QModelIndex child = findSourceIndexForNode(idx, node); + if (child.isValid()) + return child; + } + return QModelIndex(); +} + +void LayoutDockWidget::goToOffset() +{ + if (!controller) + return; + + const QString text = gotoEdit->text().trimmed(); + if (text.isEmpty()) + return; + + bool ok = false; + // base 0 auto-detects the 0x prefix; fall back to hex for bare digits. + qulonglong offset = text.toULongLong(&ok, 0); + if (!ok) + offset = text.toULongLong(&ok, 16); + if (!ok) + return; + + int budget = 200000; // guard against pathological trees (e.g. dyld cache) + moex::ViewNode *target = findNodeContainingOffset(controller->rootNode(), + static_cast(offset), budget); + if (target == nullptr) + return; + + const QModelIndex source = findSourceIndexForNode(QModelIndex(), target); + if (!source.isValid()) + return; + + const QModelIndex proxyIdx = proxyModel->mapFromSource(source); + if (!proxyIdx.isValid()) + return; + + treeView->setCurrentIndex(proxyIdx); + treeView->scrollTo(proxyIdx, QAbstractItemView::PositionAtCenter); +} + void LayoutDockWidget::onSearchTextChanged(const QString &text) { if(!proxyModel) diff --git a/src/src/dock/LayoutDockWidget.h b/src/src/dock/LayoutDockWidget.h index c686480..93d29e3 100755 --- a/src/src/dock/LayoutDockWidget.h +++ b/src/src/dock/LayoutDockWidget.h @@ -23,6 +23,7 @@ class LayoutDockWidget : public QDockWidget LayoutTreeView *treeView; LayoutController *controller; QLineEdit *searchEdit; + QLineEdit *gotoEdit; LayoutFilterProxyModel *proxyModel; bool parsing_ = false; bool nodeBuilding_ = false; @@ -35,6 +36,9 @@ class LayoutDockWidget : public QDockWidget void buildAndShowNode(moex::ViewNode *node); void collectMatches(const QModelIndex &proxyParent, QModelIndexList &out) const; void goToNextMatch(); + void goToOffset(); + moex::ViewNode *findNodeContainingOffset(moex::ViewNode *node, uint64_t offset, int &budget) const; + QModelIndex findSourceIndexForNode(const QModelIndex &parent, moex::ViewNode *node) const; public: explicit LayoutDockWidget(QWidget *parent = 0);