diff --git a/CMakeLists.txt b/CMakeLists.txt index 726c54e..92336a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ if(NOT QT_FOUND) find_package(Qt5 REQUIRED COMPONENTS ${_QT_COMPONENTS}) set(QT_VERSION_MAJOR 5) endif() +# The NAMES variant only detects the version; load the actual component targets +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${_QT_COMPONENTS}) set(QT Qt${QT_VERSION_MAJOR}) message(STATUS "Using ${QT}: ${${QT}_DIR}") @@ -44,7 +46,7 @@ add_executable(ReclassX src/resources.qrc src/core.h src/workspace_model.h - src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h src/providers/snapshot_provider.h + src/providers/buffer_provider.h src/providers/null_provider.h src/providers/provider.h src/providers/snapshot_provider.h src/providerregistry.cpp src/providerregistry.h src/pluginmanager.cpp @@ -154,15 +156,6 @@ if(BUILD_TESTING) target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_command_row COMMAND test_command_row) - add_executable(test_provider_getSymbol tests/test_provider_getSymbol.cpp) - target_include_directories(test_provider_getSymbol PRIVATE src) - target_link_libraries(test_provider_getSymbol PRIVATE ${QT}::Core ${QT}::Test) - if(WIN32) - target_compile_definitions(test_provider_getSymbol PRIVATE _WIN32) - target_link_libraries(test_provider_getSymbol PRIVATE psapi) - endif() - add_test(NAME test_provider_getSymbol COMMAND test_provider_getSymbol) - add_executable(test_controller tests/test_controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp diff --git a/src/compose.cpp b/src/compose.cpp index 6dca593..dbc3c25 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -172,16 +172,19 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, void composeNode(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false, - uint64_t scopeId = 0, int arrayElementIdx = -1); + uint64_t scopeId = 0, int arrayElementIdx = -1, + uint64_t arrayContainerAddr = 0); void composeParent(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false, - uint64_t scopeId = 0, int arrayElementIdx = -1); + uint64_t scopeId = 0, int arrayElementIdx = -1, + uint64_t arrayContainerAddr = 0); void composeParent(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, uint64_t base, uint64_t rootId, bool isArrayChild, - uint64_t scopeId, int arrayElementIdx) { + uint64_t scopeId, int arrayElementIdx, + uint64_t arrayContainerAddr) { const Node& node = tree.nodes[nodeIdx]; uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId); @@ -214,7 +217,9 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; lm.arrayElementIdx = arrayElementIdx; - state.emitLine(fmt::indent(depth) + QStringLiteral("[%1]").arg(arrayElementIdx), lm); + uint64_t relOff = absAddr - arrayContainerAddr; + QString relOffHex = QString::number(relOff, 16).toUpper(); + state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), lm); } // Detect root header: first root-level struct — suppressed from display @@ -252,7 +257,9 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.elementKind = node.elementKind; lm.arrayViewIdx = node.viewIndex; lm.arrayCount = node.arrayLen; - headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW); + QString elemStructName = (node.elementKind == NodeKind::Struct) + ? resolvePointerTarget(tree, node.refId) : QString(); + headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName); } else { // All structs (root and nested) use the same header format headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW); @@ -268,6 +275,61 @@ void composeParent(ComposeState& state, const NodeTree& tree, int childDepth = depth + 1; + // Primitive arrays with no child nodes: synthesize element lines dynamically + if (node.kind == NodeKind::Array && children.isEmpty() + && node.elementKind != NodeKind::Struct && node.elementKind != NodeKind::Array) { + int elemSize = sizeForKind(node.elementKind); + int eTW = state.effectiveTypeW(node.id); + int eNW = state.effectiveNameW(node.id); + for (int i = 0; i < node.arrayLen; i++) { + uint64_t elemAddr = absAddr + i * elemSize; + + // Type override: "float[0]", "uint32_t[1]", etc. + QString elemTypeStr = fmt::typeNameRaw(node.elementKind) + + QStringLiteral("[%1]").arg(i); + + Node elem; + elem.kind = node.elementKind; + elem.name = QString(); // no name for array elements + elem.offset = node.offset + i * elemSize; + elem.parentId = node.id; + elem.id = 0; + + LineMeta lm; + lm.nodeIdx = nodeIdx; + lm.nodeId = node.id; + lm.depth = childDepth; + lm.lineKind = LineKind::Field; + lm.nodeKind = node.elementKind; + lm.isArrayElement = true; + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits); + lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth); + lm.foldLevel = computeFoldLevel(childDepth, false); + lm.effectiveTypeW = eTW; + lm.effectiveNameW = eNW; + + state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0, + {}, eTW, eNW, elemTypeStr), lm); + } + } + + // Struct arrays with refId but no child nodes: synthesize by expanding the + // referenced struct for each element (like repeated pointer deref) + if (node.kind == NodeKind::Array && children.isEmpty() + && node.elementKind == NodeKind::Struct && node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + int elemSize = tree.structSpan(node.refId, &state.childMap); + if (elemSize <= 0) elemSize = 1; + for (int i = 0; i < node.arrayLen; i++) { + uint64_t elemBase = absAddr + (uint64_t)i * elemSize; + // Use base offset that maps refStruct's children to the right provider address + composeParent(state, tree, prov, refIdx, childDepth, elemBase, node.refId, + /*isArrayChild=*/true, node.id, i, absAddr); + } + } + } + // For arrays, render children as condensed (no header/footer for struct elements) bool childrenAreArrayElements = (node.kind == NodeKind::Array); int elementIdx = 0; @@ -276,7 +338,8 @@ void composeParent(ComposeState& state, const NodeTree& tree, // For array elements, also pass the element index for [N] separator composeNode(state, tree, prov, childIdx, childDepth, base, rootId, childrenAreArrayElements, node.id, - childrenAreArrayElements ? elementIdx++ : -1); + childrenAreArrayElements ? elementIdx++ : -1, + childrenAreArrayElements ? absAddr : 0); } } @@ -302,7 +365,8 @@ void composeParent(ComposeState& state, const NodeTree& tree, void composeNode(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, uint64_t base, uint64_t rootId, bool isArrayChild, - uint64_t scopeId, int arrayElementIdx) { + uint64_t scopeId, int arrayElementIdx, + uint64_t arrayContainerAddr) { const Node& node = tree.nodes[nodeIdx]; uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId); @@ -392,7 +456,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, } if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { - composeParent(state, tree, prov, nodeIdx, depth, base, rootId, isArrayChild, scopeId, arrayElementIdx); + composeParent(state, tree, prov, nodeIdx, depth, base, rootId, isArrayChild, scopeId, arrayElementIdx, arrayContainerAddr); } else { composeLeaf(state, tree, prov, nodeIdx, depth, absAddr, scopeId); } @@ -427,8 +491,11 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR // Helper: compute the display type string for a node (for width calculation) auto nodeTypeName = [&](const Node& n) -> QString { - if (n.kind == NodeKind::Array) - return fmt::arrayTypeName(n.elementKind, n.arrayLen); + if (n.kind == NodeKind::Array) { + QString sn = (n.elementKind == NodeKind::Struct) + ? resolvePointerTarget(tree, n.refId) : QString(); + return fmt::arrayTypeName(n.elementKind, n.arrayLen, sn); + } if (n.kind == NodeKind::Struct) return fmt::structTypeName(n); if (n.kind == NodeKind::Pointer32 || n.kind == NodeKind::Pointer64) @@ -473,6 +540,19 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR } } + // Primitive arrays with no tree children: account for synthesized element types + // e.g. "uint32_t[0]", "uint32_t[99]" — longest index determines width + if (container.kind == NodeKind::Array + && state.childMap.value(container.id).isEmpty() + && container.elementKind != NodeKind::Struct + && container.elementKind != NodeKind::Array + && container.arrayLen > 0) { + int maxIdx = container.arrayLen - 1; + QString longestElemType = fmt::typeNameRaw(container.elementKind) + + QStringLiteral("[%1]").arg(maxIdx); + scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size()); + } + state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW); state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW); } diff --git a/src/controller.h b/src/controller.h index 11f7f71..a9ccc1d 100644 --- a/src/controller.h +++ b/src/controller.h @@ -61,11 +61,10 @@ private: // ── Saved source entry ── struct SavedSourceEntry { - QString kind; // "File" or "Process" + QString kind; // "File" or provider identifier (e.g. "processmemory") QString displayName; // filename or process name QString filePath; // for File sources - uint32_t pid = 0; // for Process sources - QString processName; // for Process sources + QString providerTarget; // for plugin providers (e.g. "pid:name") uint64_t baseAddress = 0; }; @@ -112,7 +111,7 @@ public: // MCP bridge accessors void setSuppressRefresh(bool v) { m_suppressRefresh = v; } - void attachToProcess(uint32_t pid, const QString& processName); + void attachViaPlugin(const QString& providerIdentifier, const QString& target); const QVector& savedSources() const { return m_savedSources; } int activeSourceIndex() const { return m_activeSourceIdx; } void switchSource(int idx) { switchToSavedSource(idx); } @@ -151,6 +150,8 @@ private: void switchToSavedSource(int idx); void pushSavedSourcesToEditors(); void showTypeSelectorPopup(RcxEditor* editor); + void showTypePickerPopup(RcxEditor* editor, EditTarget target, int nodeIdx, QPoint globalPos); + void applyTypePickerResult(EditTarget target, int nodeIdx, uint64_t selectedId, const QString& displayName); // ── Auto-refresh methods ── void setupAutoRefresh(); diff --git a/src/core.h b/src/core.h index a73b107..d949bb7 100644 --- a/src/core.h +++ b/src/core.h @@ -650,7 +650,7 @@ inline ColumnSpan commandRowRootNameSpan(const QString& lineText) { inline ColumnSpan commandRowChevronSpan(const QString& lineText) { if (lineText.size() < 3) return {}; if (lineText[0] == '[' && lineText[1] == QChar(0x25B8) && lineText[2] == ']') - return {0, 3, true}; + return {0, qMin(4, (int)lineText.size()), true}; // include trailing space for easier clicking return {}; } diff --git a/src/editor.cpp b/src/editor.cpp index 7fd54c0..44bd2ba 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -466,6 +466,10 @@ void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) { void RcxEditor::applyHexDimming(const QVector& meta) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM); for (int i = 0; i < meta.size(); i++) { + // Dim fold arrows (▸/▾) on fold head lines + if (meta[i].foldHead && meta[i].lineKind != LineKind::CommandRow) + fillIndicatorCols(IND_HEX_DIM, i, 0, kFoldCol); + if (isHexPreview(meta[i].nodeKind)) { long pos, len; lineRangeNoEol(m_sci, i, pos, len); if (len > 0) @@ -979,7 +983,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const { if (h.line >= 0 && h.line < m_meta.size()) { h.nodeId = m_meta[h.line].nodeId; - h.inFoldCol = (h.col >= 0 && h.col < kFoldCol && m_meta[h.line].foldHead); + h.inFoldCol = (h.col >= 0 && h.col < kFoldCol + 1 && m_meta[h.line].foldHead); } return h; } @@ -1044,11 +1048,12 @@ static bool hitTestTarget(QsciScintilla* sci, } // Array headers: check element type and count sub-spans first + // Count click area includes brackets [N] so clicking [ or ] edits the count if (lm.isArrayHeader) { + ColumnSpan elemCountClick = arrayElemCountClickSpanFor(lm, lineText); ColumnSpan elemType = arrayElemTypeSpanFor(lm, lineText); - ColumnSpan elemCount = arrayElemCountSpanFor(lm, lineText); - if (inSpan(elemCount)) { outTarget = EditTarget::ArrayElementCount; outLine = line; return true; } - if (inSpan(elemType)) { outTarget = EditTarget::ArrayElementType; outLine = line; return true; } + if (inSpan(elemCountClick)) { outTarget = EditTarget::ArrayElementCount; outLine = line; return true; } + if (inSpan(elemType)) { outTarget = EditTarget::ArrayElementType; outLine = line; return true; } } // Fallback spans for header lines @@ -1065,6 +1070,26 @@ static bool hitTestTarget(QsciScintilla* sci, else if (inSpan(vs)) outTarget = EditTarget::Value; else return false; + // Array headers: redirect generic Type hit to ArrayElementType (uses popup, not inline edit) + if (lm.isArrayHeader && outTarget == EditTarget::Type) { + outTarget = EditTarget::ArrayElementType; + outLine = line; + return true; + } + // Array element lines: type/name click opens element type picker on the parent array header + if (lm.isArrayElement && (outTarget == EditTarget::Type || outTarget == EditTarget::Name)) { + outTarget = EditTarget::ArrayElementType; + // Find the array header line (previous line with isArrayHeader and same nodeIdx) + for (int l = line - 1; l >= 0; l--) { + if (l >= meta.size()) continue; + const LineMeta& hdr = meta[l]; + if (hdr.isArrayHeader && hdr.nodeIdx == lm.nodeIdx) { + outLine = l; + return true; + } + } + return false; + } // Padding nodes: hex bytes are display-only, not editable if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding) return false; @@ -1446,6 +1471,24 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { bool RcxEditor::beginInlineEdit(EditTarget target, int line) { if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit + + // Array element type and pointer target: handled by TypeSelectorPopup, not inline edit + if (target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) { + if (line < 0) { + int col; + m_sci->getCursorPosition(&line, &col); + } + auto* lm = metaForLine(line); + if (!lm) return false; + long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line); + int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, (unsigned long)line); + int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, (unsigned long)0, lineStart); + int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, (unsigned long)0, lineStart); + QPoint pos = m_sci->viewport()->mapToGlobal(QPoint(x, y + lineH)); + emit typePickerRequested(target, lm->nodeIdx, pos); + return true; + } + if (m_editState.active) return false; m_hoveredNodeId = 0; m_hoveredLine = -1; @@ -1759,7 +1802,6 @@ void RcxEditor::showSourcePicker() { menuFont.setPointSize(menuFont.pointSize() + zoom); menu.setFont(menuFont); menu.addAction("file"); - menu.addAction("process"); // Add all registered providers from global registry const auto& providers = ProviderRegistry::instance().providers(); @@ -2015,6 +2057,12 @@ void RcxEditor::applyHoverCursor() { } } + // Apply hover span on fold arrows (▸/▾) — same visual feedback as editable tokens + if (h.inFoldCol && h.line >= 0 && h.line < m_meta.size()) { + fillIndicatorCols(IND_HOVER_SPAN, h.line, 0, kFoldCol); + m_hoverSpanLines.append(h.line); + } + // Determine cursor shape based on interaction type Qt::CursorShape desired = Qt::ArrowCursor; diff --git a/src/editor.h b/src/editor.h index 5312308..b04c624 100644 --- a/src/editor.h +++ b/src/editor.h @@ -65,6 +65,7 @@ signals: EditTarget target, const QString& text); void inlineEditCancelled(); void typeSelectorRequested(); + void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); protected: bool eventFilter(QObject* obj, QEvent* event) override; diff --git a/src/examples/demo.rcx b/src/examples/demo.rcx index 024ad5e..ea2f037 100644 --- a/src/examples/demo.rcx +++ b/src/examples/demo.rcx @@ -317,27 +317,27 @@ "strLen": 64 }, { - "arrayLen": 1, + "arrayLen": 4, "collapsed": false, - "elementKind": "UInt8", + "elementKind": "Float", "id": "27", - "kind": "Hex64", - "name": "field_70", + "kind": "Array", + "name": "scores", "offset": 112, "parentId": "1", "refId": "0", "strLen": 64 }, { - "arrayLen": 1, + "arrayLen": 2, "collapsed": false, - "elementKind": "UInt8", + "elementKind": "Struct", "id": "28", - "kind": "Hex64", - "name": "field_78", - "offset": 120, + "kind": "Array", + "name": "materials", + "offset": 128, "parentId": "1", - "refId": "0", + "refId": "20", "strLen": 64 } ] diff --git a/src/format.cpp b/src/format.cpp index 69045da..18fc651 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -41,13 +41,15 @@ QString typeName(NodeKind kind, int colType) { return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType); } -// Array type string: "uint32_t[16]" or "char[64]" -QString arrayTypeName(NodeKind elemKind, int count) { - auto* m = kindMeta(elemKind); - QString elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"); - // char[] for UInt8, wchar_t[] for UInt16 - if (elemKind == NodeKind::UInt8) elem = QStringLiteral("char"); - else if (elemKind == NodeKind::UInt16) elem = QStringLiteral("wchar_t"); +// Array type string: "uint32_t[16]" or "Material[2]" +QString arrayTypeName(NodeKind elemKind, int count, const QString& structName) { + QString elem; + if (elemKind == NodeKind::Struct && !structName.isEmpty()) + elem = structName; + else { + auto* m = kindMeta(elemKind); + elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"); + } return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]"); } @@ -143,9 +145,9 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { // ── Array header ── // Columnar format: { (or no brace when collapsed) -QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) { +QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName) { QString ind = indent(depth); - QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType); + QString type = fit(arrayTypeName(node.elementKind, node.arrayLen, elemStructName), colType); QString suffix = collapsed ? QString() : QStringLiteral("{"); return ind + type + SEP + node.name + SEP + suffix; } diff --git a/src/main.cpp b/src/main.cpp index 44e47fe..aab1810 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -597,8 +597,12 @@ static void buildBallDemo(NodeTree& tree) { // Pointer to Material in Ball struct { Node n; n.kind = NodeKind::Pointer64; n.name = "material"; n.parentId = ballId; n.offset = 104; n.refId = matId; n.collapsed = true; tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 112; tree.addNode(n); } - { Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 120; tree.addNode(n); } + + // float[4] scores at offset 112 + { Node n; n.kind = NodeKind::Array; n.name = "scores"; n.parentId = ballId; n.offset = 112; n.elementKind = NodeKind::Float; n.arrayLen = 4; tree.addNode(n); } + + // Material[2] materials at offset 128 (112 + 16 for float[4]) + { Node n; n.kind = NodeKind::Array; n.name = "materials"; n.parentId = ballId; n.offset = 128; n.elementKind = NodeKind::Struct; n.arrayLen = 2; n.refId = matId; tree.addNode(n); } } void MainWindow::newFile() { diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index 75379b2..4b91a56 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -796,7 +796,8 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) { uint32_t pid = (uint32_t)args.value("pid").toInteger(); QString name = args.value("processName").toString(); if (name.isEmpty()) name = QString("PID %1").arg(pid); - ctrl->attachToProcess(pid, name); + QString target = QString("%1:%2").arg(pid).arg(name); + ctrl->attachViaPlugin(QStringLiteral("processmemory"), target); return makeTextResult("Attached to process " + name + " (PID " + QString::number(pid) + ")"); } diff --git a/src/providers/process_provider.h b/src/providers/process_provider.h deleted file mode 100644 index 56aefd8..0000000 --- a/src/providers/process_provider.h +++ /dev/null @@ -1,109 +0,0 @@ -#pragma once -#include "provider.h" - -#ifdef _WIN32 -#include -#include - -namespace rcx { - -class ProcessProvider : public Provider { - HANDLE m_handle = nullptr; - uint64_t m_base = 0; - int m_size = 0; - QString m_name; - - struct ModuleInfo { - QString name; - uint64_t base; - uint64_t size; - }; - QVector m_modules; - -public: - ProcessProvider(HANDLE proc, uint64_t base, int regionSize, const QString& name) - : m_handle(proc), m_base(base), m_size(regionSize), m_name(name) - { - cacheModules(); - } - - ~ProcessProvider() override { - if (m_handle) CloseHandle(m_handle); - } - - ProcessProvider(const ProcessProvider&) = delete; - ProcessProvider& operator=(const ProcessProvider&) = delete; - - int size() const override { return m_size; } - bool isReadable(uint64_t, int len) const override { return len >= 0; } - - bool read(uint64_t addr, void* buf, int len) const override { - if (!m_handle || len <= 0) return false; - SIZE_T got = 0; - ReadProcessMemory(m_handle, - (LPCVOID)(m_base + addr), buf, len, &got); - if ((int)got < len) - memset((char*)buf + got, 0, len - got); - return got > 0; - } - - bool isWritable() const override { return true; } - - bool write(uint64_t addr, const void* buf, int len) override { - SIZE_T got = 0; - BOOL ok = WriteProcessMemory(m_handle, - (LPVOID)(m_base + addr), buf, len, &got); - return ok && (int)got == len; - } - - QString name() const override { return m_name; } - QString kind() const override { return QStringLiteral("Process"); } - bool isLive() const override { return true; } - - // getSymbol takes an absolute virtual address and resolves it to - // "module.dll+0xOFFSET" using the cached module list. - QString getSymbol(uint64_t absAddr) const override { - for (const auto& mod : m_modules) { - if (absAddr >= mod.base && absAddr < mod.base + mod.size) { - uint64_t offset = absAddr - mod.base; - return QStringLiteral("%1+0x%2") - .arg(mod.name) - .arg(offset, 0, 16, QChar('0')); - } - } - return {}; - } - - HANDLE handle() const { return m_handle; } - uint64_t baseAddress() const { return m_base; } - uint64_t base() const override { return m_base; } - void setBase(uint64_t b) override { m_base = b; } - void refreshModules() { m_modules.clear(); cacheModules(); } - -private: - void cacheModules() { - HMODULE mods[1024]; - DWORD needed = 0; - if (!EnumProcessModulesEx(m_handle, mods, sizeof(mods), - &needed, LIST_MODULES_ALL)) - return; - int count = qMin((int)(needed / sizeof(HMODULE)), 1024); - m_modules.reserve(count); - for (int i = 0; i < count; ++i) { - MODULEINFO mi{}; - WCHAR modName[MAX_PATH]; - if (GetModuleInformation(m_handle, mods[i], &mi, sizeof(mi)) - && GetModuleBaseNameW(m_handle, mods[i], modName, MAX_PATH)) - { - m_modules.append({ - QString::fromWCharArray(modName), - (uint64_t)mi.lpBaseOfDll, - (uint64_t)mi.SizeOfImage - }); - } - } - } -}; - -} // namespace rcx -#endif // _WIN32 diff --git a/src/providers/provider.h b/src/providers/provider.h index 9c9d1c6..3d778b6 100644 --- a/src/providers/provider.h +++ b/src/providers/provider.h @@ -40,7 +40,7 @@ public: // Resolve an absolute address to a symbol name. // Returns empty string if no symbol is known. - // ProcessProvider: "ntdll.dll+0x1A30" + // Example: "ntdll.dll+0x1A30" // BufferProvider: "" (no symbols in flat files) virtual QString getSymbol(uint64_t addr) const { Q_UNUSED(addr); diff --git a/src/typeselectorpopup.cpp b/src/typeselectorpopup.cpp index 8012fae..cae013b 100644 --- a/src/typeselectorpopup.cpp +++ b/src/typeselectorpopup.cpp @@ -58,10 +58,14 @@ public: } x += 18; - // Icon 16x16 - static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg")); - structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16); - x += 20; + // Icon 16x16 — only for struct/class/enum entries (non-empty classKeyword) + bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size() + && !(*m_filtered)[row].classKeyword.isEmpty()); + if (hasIcon) { + static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg")); + structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16); + } + x += 20; // reserve space for alignment // Text painter->setPen(option.state & QStyle::State_Selected @@ -122,7 +126,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) { auto* row = new QHBoxLayout; row->setContentsMargins(0, 0, 0, 0); - m_titleLabel = new QLabel(QStringLiteral("View as type")); + m_titleLabel = new QLabel(QStringLiteral("Change root")); m_titleLabel->setPalette(pal); QFont bold = m_titleLabel->font(); bold.setBold(true); @@ -236,7 +240,7 @@ void TypeSelectorPopup::setTypes(const QVector& types, uint64_t curre void TypeSelectorPopup::popup(const QPoint& globalPos) { // Size: width based on longest entry, height based on count QFontMetrics fm(m_font); - int maxTextW = fm.horizontalAdvance(QStringLiteral("View as type Esc")); + int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element type Esc")); for (const auto& t : m_allTypes) { QString text = t.classKeyword + QStringLiteral(" ") + t.displayName; int w = 18 + 20 + fm.horizontalAdvance(text) + 16; // gutter + icon + text + pad @@ -283,7 +287,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) { || t.displayName.contains(text, Qt::CaseInsensitive) || t.classKeyword.contains(text, Qt::CaseInsensitive)) { m_filteredTypes.append(t); - displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName); + if (t.classKeyword.isEmpty()) + displayStrings << t.displayName; + else + displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName); } } @@ -305,9 +312,13 @@ void TypeSelectorPopup::acceptCurrent() { acceptIndex(idx.row()); } +void TypeSelectorPopup::setTitle(const QString& title) { + m_titleLabel->setText(title); +} + void TypeSelectorPopup::acceptIndex(int row) { if (row < 0 || row >= m_filteredTypes.size()) return; - emit typeSelected(m_filteredTypes[row].id); + emit typeSelected(m_filteredTypes[row].id, m_filteredTypes[row].displayName); hide(); } diff --git a/src/typeselectorpopup.h b/src/typeselectorpopup.h index 8d09230..32f6555 100644 --- a/src/typeselectorpopup.h +++ b/src/typeselectorpopup.h @@ -25,11 +25,12 @@ public: explicit TypeSelectorPopup(QWidget* parent = nullptr); void setFont(const QFont& font); + void setTitle(const QString& title); void setTypes(const QVector& types, uint64_t currentId); void popup(const QPoint& globalPos); signals: - void typeSelected(uint64_t structId); + void typeSelected(uint64_t id, const QString& displayName); void createNewTypeRequested(); void dismissed(); diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index b862917..3294b31 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -732,7 +732,7 @@ private slots: } void testArrayHeaderCharTypes() { - // UInt8 array → "char[N]", UInt16 → "wchar_t[N]" + // UInt8 array → "uint8_t[N]", UInt16 → "uint16_t[N]" NodeTree tree; tree.baseAddress = 0; @@ -769,11 +769,11 @@ private slots: for (int i = 0; i < result.meta.size(); i++) { if (!result.meta[i].isArrayHeader) continue; QString text = lines[i]; - if (text.contains("char[64]")) foundChar = true; - if (text.contains("wchar_t[32]")) foundWchar = true; + if (text.contains("uint8_t[64]")) foundChar = true; + if (text.contains("uint16_t[32]")) foundWchar = true; } - QVERIFY2(foundChar, "Should have 'char[64]' header"); - QVERIFY2(foundWchar, "Should have 'wchar_t[32]' header"); + QVERIFY2(foundChar, "Should have 'uint8_t[64]' header"); + QVERIFY2(foundWchar, "Should have 'uint16_t[32]' header"); } void testArraySpansClickable() { @@ -995,13 +995,13 @@ private slots: ComposeResult r2 = compose(tree, prov); QStringList lines2 = r2.text.split('\n'); bool found42 = false; - bool still10 = false; - for (const QString& l : lines2) { - if (l.contains("[42]")) found42 = true; - if (l.contains("[10]")) still10 = true; + bool still10Header = false; + for (int i = 0; i < r2.meta.size(); i++) { + if (r2.meta[i].isArrayHeader && lines2[i].contains("uint8_t[42]")) found42 = true; + if (r2.meta[i].isArrayHeader && lines2[i].contains("uint8_t[10]")) still10Header = true; } - QVERIFY2(found42, "Recomposed text should show [42]"); - QVERIFY2(!still10, "Recomposed text should NOT still show [10]"); + QVERIFY2(found42, "Recomposed header should show uint8_t[42]"); + QVERIFY2(!still10Header, "Recomposed header should NOT still show uint8_t[10]"); // Spans must still work after recompose int headerLine = -1; @@ -1015,6 +1015,161 @@ private slots: QCOMPARE(countText, QString("42")); } + void testPrimitiveArrayElements() { + // Expanded primitive array should synthesize element lines dynamically + NodeTree tree; + tree.baseAddress = 0x1000; + + Node root; + root.kind = NodeKind::Struct; + root.name = "Root"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node arr; + arr.kind = NodeKind::Array; + arr.name = "values"; + arr.parentId = rootId; + arr.offset = 0; + arr.elementKind = NodeKind::UInt32; + arr.arrayLen = 4; + tree.addNode(arr); + + // Buffer with known values: 0x11, 0x22, 0x33, 0x44 + QByteArray data(64, '\0'); + uint32_t v0 = 0x11, v1 = 0x22, v2 = 0x33, v3 = 0x44; + memcpy(data.data() + 0, &v0, 4); + memcpy(data.data() + 4, &v1, 4); + memcpy(data.data() + 8, &v2, 4); + memcpy(data.data() + 12, &v3, 4); + BufferProvider prov(data); + + ComposeResult result = compose(tree, prov); + QStringList lines = result.text.split('\n'); + + // Find array header + int headerLine = -1; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].isArrayHeader) { headerLine = i; break; } + } + QVERIFY2(headerLine >= 0, "Array header must exist"); + QVERIFY2(lines[headerLine].contains("uint32_t[4]"), + qPrintable("Header should contain 'uint32_t[4]': " + lines[headerLine])); + + // Count element field lines (depth >= 2, lineKind == Field) + int elemCount = 0; + bool found0 = false, found3 = false; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth >= 2) { + elemCount++; + // Type column should have combined type+index: "uint32_t[0]" + if (lines[i].contains("uint32_t[0]")) found0 = true; + if (lines[i].contains("uint32_t[3]")) found3 = true; + // isArrayElement flag must be set + QVERIFY2(result.meta[i].isArrayElement, + qPrintable("Element line must have isArrayElement=true: " + lines[i])); + } + } + QCOMPARE(elemCount, 4); + QVERIFY2(found0, "Should have uint32_t[0] element"); + QVERIFY2(found3, "Should have uint32_t[3] element"); + + // Check footer exists + bool hasFooter = false; + for (int i = headerLine + 1; i < result.meta.size(); i++) { + if (result.meta[i].lineKind == LineKind::Footer && result.meta[i].nodeKind == NodeKind::Array) { + hasFooter = true; + break; + } + } + QVERIFY2(hasFooter, "Array should have footer line"); + } + + void testPrimitiveArrayCollapsed() { + // Collapsed primitive array should show NO element lines + NodeTree tree; + tree.baseAddress = 0; + + Node root; + root.kind = NodeKind::Struct; + root.name = "Root"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node arr; + arr.kind = NodeKind::Array; + arr.name = "data"; + arr.parentId = rootId; + arr.offset = 0; + arr.elementKind = NodeKind::UInt16; + arr.arrayLen = 8; + arr.collapsed = true; + tree.addNode(arr); + + NullProvider prov; + ComposeResult result = compose(tree, prov); + + // No field lines at depth >= 2 (no synthesized elements) + int elemFields = 0; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth >= 2) + elemFields++; + } + QCOMPARE(elemFields, 0); + } + + void testStructArrayStillUsesChildren() { + // Struct array with manual children should still render child nodes, not synthesize + NodeTree tree; + tree.baseAddress = 0; + + Node root; + root.kind = NodeKind::Struct; + root.name = "Root"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node arr; + arr.kind = NodeKind::Array; + arr.name = "items"; + arr.parentId = rootId; + arr.offset = 0; + arr.elementKind = NodeKind::Struct; + arr.arrayLen = 1; + int ai = tree.addNode(arr); + uint64_t arrId = tree.nodes[ai].id; + + // One struct child + Node elem; + elem.kind = NodeKind::Struct; + elem.name = "Item"; + elem.parentId = arrId; + elem.offset = 0; + int ei = tree.addNode(elem); + uint64_t elemId = tree.nodes[ei].id; + + Node field; + field.kind = NodeKind::UInt32; + field.name = "val"; + field.parentId = elemId; + field.offset = 0; + tree.addNode(field); + + NullProvider prov; + ComposeResult result = compose(tree, prov); + + // Should have the child struct's field rendered + bool hasVal = false; + QStringList lines = result.text.split('\n'); + for (int i = 0; i < lines.size(); i++) { + if (lines[i].contains("val")) { hasVal = true; break; } + } + QVERIFY2(hasVal, "Struct array child field 'val' should be rendered"); + } + // ═════════════════════════════════════════════════════════════ // Pointer tests // ═════════════════════════════════════════════════════════════ diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index ffe32f0..2b7aa5e 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -61,7 +61,7 @@ private slots: ColumnSpan span = commandRowChevronSpan(text); QVERIFY(span.valid); QCOMPARE(span.start, 0); - QCOMPARE(span.end, 3); + QCOMPARE(span.end, 4); // includes trailing space for easier clicking } void testChevronSpanRejects() { @@ -117,9 +117,10 @@ private slots: QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected); QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested); - emit popup.typeSelected(2); + emit popup.typeSelected(2, QStringLiteral("B")); QCOMPARE(typeSpy.count(), 1); QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2); + QCOMPARE(typeSpy.at(0).at(1).toString(), QStringLiteral("B")); emit popup.createNewTypeRequested(); QCOMPARE(createSpy.count(), 1);