mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Array element offset display, fold arrow UX, type picker popup, and provider cleanup
- Show relative hex offset on array element separators ([N] +0x...) - Dim fold arrows and add hover highlight for better visibility - Extend fold/chevron click areas for easier interaction - Add type picker popup for array element type and pointer target editing - Remove process_provider.h in favor of plugin-based provider system - Expand compose/format to handle struct-of-array type names and widths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ if(NOT QT_FOUND)
|
|||||||
find_package(Qt5 REQUIRED COMPONENTS ${_QT_COMPONENTS})
|
find_package(Qt5 REQUIRED COMPONENTS ${_QT_COMPONENTS})
|
||||||
set(QT_VERSION_MAJOR 5)
|
set(QT_VERSION_MAJOR 5)
|
||||||
endif()
|
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})
|
set(QT Qt${QT_VERSION_MAJOR})
|
||||||
message(STATUS "Using ${QT}: ${${QT}_DIR}")
|
message(STATUS "Using ${QT}: ${${QT}_DIR}")
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ add_executable(ReclassX
|
|||||||
src/resources.qrc
|
src/resources.qrc
|
||||||
src/core.h
|
src/core.h
|
||||||
src/workspace_model.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.cpp
|
||||||
src/providerregistry.h
|
src/providerregistry.h
|
||||||
src/pluginmanager.cpp
|
src/pluginmanager.cpp
|
||||||
@@ -154,15 +156,6 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_command_row COMMAND test_command_row)
|
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
|
add_executable(test_controller tests/test_controller.cpp
|
||||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
|
|||||||
100
src/compose.cpp
100
src/compose.cpp
@@ -172,16 +172,19 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
void composeNode(ComposeState& state, const NodeTree& tree,
|
void composeNode(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false,
|
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,
|
void composeParent(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false,
|
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,
|
void composeParent(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base, uint64_t rootId, bool isArrayChild,
|
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];
|
const Node& node = tree.nodes[nodeIdx];
|
||||||
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
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.foldLevel = computeFoldLevel(depth, false);
|
||||||
lm.markerMask = 0;
|
lm.markerMask = 0;
|
||||||
lm.arrayElementIdx = arrayElementIdx;
|
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
|
// 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.elementKind = node.elementKind;
|
||||||
lm.arrayViewIdx = node.viewIndex;
|
lm.arrayViewIdx = node.viewIndex;
|
||||||
lm.arrayCount = node.arrayLen;
|
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 {
|
} else {
|
||||||
// All structs (root and nested) use the same header format
|
// All structs (root and nested) use the same header format
|
||||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
|
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
|
||||||
@@ -268,6 +275,61 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
|
|
||||||
int childDepth = depth + 1;
|
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)
|
// For arrays, render children as condensed (no header/footer for struct elements)
|
||||||
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
|
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
|
||||||
int elementIdx = 0;
|
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
|
// For array elements, also pass the element index for [N] separator
|
||||||
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
|
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
|
||||||
childrenAreArrayElements, node.id,
|
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,
|
void composeNode(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx, int depth,
|
const Provider& prov, int nodeIdx, int depth,
|
||||||
uint64_t base, uint64_t rootId, bool isArrayChild,
|
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];
|
const Node& node = tree.nodes[nodeIdx];
|
||||||
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
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) {
|
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 {
|
} else {
|
||||||
composeLeaf(state, tree, prov, nodeIdx, depth, absAddr, scopeId);
|
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)
|
// Helper: compute the display type string for a node (for width calculation)
|
||||||
auto nodeTypeName = [&](const Node& n) -> QString {
|
auto nodeTypeName = [&](const Node& n) -> QString {
|
||||||
if (n.kind == NodeKind::Array)
|
if (n.kind == NodeKind::Array) {
|
||||||
return fmt::arrayTypeName(n.elementKind, n.arrayLen);
|
QString sn = (n.elementKind == NodeKind::Struct)
|
||||||
|
? resolvePointerTarget(tree, n.refId) : QString();
|
||||||
|
return fmt::arrayTypeName(n.elementKind, n.arrayLen, sn);
|
||||||
|
}
|
||||||
if (n.kind == NodeKind::Struct)
|
if (n.kind == NodeKind::Struct)
|
||||||
return fmt::structTypeName(n);
|
return fmt::structTypeName(n);
|
||||||
if (n.kind == NodeKind::Pointer32 || n.kind == NodeKind::Pointer64)
|
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.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
|
||||||
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,11 +61,10 @@ private:
|
|||||||
// ── Saved source entry ──
|
// ── Saved source entry ──
|
||||||
|
|
||||||
struct SavedSourceEntry {
|
struct SavedSourceEntry {
|
||||||
QString kind; // "File" or "Process"
|
QString kind; // "File" or provider identifier (e.g. "processmemory")
|
||||||
QString displayName; // filename or process name
|
QString displayName; // filename or process name
|
||||||
QString filePath; // for File sources
|
QString filePath; // for File sources
|
||||||
uint32_t pid = 0; // for Process sources
|
QString providerTarget; // for plugin providers (e.g. "pid:name")
|
||||||
QString processName; // for Process sources
|
|
||||||
uint64_t baseAddress = 0;
|
uint64_t baseAddress = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,7 +111,7 @@ public:
|
|||||||
|
|
||||||
// MCP bridge accessors
|
// MCP bridge accessors
|
||||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
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<SavedSourceEntry>& savedSources() const { return m_savedSources; }
|
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
|
||||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||||
@@ -151,6 +150,8 @@ private:
|
|||||||
void switchToSavedSource(int idx);
|
void switchToSavedSource(int idx);
|
||||||
void pushSavedSourcesToEditors();
|
void pushSavedSourcesToEditors();
|
||||||
void showTypeSelectorPopup(RcxEditor* editor);
|
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 ──
|
// ── Auto-refresh methods ──
|
||||||
void setupAutoRefresh();
|
void setupAutoRefresh();
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ inline ColumnSpan commandRowRootNameSpan(const QString& lineText) {
|
|||||||
inline ColumnSpan commandRowChevronSpan(const QString& lineText) {
|
inline ColumnSpan commandRowChevronSpan(const QString& lineText) {
|
||||||
if (lineText.size() < 3) return {};
|
if (lineText.size() < 3) return {};
|
||||||
if (lineText[0] == '[' && lineText[1] == QChar(0x25B8) && lineText[2] == ']')
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -466,6 +466,10 @@ void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) {
|
|||||||
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
||||||
for (int i = 0; i < meta.size(); i++) {
|
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)) {
|
if (isHexPreview(meta[i].nodeKind)) {
|
||||||
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
||||||
if (len > 0)
|
if (len > 0)
|
||||||
@@ -979,7 +983,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
|
|||||||
|
|
||||||
if (h.line >= 0 && h.line < m_meta.size()) {
|
if (h.line >= 0 && h.line < m_meta.size()) {
|
||||||
h.nodeId = m_meta[h.line].nodeId;
|
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;
|
return h;
|
||||||
}
|
}
|
||||||
@@ -1044,11 +1048,12 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Array headers: check element type and count sub-spans first
|
// 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) {
|
if (lm.isArrayHeader) {
|
||||||
|
ColumnSpan elemCountClick = arrayElemCountClickSpanFor(lm, lineText);
|
||||||
ColumnSpan elemType = arrayElemTypeSpanFor(lm, lineText);
|
ColumnSpan elemType = arrayElemTypeSpanFor(lm, lineText);
|
||||||
ColumnSpan elemCount = arrayElemCountSpanFor(lm, lineText);
|
if (inSpan(elemCountClick)) { outTarget = EditTarget::ArrayElementCount; outLine = line; return true; }
|
||||||
if (inSpan(elemCount)) { outTarget = EditTarget::ArrayElementCount; outLine = line; return true; }
|
if (inSpan(elemType)) { outTarget = EditTarget::ArrayElementType; outLine = line; return true; }
|
||||||
if (inSpan(elemType)) { outTarget = EditTarget::ArrayElementType; outLine = line; return true; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback spans for header lines
|
// Fallback spans for header lines
|
||||||
@@ -1065,6 +1070,26 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
||||||
else return false;
|
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
|
// Padding nodes: hex bytes are display-only, not editable
|
||||||
if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding)
|
if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding)
|
||||||
return false;
|
return false;
|
||||||
@@ -1446,6 +1471,24 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
|
|
||||||
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||||
if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit
|
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;
|
if (m_editState.active) return false;
|
||||||
m_hoveredNodeId = 0;
|
m_hoveredNodeId = 0;
|
||||||
m_hoveredLine = -1;
|
m_hoveredLine = -1;
|
||||||
@@ -1759,7 +1802,6 @@ void RcxEditor::showSourcePicker() {
|
|||||||
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
menuFont.setPointSize(menuFont.pointSize() + zoom);
|
||||||
menu.setFont(menuFont);
|
menu.setFont(menuFont);
|
||||||
menu.addAction("file");
|
menu.addAction("file");
|
||||||
menu.addAction("process");
|
|
||||||
|
|
||||||
// Add all registered providers from global registry
|
// Add all registered providers from global registry
|
||||||
const auto& providers = ProviderRegistry::instance().providers();
|
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
|
// Determine cursor shape based on interaction type
|
||||||
Qt::CursorShape desired = Qt::ArrowCursor;
|
Qt::CursorShape desired = Qt::ArrowCursor;
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ signals:
|
|||||||
EditTarget target, const QString& text);
|
EditTarget target, const QString& text);
|
||||||
void inlineEditCancelled();
|
void inlineEditCancelled();
|
||||||
void typeSelectorRequested();
|
void typeSelectorRequested();
|
||||||
|
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||||
|
|||||||
@@ -317,27 +317,27 @@
|
|||||||
"strLen": 64
|
"strLen": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"arrayLen": 1,
|
"arrayLen": 4,
|
||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"elementKind": "UInt8",
|
"elementKind": "Float",
|
||||||
"id": "27",
|
"id": "27",
|
||||||
"kind": "Hex64",
|
"kind": "Array",
|
||||||
"name": "field_70",
|
"name": "scores",
|
||||||
"offset": 112,
|
"offset": 112,
|
||||||
"parentId": "1",
|
"parentId": "1",
|
||||||
"refId": "0",
|
"refId": "0",
|
||||||
"strLen": 64
|
"strLen": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"arrayLen": 1,
|
"arrayLen": 2,
|
||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"elementKind": "UInt8",
|
"elementKind": "Struct",
|
||||||
"id": "28",
|
"id": "28",
|
||||||
"kind": "Hex64",
|
"kind": "Array",
|
||||||
"name": "field_78",
|
"name": "materials",
|
||||||
"offset": 120,
|
"offset": 128,
|
||||||
"parentId": "1",
|
"parentId": "1",
|
||||||
"refId": "0",
|
"refId": "20",
|
||||||
"strLen": 64
|
"strLen": 64
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -41,13 +41,15 @@ QString typeName(NodeKind kind, int colType) {
|
|||||||
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType);
|
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Array type string: "uint32_t[16]" or "char[64]"
|
// Array type string: "uint32_t[16]" or "Material[2]"
|
||||||
QString arrayTypeName(NodeKind elemKind, int count) {
|
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName) {
|
||||||
auto* m = kindMeta(elemKind);
|
QString elem;
|
||||||
QString elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
if (elemKind == NodeKind::Struct && !structName.isEmpty())
|
||||||
// char[] for UInt8, wchar_t[] for UInt16
|
elem = structName;
|
||||||
if (elemKind == NodeKind::UInt8) elem = QStringLiteral("char");
|
else {
|
||||||
else if (elemKind == NodeKind::UInt16) elem = QStringLiteral("wchar_t");
|
auto* m = kindMeta(elemKind);
|
||||||
|
elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||||
|
}
|
||||||
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
|
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +145,9 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
|||||||
|
|
||||||
// ── Array header ──
|
// ── Array header ──
|
||||||
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
// Columnar format: <type[count]> <name> { (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 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("{");
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||||
return ind + type + SEP + node.name + SEP + suffix;
|
return ind + type + SEP + node.name + SEP + suffix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -597,8 +597,12 @@ static void buildBallDemo(NodeTree& tree) {
|
|||||||
|
|
||||||
// Pointer to Material in Ball struct
|
// 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::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() {
|
void MainWindow::newFile() {
|
||||||
|
|||||||
@@ -796,7 +796,8 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
|||||||
uint32_t pid = (uint32_t)args.value("pid").toInteger();
|
uint32_t pid = (uint32_t)args.value("pid").toInteger();
|
||||||
QString name = args.value("processName").toString();
|
QString name = args.value("processName").toString();
|
||||||
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
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) + ")");
|
return makeTextResult("Attached to process " + name + " (PID " + QString::number(pid) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "provider.h"
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
#include <windows.h>
|
|
||||||
#include <psapi.h>
|
|
||||||
|
|
||||||
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<ModuleInfo> 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
|
|
||||||
@@ -40,7 +40,7 @@ public:
|
|||||||
|
|
||||||
// Resolve an absolute address to a symbol name.
|
// Resolve an absolute address to a symbol name.
|
||||||
// Returns empty string if no symbol is known.
|
// Returns empty string if no symbol is known.
|
||||||
// ProcessProvider: "ntdll.dll+0x1A30"
|
// Example: "ntdll.dll+0x1A30"
|
||||||
// BufferProvider: "" (no symbols in flat files)
|
// BufferProvider: "" (no symbols in flat files)
|
||||||
virtual QString getSymbol(uint64_t addr) const {
|
virtual QString getSymbol(uint64_t addr) const {
|
||||||
Q_UNUSED(addr);
|
Q_UNUSED(addr);
|
||||||
|
|||||||
@@ -58,10 +58,14 @@ public:
|
|||||||
}
|
}
|
||||||
x += 18;
|
x += 18;
|
||||||
|
|
||||||
// Icon 16x16
|
// Icon 16x16 — only for struct/class/enum entries (non-empty classKeyword)
|
||||||
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
|
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||||
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
|
&& !(*m_filtered)[row].classKeyword.isEmpty());
|
||||||
x += 20;
|
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
|
// Text
|
||||||
painter->setPen(option.state & QStyle::State_Selected
|
painter->setPen(option.state & QStyle::State_Selected
|
||||||
@@ -122,7 +126,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
{
|
{
|
||||||
auto* row = new QHBoxLayout;
|
auto* row = new QHBoxLayout;
|
||||||
row->setContentsMargins(0, 0, 0, 0);
|
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);
|
m_titleLabel->setPalette(pal);
|
||||||
QFont bold = m_titleLabel->font();
|
QFont bold = m_titleLabel->font();
|
||||||
bold.setBold(true);
|
bold.setBold(true);
|
||||||
@@ -236,7 +240,7 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, uint64_t curre
|
|||||||
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||||
// Size: width based on longest entry, height based on count
|
// Size: width based on longest entry, height based on count
|
||||||
QFontMetrics fm(m_font);
|
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) {
|
for (const auto& t : m_allTypes) {
|
||||||
QString text = t.classKeyword + QStringLiteral(" ") + t.displayName;
|
QString text = t.classKeyword + QStringLiteral(" ") + t.displayName;
|
||||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16; // gutter + icon + text + pad
|
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.displayName.contains(text, Qt::CaseInsensitive)
|
||||||
|| t.classKeyword.contains(text, Qt::CaseInsensitive)) {
|
|| t.classKeyword.contains(text, Qt::CaseInsensitive)) {
|
||||||
m_filteredTypes.append(t);
|
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());
|
acceptIndex(idx.row());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TypeSelectorPopup::setTitle(const QString& title) {
|
||||||
|
m_titleLabel->setText(title);
|
||||||
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::acceptIndex(int row) {
|
void TypeSelectorPopup::acceptIndex(int row) {
|
||||||
if (row < 0 || row >= m_filteredTypes.size()) return;
|
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();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ public:
|
|||||||
explicit TypeSelectorPopup(QWidget* parent = nullptr);
|
explicit TypeSelectorPopup(QWidget* parent = nullptr);
|
||||||
|
|
||||||
void setFont(const QFont& font);
|
void setFont(const QFont& font);
|
||||||
|
void setTitle(const QString& title);
|
||||||
void setTypes(const QVector<TypeEntry>& types, uint64_t currentId);
|
void setTypes(const QVector<TypeEntry>& types, uint64_t currentId);
|
||||||
void popup(const QPoint& globalPos);
|
void popup(const QPoint& globalPos);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void typeSelected(uint64_t structId);
|
void typeSelected(uint64_t id, const QString& displayName);
|
||||||
void createNewTypeRequested();
|
void createNewTypeRequested();
|
||||||
void dismissed();
|
void dismissed();
|
||||||
|
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ private slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void testArrayHeaderCharTypes() {
|
void testArrayHeaderCharTypes() {
|
||||||
// UInt8 array → "char[N]", UInt16 → "wchar_t[N]"
|
// UInt8 array → "uint8_t[N]", UInt16 → "uint16_t[N]"
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0;
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
@@ -769,11 +769,11 @@ private slots:
|
|||||||
for (int i = 0; i < result.meta.size(); i++) {
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
if (!result.meta[i].isArrayHeader) continue;
|
if (!result.meta[i].isArrayHeader) continue;
|
||||||
QString text = lines[i];
|
QString text = lines[i];
|
||||||
if (text.contains("char[64]")) foundChar = true;
|
if (text.contains("uint8_t[64]")) foundChar = true;
|
||||||
if (text.contains("wchar_t[32]")) foundWchar = true;
|
if (text.contains("uint16_t[32]")) foundWchar = true;
|
||||||
}
|
}
|
||||||
QVERIFY2(foundChar, "Should have 'char[64]' header");
|
QVERIFY2(foundChar, "Should have 'uint8_t[64]' header");
|
||||||
QVERIFY2(foundWchar, "Should have 'wchar_t[32]' header");
|
QVERIFY2(foundWchar, "Should have 'uint16_t[32]' header");
|
||||||
}
|
}
|
||||||
|
|
||||||
void testArraySpansClickable() {
|
void testArraySpansClickable() {
|
||||||
@@ -995,13 +995,13 @@ private slots:
|
|||||||
ComposeResult r2 = compose(tree, prov);
|
ComposeResult r2 = compose(tree, prov);
|
||||||
QStringList lines2 = r2.text.split('\n');
|
QStringList lines2 = r2.text.split('\n');
|
||||||
bool found42 = false;
|
bool found42 = false;
|
||||||
bool still10 = false;
|
bool still10Header = false;
|
||||||
for (const QString& l : lines2) {
|
for (int i = 0; i < r2.meta.size(); i++) {
|
||||||
if (l.contains("[42]")) found42 = true;
|
if (r2.meta[i].isArrayHeader && lines2[i].contains("uint8_t[42]")) found42 = true;
|
||||||
if (l.contains("[10]")) still10 = true;
|
if (r2.meta[i].isArrayHeader && lines2[i].contains("uint8_t[10]")) still10Header = true;
|
||||||
}
|
}
|
||||||
QVERIFY2(found42, "Recomposed text should show [42]");
|
QVERIFY2(found42, "Recomposed header should show uint8_t[42]");
|
||||||
QVERIFY2(!still10, "Recomposed text should NOT still show [10]");
|
QVERIFY2(!still10Header, "Recomposed header should NOT still show uint8_t[10]");
|
||||||
|
|
||||||
// Spans must still work after recompose
|
// Spans must still work after recompose
|
||||||
int headerLine = -1;
|
int headerLine = -1;
|
||||||
@@ -1015,6 +1015,161 @@ private slots:
|
|||||||
QCOMPARE(countText, QString("42"));
|
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
|
// Pointer tests
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ private slots:
|
|||||||
ColumnSpan span = commandRowChevronSpan(text);
|
ColumnSpan span = commandRowChevronSpan(text);
|
||||||
QVERIFY(span.valid);
|
QVERIFY(span.valid);
|
||||||
QCOMPARE(span.start, 0);
|
QCOMPARE(span.start, 0);
|
||||||
QCOMPARE(span.end, 3);
|
QCOMPARE(span.end, 4); // includes trailing space for easier clicking
|
||||||
}
|
}
|
||||||
|
|
||||||
void testChevronSpanRejects() {
|
void testChevronSpanRejects() {
|
||||||
@@ -117,9 +117,10 @@ private slots:
|
|||||||
QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected);
|
QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected);
|
||||||
QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested);
|
QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested);
|
||||||
|
|
||||||
emit popup.typeSelected(2);
|
emit popup.typeSelected(2, QStringLiteral("B"));
|
||||||
QCOMPARE(typeSpy.count(), 1);
|
QCOMPARE(typeSpy.count(), 1);
|
||||||
QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2);
|
QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2);
|
||||||
|
QCOMPARE(typeSpy.at(0).at(1).toString(), QStringLiteral("B"));
|
||||||
|
|
||||||
emit popup.createNewTypeRequested();
|
emit popup.createNewTypeRequested();
|
||||||
QCOMPARE(createSpy.count(), 1);
|
QCOMPARE(createSpy.count(), 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user