mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: primitive pointer modifiers, type chooser fixes, double-click to edit
Type chooser: - Fix PointerTarget mode hiding primitives due to stale modifier state - Preselect */[n] modifier buttons to reflect current node type - Primitive pointer support: int32*, double**, etc with provider deref - hex64*/ptr64* with * modifier falls back to void* (meaningless deref) - isValidPrimitivePtrTarget guard in controller, compose, format - Modifier toggle no longer resets list selection - Primitive pointers open FieldType mode (not PointerTarget) - Type edit requires double-click (was single-click, too easy to misclick) Other: - Custom dock titlebar with themed close button, no float button - Status bar font synced at startup - Resize grip reworked as direct MainWindow child, font-independent - File menu "Source" renamed to "Current Tab Source" Tests: 41 type_selector, 39 editor, 17 controller (200 total, 0 failures)
This commit is contained in:
@@ -119,8 +119,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
QString ptrTypeOverride;
|
QString ptrTypeOverride;
|
||||||
QString ptrTargetName;
|
QString ptrTargetName;
|
||||||
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
|
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
|
||||||
ptrTargetName = resolvePointerTarget(tree, node.refId);
|
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
|
||||||
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
// Primitive pointer: e.g. "int32*" or "f64**"
|
||||||
|
const auto* meta = kindMeta(node.elementKind);
|
||||||
|
QString baseName = meta ? QString::fromLatin1(meta->typeName)
|
||||||
|
: QStringLiteral("void");
|
||||||
|
QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*");
|
||||||
|
ptrTypeOverride = baseName + stars;
|
||||||
|
} else {
|
||||||
|
ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||||
|
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int sub = 0; sub < numLines; sub++) {
|
for (int sub = 0; sub < numLines; sub++) {
|
||||||
|
|||||||
@@ -223,8 +223,17 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
TypePopupMode mode = TypePopupMode::FieldType;
|
TypePopupMode mode = TypePopupMode::FieldType;
|
||||||
if (target == EditTarget::ArrayElementType)
|
if (target == EditTarget::ArrayElementType)
|
||||||
mode = TypePopupMode::ArrayElement;
|
mode = TypePopupMode::ArrayElement;
|
||||||
else if (target == EditTarget::PointerTarget)
|
else if (target == EditTarget::PointerTarget) {
|
||||||
mode = TypePopupMode::PointerTarget;
|
// Primitive pointers (ptrDepth>0) should open FieldType with
|
||||||
|
// the base type selected and *//** preselected — not PointerTarget.
|
||||||
|
bool isPrimPtr = false;
|
||||||
|
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
|
||||||
|
const auto& n = m_doc->tree.nodes[nodeIdx];
|
||||||
|
isPrimPtr = n.ptrDepth > 0 && n.refId == 0;
|
||||||
|
}
|
||||||
|
mode = isPrimPtr ? TypePopupMode::FieldType
|
||||||
|
: TypePopupMode::PointerTarget;
|
||||||
|
}
|
||||||
showTypePopup(editor, mode, nodeIdx, globalPos);
|
showTypePopup(editor, mode, nodeIdx, globalPos);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1855,6 +1864,8 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
QVector<TypeEntry> entries;
|
QVector<TypeEntry> entries;
|
||||||
TypeEntry currentEntry;
|
TypeEntry currentEntry;
|
||||||
bool hasCurrent = false;
|
bool hasCurrent = false;
|
||||||
|
int preModId = 0; // modifier to preselect: 0=plain, 1=*, 2=**, 3=[n]
|
||||||
|
int preArrayCount = 0; // array count when preModId==3
|
||||||
|
|
||||||
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
||||||
for (const auto& m : kKindMeta) {
|
for (const auto& m : kKindMeta) {
|
||||||
@@ -1894,10 +1905,43 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TypePopupMode::FieldType:
|
case TypePopupMode::FieldType: {
|
||||||
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
|
||||||
if (node) {
|
bool isPtr = node
|
||||||
// Mark current primitive
|
&& (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64);
|
||||||
|
bool isTypedPtr = isPtr && node->refId != 0;
|
||||||
|
bool isPrimPtr = isPtr && node->ptrDepth > 0 && node->refId == 0;
|
||||||
|
bool isArray = node && node->kind == NodeKind::Array;
|
||||||
|
|
||||||
|
if (isPrimPtr) {
|
||||||
|
// Primitive pointer (e.g. int32* or f64**) — current = element kind, modifier = *//**
|
||||||
|
preModId = (node->ptrDepth >= 2) ? 2 : 1;
|
||||||
|
for (auto& e : entries) {
|
||||||
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
||||||
|
currentEntry = e;
|
||||||
|
hasCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isTypedPtr) {
|
||||||
|
// Typed pointer (e.g. Ball*) — current = composite target, modifier = *
|
||||||
|
preModId = 1;
|
||||||
|
} else if (isArray) {
|
||||||
|
// Array — modifier = [n]
|
||||||
|
preModId = 3;
|
||||||
|
preArrayCount = node->arrayLen;
|
||||||
|
if (node->elementKind != NodeKind::Struct) {
|
||||||
|
// Primitive array — mark element kind as current
|
||||||
|
for (auto& e : entries) {
|
||||||
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
||||||
|
currentEntry = e;
|
||||||
|
hasCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node) {
|
||||||
|
// Plain primitive — mark current
|
||||||
for (auto& e : entries) {
|
for (auto& e : entries) {
|
||||||
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
|
||||||
currentEntry = e;
|
currentEntry = e;
|
||||||
@@ -1906,8 +1950,14 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addComposites([](const Node&, const TypeEntry&) { return false; });
|
// For isTypedPtr or struct-array: current is a Composite, set by addComposites below
|
||||||
|
addComposites([&](const Node& n, const TypeEntry& e) {
|
||||||
|
if (isTypedPtr && n.refId == e.structId) return true;
|
||||||
|
if (isArray && n.elementKind == NodeKind::Struct && n.refId == e.structId) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case TypePopupMode::ArrayElement:
|
case TypePopupMode::ArrayElement:
|
||||||
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
|
||||||
@@ -1994,6 +2044,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
popup->setFont(font);
|
popup->setFont(font);
|
||||||
popup->setMode(mode);
|
popup->setMode(mode);
|
||||||
|
|
||||||
|
// Preselect modifier button to reflect current node state (after setMode resets to plain)
|
||||||
|
if (preModId > 0)
|
||||||
|
popup->setModifier(preModId, preArrayCount);
|
||||||
|
|
||||||
// Pass current node size for same-size sorting
|
// Pass current node size for same-size sorting
|
||||||
int nodeSize = 0;
|
int nodeSize = 0;
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -2107,6 +2161,44 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
|||||||
m_doc->undoStack.endMacro();
|
m_doc->undoStack.endMacro();
|
||||||
m_suppressRefresh = wasSuppressed;
|
m_suppressRefresh = wasSuppressed;
|
||||||
if (!m_suppressRefresh) refresh();
|
if (!m_suppressRefresh) refresh();
|
||||||
|
} else if (spec.isPointer) {
|
||||||
|
if (!isValidPrimitivePtrTarget(resolved.primitiveKind)) {
|
||||||
|
// Hex, pointer, fnptr types with * → plain void pointer
|
||||||
|
if (nodeKind != NodeKind::Pointer64)
|
||||||
|
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
||||||
|
int idx = m_doc->tree.indexOfId(nodeId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
auto& n = m_doc->tree.nodes[idx];
|
||||||
|
n.ptrDepth = 0;
|
||||||
|
if (n.refId != 0)
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::ChangePointerRef{nodeId, n.refId, 0}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Primitive pointer: e.g. "int32*" or "f64**" → Pointer64 + elementKind + ptrDepth
|
||||||
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
|
m_suppressRefresh = true;
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
|
||||||
|
if (nodeKind != NodeKind::Pointer64)
|
||||||
|
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
||||||
|
int idx = m_doc->tree.indexOfId(nodeId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
auto& n = m_doc->tree.nodes[idx];
|
||||||
|
if (n.elementKind != resolved.primitiveKind || n.ptrDepth != spec.ptrDepth) {
|
||||||
|
NodeKind oldEK = n.elementKind;
|
||||||
|
int oldDepth = n.ptrDepth;
|
||||||
|
n.elementKind = resolved.primitiveKind;
|
||||||
|
n.ptrDepth = spec.ptrDepth;
|
||||||
|
if (n.refId != 0)
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::ChangePointerRef{nodeId, n.refId, 0}));
|
||||||
|
Q_UNUSED(oldEK); Q_UNUSED(oldDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
m_suppressRefresh = wasSuppressed;
|
||||||
|
if (!m_suppressRefresh) refresh();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (resolved.primitiveKind != nodeKind)
|
if (resolved.primitiveKind != nodeKind)
|
||||||
changeNodeKind(nodeIdx, resolved.primitiveKind);
|
changeNodeKind(nodeIdx, resolved.primitiveKind);
|
||||||
|
|||||||
15
src/core.h
15
src/core.h
@@ -142,6 +142,15 @@ inline constexpr bool isMatrixKind(NodeKind k) {
|
|||||||
inline constexpr bool isFuncPtr(NodeKind k) {
|
inline constexpr bool isFuncPtr(NodeKind k) {
|
||||||
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
|
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
|
||||||
}
|
}
|
||||||
|
// Hex types, pointer types, function pointers, and containers are not meaningful
|
||||||
|
// primitive-pointer targets — dereferencing them produces the same output as void*.
|
||||||
|
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
|
||||||
|
if (isHexNode(k)) return false;
|
||||||
|
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
|
||||||
|
if (isFuncPtr(k)) return false;
|
||||||
|
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||||
QStringList out;
|
QStringList out;
|
||||||
@@ -184,7 +193,8 @@ struct Node {
|
|||||||
int strLen = 64;
|
int strLen = 64;
|
||||||
bool collapsed = false;
|
bool collapsed = false;
|
||||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type
|
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
||||||
|
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||||
int viewIndex = 0; // Array: current view offset (transient)
|
int viewIndex = 0; // Array: current view offset (transient)
|
||||||
|
|
||||||
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||||
@@ -217,6 +227,8 @@ struct Node {
|
|||||||
o["collapsed"] = collapsed;
|
o["collapsed"] = collapsed;
|
||||||
o["refId"] = QString::number(refId);
|
o["refId"] = QString::number(refId);
|
||||||
o["elementKind"] = kindToString(elementKind);
|
o["elementKind"] = kindToString(elementKind);
|
||||||
|
if (ptrDepth > 0)
|
||||||
|
o["ptrDepth"] = ptrDepth;
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
static Node fromJson(const QJsonObject& o) {
|
static Node fromJson(const QJsonObject& o) {
|
||||||
@@ -233,6 +245,7 @@ struct Node {
|
|||||||
n.collapsed = o["collapsed"].toBool(false);
|
n.collapsed = o["collapsed"].toBool(false);
|
||||||
n.refId = o["refId"].toString("0").toULongLong();
|
n.refId = o["refId"].toString("0").toULongLong();
|
||||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||||
|
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1789,15 +1789,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
// Single-click on editable token of already-selected node → edit
|
// Single-click on editable token of already-selected node → edit
|
||||||
int tLine, tCol; EditTarget t;
|
int tLine, tCol; EditTarget t;
|
||||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
||||||
// Type/ArrayElementType/PointerTarget open a dismissible popup
|
if (alreadySelected && plain) {
|
||||||
// (not inline text edit), so allow on first click without
|
|
||||||
// requiring the node to be pre-selected.
|
|
||||||
bool isPopupTarget = (t == EditTarget::Type
|
|
||||||
|| t == EditTarget::ArrayElementType
|
|
||||||
|| t == EditTarget::PointerTarget);
|
|
||||||
if ((alreadySelected || isPopupTarget) && plain) {
|
|
||||||
if (!alreadySelected)
|
|
||||||
emit nodeClicked(h.line, h.nodeId, me->modifiers());
|
|
||||||
m_pendingClickNodeId = 0;
|
m_pendingClickNodeId = 0;
|
||||||
return beginInlineEdit(t, tLine, tCol);
|
return beginInlineEdit(t, tLine, tCol);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,30 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
|||||||
}
|
}
|
||||||
case NodeKind::Pointer64: {
|
case NodeKind::Pointer64: {
|
||||||
uint64_t val = prov.readU64(addr);
|
uint64_t val = prov.readU64(addr);
|
||||||
|
// Primitive pointer: dereference and show target value
|
||||||
|
// (hex/ptr/fnptr targets fall through to plain void* display)
|
||||||
|
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
|
||||||
|
uint64_t target = val;
|
||||||
|
for (int d = 1; d < node.ptrDepth && target != 0; d++)
|
||||||
|
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
|
||||||
|
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
|
||||||
|
// Create a temporary node of the target kind to format the value
|
||||||
|
Node tmp;
|
||||||
|
tmp.kind = node.elementKind;
|
||||||
|
tmp.strLen = node.strLen;
|
||||||
|
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
|
||||||
|
if (display) {
|
||||||
|
QString arrow = QStringLiteral("-> ");
|
||||||
|
QString sym = prov.getSymbol(val);
|
||||||
|
if (!sym.isEmpty())
|
||||||
|
return arrow + derefVal + QStringLiteral(" // ") + sym;
|
||||||
|
return arrow + derefVal;
|
||||||
|
}
|
||||||
|
return derefVal;
|
||||||
|
}
|
||||||
|
if (!display) return rawHex(val, 16);
|
||||||
|
return fmtPointer64(val);
|
||||||
|
}
|
||||||
if (!display) return rawHex(val, 16);
|
if (!display) return rawHex(val, 16);
|
||||||
QString s = fmtPointer64(val);
|
QString s = fmtPointer64(val);
|
||||||
QString sym = prov.getSymbol(val);
|
QString sym = prov.getSymbol(val);
|
||||||
|
|||||||
39
src/main.cpp
39
src/main.cpp
@@ -410,7 +410,7 @@ void MainWindow::createMenus() {
|
|||||||
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
||||||
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
m_sourceMenu = file->addMenu("So&urce");
|
m_sourceMenu = file->addMenu("Current Tab So&urce");
|
||||||
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||||
@@ -499,14 +499,26 @@ void MainWindow::createMenus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Themed resize grip (replaces ugly default QSizeGrip) ──
|
// ── Themed resize grip (replaces ugly default QSizeGrip) ──
|
||||||
|
// Positioned as a direct child of MainWindow at the bottom-right corner,
|
||||||
|
// NOT inside the status bar layout (which is font-height dependent).
|
||||||
class ResizeGrip : public QWidget {
|
class ResizeGrip : public QWidget {
|
||||||
public:
|
public:
|
||||||
|
static constexpr int kSize = 16; // widget size
|
||||||
|
static constexpr int kPad = 4; // padding from window corner (identical right & bottom)
|
||||||
|
|
||||||
explicit ResizeGrip(QWidget* parent) : QWidget(parent) {
|
explicit ResizeGrip(QWidget* parent) : QWidget(parent) {
|
||||||
setFixedSize(16, 16);
|
setFixedSize(kSize, kSize);
|
||||||
setCursor(Qt::SizeFDiagCursor);
|
setCursor(Qt::SizeFDiagCursor);
|
||||||
m_color = rcx::ThemeManager::instance().current().textFaint;
|
m_color = rcx::ThemeManager::instance().current().textFaint;
|
||||||
}
|
}
|
||||||
void setGripColor(const QColor& c) { m_color = c; update(); }
|
void setGripColor(const QColor& c) { m_color = c; update(); }
|
||||||
|
|
||||||
|
// Call from parent's resizeEvent to pin to bottom-right corner
|
||||||
|
void reposition() {
|
||||||
|
QWidget* w = parentWidget();
|
||||||
|
if (w) move(w->width() - kSize - kPad, w->height() - kSize - kPad);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent*) override {
|
void paintEvent(QPaintEvent*) override {
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
@@ -514,8 +526,11 @@ protected:
|
|||||||
p.setPen(Qt::NoPen);
|
p.setPen(Qt::NoPen);
|
||||||
p.setBrush(m_color);
|
p.setBrush(m_color);
|
||||||
// 6 dots in a triangle pointing bottom-right (VS2022 style)
|
// 6 dots in a triangle pointing bottom-right (VS2022 style)
|
||||||
|
// Dot grid is centered within the widget: same inset from right and bottom
|
||||||
const double r = 1.0, s = 4.0;
|
const double r = 1.0, s = 4.0;
|
||||||
double bx = width() - 5, by = height() - 4;
|
const double inset = 4.0;
|
||||||
|
double bx = width() - inset;
|
||||||
|
double by = height() - inset;
|
||||||
// bottom row: 3 dots
|
// bottom row: 3 dots
|
||||||
p.drawEllipse(QPointF(bx, by), r, r);
|
p.drawEllipse(QPointF(bx, by), r, r);
|
||||||
p.drawEllipse(QPointF(bx - s, by), r, r);
|
p.drawEllipse(QPointF(bx - s, by), r, r);
|
||||||
@@ -539,13 +554,15 @@ private:
|
|||||||
void MainWindow::createStatusBar() {
|
void MainWindow::createStatusBar() {
|
||||||
m_statusLabel = new QLabel("Ready");
|
m_statusLabel = new QLabel("Ready");
|
||||||
m_statusLabel->setContentsMargins(10, 0, 0, 0);
|
m_statusLabel->setContentsMargins(10, 0, 0, 0);
|
||||||
statusBar()->setContentsMargins(0, 4, 0, 0);
|
statusBar()->setContentsMargins(0, 0, 0, 0);
|
||||||
statusBar()->setSizeGripEnabled(false); // disable ugly default grip
|
statusBar()->setSizeGripEnabled(false); // disable ugly default grip
|
||||||
statusBar()->addWidget(m_statusLabel, 1);
|
statusBar()->addWidget(m_statusLabel, 1);
|
||||||
|
|
||||||
|
// Grip is a direct child of the main window, NOT in the status bar layout.
|
||||||
|
// Positioned via reposition() in resizeEvent — immune to font/margin changes.
|
||||||
auto* grip = new ResizeGrip(this);
|
auto* grip = new ResizeGrip(this);
|
||||||
grip->setObjectName("resizeGrip");
|
grip->setObjectName("resizeGrip");
|
||||||
statusBar()->addPermanentWidget(grip);
|
grip->raise();
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
@@ -1116,15 +1133,16 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
// Re-style ✕ close buttons on MDI tabs
|
// Re-style ✕ close buttons on MDI tabs
|
||||||
styleTabCloseButtons();
|
styleTabCloseButtons();
|
||||||
|
|
||||||
// Status bar + resize grip
|
// Status bar
|
||||||
{
|
{
|
||||||
QPalette sbPal = statusBar()->palette();
|
QPalette sbPal = statusBar()->palette();
|
||||||
sbPal.setColor(QPalette::Window, theme.background);
|
sbPal.setColor(QPalette::Window, theme.background);
|
||||||
sbPal.setColor(QPalette::WindowText, theme.textDim);
|
sbPal.setColor(QPalette::WindowText, theme.textDim);
|
||||||
statusBar()->setPalette(sbPal);
|
statusBar()->setPalette(sbPal);
|
||||||
auto* grip = statusBar()->findChild<ResizeGrip*>("resizeGrip");
|
|
||||||
if (grip) grip->setGripColor(theme.textFaint);
|
|
||||||
}
|
}
|
||||||
|
// Resize grip (direct child of main window, not in status bar)
|
||||||
|
if (auto* grip = findChild<ResizeGrip*>("resizeGrip"))
|
||||||
|
grip->setGripColor(theme.textFaint);
|
||||||
|
|
||||||
// Workspace tree: text color matches menu bar
|
// Workspace tree: text color matches menu bar
|
||||||
if (m_workspaceTree) {
|
if (m_workspaceTree) {
|
||||||
@@ -2060,6 +2078,11 @@ void MainWindow::resizeEvent(QResizeEvent* event) {
|
|||||||
m_borderOverlay->setGeometry(rect());
|
m_borderOverlay->setGeometry(rect());
|
||||||
m_borderOverlay->raise();
|
m_borderOverlay->raise();
|
||||||
}
|
}
|
||||||
|
auto* grip = findChild<ResizeGrip*>("resizeGrip");
|
||||||
|
if (grip) {
|
||||||
|
grip->reposition();
|
||||||
|
grip->raise();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::updateBorderColor(const QColor& color) {
|
void MainWindow::updateBorderColor(const QColor& color) {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
|
|||||||
if (s.endsWith('*')) {
|
if (s.endsWith('*')) {
|
||||||
spec.isPointer = true;
|
spec.isPointer = true;
|
||||||
s.chop(1);
|
s.chop(1);
|
||||||
if (s.endsWith('*')) s.chop(1); // double pointer
|
spec.ptrDepth = 1;
|
||||||
|
if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; }
|
||||||
spec.baseName = s.trimmed();
|
spec.baseName = s.trimmed();
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
@@ -347,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
m_arrayCountEdit->selectAll();
|
m_arrayCountEdit->selectAll();
|
||||||
}
|
}
|
||||||
updateModifierPreview();
|
updateModifierPreview();
|
||||||
applyFilter(m_filterEdit->text());
|
|
||||||
});
|
});
|
||||||
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||||
this, [this]() { updateModifierPreview(); });
|
this, [this]() { updateModifierPreview(); });
|
||||||
@@ -516,22 +516,32 @@ void TypeSelectorPopup::setTitle(const QString& title) {
|
|||||||
|
|
||||||
void TypeSelectorPopup::setMode(TypePopupMode mode) {
|
void TypeSelectorPopup::setMode(TypePopupMode mode) {
|
||||||
m_mode = mode;
|
m_mode = mode;
|
||||||
// Show modifier toggles for modes where type modifiers make sense
|
|
||||||
bool showMods = (mode == TypePopupMode::FieldType
|
bool showMods = (mode == TypePopupMode::FieldType
|
||||||
|| mode == TypePopupMode::ArrayElement);
|
|| mode == TypePopupMode::ArrayElement);
|
||||||
m_modRow->setVisible(showMods);
|
m_modRow->setVisible(showMods);
|
||||||
// Reset to plain when showing
|
// Always reset to plain — prevents stale state from leaking across modes
|
||||||
if (showMods) {
|
// (PointerTarget hides buttons but applyFilter still reads their state)
|
||||||
m_btnPlain->setChecked(true);
|
m_btnPlain->setChecked(true);
|
||||||
m_arrayCountEdit->clear();
|
m_arrayCountEdit->clear();
|
||||||
m_arrayCountEdit->hide();
|
m_arrayCountEdit->hide();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
|
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
|
||||||
m_currentNodeSize = bytes;
|
m_currentNodeSize = bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TypeSelectorPopup::setModifier(int modId, int arrayCount) {
|
||||||
|
if (modId == 1) m_btnPtr->setChecked(true);
|
||||||
|
else if (modId == 2) m_btnDblPtr->setChecked(true);
|
||||||
|
else if (modId == 3) {
|
||||||
|
m_btnArray->setChecked(true);
|
||||||
|
m_arrayCountEdit->setText(QString::number(arrayCount));
|
||||||
|
m_arrayCountEdit->show();
|
||||||
|
} else {
|
||||||
|
m_btnPlain->setChecked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
|
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
|
||||||
m_allTypes = types;
|
m_allTypes = types;
|
||||||
if (current) {
|
if (current) {
|
||||||
@@ -541,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
|||||||
m_currentEntry = TypeEntry{};
|
m_currentEntry = TypeEntry{};
|
||||||
m_hasCurrent = false;
|
m_hasCurrent = false;
|
||||||
}
|
}
|
||||||
// Reset modifier toggles
|
// Don't reset modifier buttons here — setMode() already resets to plain,
|
||||||
m_btnPlain->setChecked(true);
|
// and setModifier() may have preselected a button between setMode/setTypes.
|
||||||
m_arrayCountEdit->clear();
|
|
||||||
m_arrayCountEdit->hide();
|
|
||||||
m_previewLabel->hide();
|
m_previewLabel->hide();
|
||||||
|
|
||||||
m_filterEdit->clear();
|
m_filterEdit->clear();
|
||||||
@@ -630,23 +638,18 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
|||||||
|
|
||||||
QString filterBase = text.trimmed();
|
QString filterBase = text.trimmed();
|
||||||
|
|
||||||
// Hide primitives when a pointer modifier (* or **) is active
|
// Separate primitives and composites (all types shown regardless of modifier)
|
||||||
int modId = m_modGroup->checkedId();
|
|
||||||
bool hideprimitives = (modId == 1 || modId == 2);
|
|
||||||
|
|
||||||
// Separate primitives and composites
|
|
||||||
QVector<TypeEntry> primitives, composites;
|
QVector<TypeEntry> primitives, composites;
|
||||||
for (const auto& t : m_allTypes) {
|
for (const auto& t : m_allTypes) {
|
||||||
if (t.entryKind == TypeEntry::Section) continue; // skip stale sections
|
if (t.entryKind == TypeEntry::Section) continue;
|
||||||
bool matchesFilter = filterBase.isEmpty()
|
bool matchesFilter = filterBase.isEmpty()
|
||||||
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|
||||||
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||||
if (!matchesFilter) continue;
|
if (!matchesFilter) continue;
|
||||||
|
|
||||||
if (t.entryKind == TypeEntry::Primitive) {
|
if (t.entryKind == TypeEntry::Primitive)
|
||||||
if (!hideprimitives)
|
primitives.append(t);
|
||||||
primitives.append(t);
|
else if (t.entryKind == TypeEntry::Composite)
|
||||||
} else if (t.entryKind == TypeEntry::Composite)
|
|
||||||
composites.append(t);
|
composites.append(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ struct TypeEntry {
|
|||||||
struct TypeSpec {
|
struct TypeSpec {
|
||||||
QString baseName;
|
QString baseName;
|
||||||
bool isPointer = false;
|
bool isPointer = false;
|
||||||
|
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
|
||||||
int arrayCount = 0; // 0 = not array
|
int arrayCount = 0; // 0 = not array
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ public:
|
|||||||
void setMode(TypePopupMode mode);
|
void setMode(TypePopupMode mode);
|
||||||
void applyTheme(const Theme& theme);
|
void applyTheme(const Theme& theme);
|
||||||
void setCurrentNodeSize(int bytes);
|
void setCurrentNodeSize(int bytes);
|
||||||
|
void setModifier(int modId, int arrayCount = 0);
|
||||||
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
||||||
void popup(const QPoint& globalPos);
|
void popup(const QPoint& globalPos);
|
||||||
|
|
||||||
|
|||||||
@@ -2048,26 +2048,33 @@ private slots:
|
|||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: resize grip equidistant from right and bottom window edges ──
|
// ── Test: resize grip dots are equidistant from right and bottom window edges ──
|
||||||
|
// The grip is a direct child of the window positioned via move(), not inside
|
||||||
|
// the status bar layout. This test verifies the dot placement is symmetric
|
||||||
|
// regardless of font, and runs the check at two different font sizes to prove
|
||||||
|
// font independence.
|
||||||
void testResizeGripCornerSymmetry() {
|
void testResizeGripCornerSymmetry() {
|
||||||
// Reproduce the exact MainWindow status bar + grip setup
|
// Same constants as production ResizeGrip in main.cpp
|
||||||
QMainWindow win;
|
static constexpr int kSize = 16;
|
||||||
win.resize(400, 300);
|
static constexpr int kPad = 4;
|
||||||
win.statusBar()->setSizeGripEnabled(false);
|
static constexpr double kInset = 4.0;
|
||||||
win.statusBar()->setContentsMargins(0, 4, 0, 0);
|
|
||||||
|
|
||||||
// Inline replica of the ResizeGrip paint (same constants as main.cpp)
|
|
||||||
class Grip : public QWidget {
|
class Grip : public QWidget {
|
||||||
public:
|
public:
|
||||||
explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(16, 16); }
|
explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(kSize, kSize); }
|
||||||
|
void reposition() {
|
||||||
|
if (auto* w = parentWidget())
|
||||||
|
move(w->width() - kSize - kPad, w->height() - kSize - kPad);
|
||||||
|
}
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent*) override {
|
void paintEvent(QPaintEvent*) override {
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
p.setRenderHint(QPainter::Antialiasing);
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
p.setPen(Qt::NoPen);
|
p.setPen(Qt::NoPen);
|
||||||
p.setBrush(Qt::red); // high-contrast so we can find it
|
p.setBrush(Qt::red);
|
||||||
const double r = 1.0, s = 4.0;
|
const double r = 1.0, s = 4.0;
|
||||||
double bx = width() - 5, by = height() - 4;
|
double bx = width() - kInset;
|
||||||
|
double by = height() - kInset;
|
||||||
p.drawEllipse(QPointF(bx, by), r, r);
|
p.drawEllipse(QPointF(bx, by), r, r);
|
||||||
p.drawEllipse(QPointF(bx - s, by), r, r);
|
p.drawEllipse(QPointF(bx - s, by), r, r);
|
||||||
p.drawEllipse(QPointF(bx - 2 * s, by), r, r);
|
p.drawEllipse(QPointF(bx - 2 * s, by), r, r);
|
||||||
@@ -2077,73 +2084,87 @@ private slots:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto* grip = new Grip(&win);
|
// Helper: grab window, find bottommost-rightmost red pixel, measure gaps
|
||||||
win.statusBar()->addPermanentWidget(grip);
|
auto measureGaps = [](QWidget* win, int& gapRight, int& gapBottom) -> bool {
|
||||||
|
QPixmap px = win->grab();
|
||||||
|
QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
|
int W = img.width(), H = img.height();
|
||||||
|
if (W < 50 || H < 50) return false;
|
||||||
|
|
||||||
// Use a known background so non-grip pixels are easy to identify
|
int foundX = -1, foundY = -1;
|
||||||
QPalette pal = win.statusBar()->palette();
|
for (int y = H - 1; y >= H - 40 && foundY < 0; --y) {
|
||||||
pal.setColor(QPalette::Window, QColor(30, 30, 30));
|
for (int x = W - 1; x >= W - 40; --x) {
|
||||||
win.statusBar()->setPalette(pal);
|
QColor c(img.pixel(x, y));
|
||||||
win.statusBar()->setAutoFillBackground(true);
|
if (c.red() > 180 && c.green() < 80 && c.blue() < 80) {
|
||||||
|
foundX = x; foundY = y; break;
|
||||||
win.show();
|
}
|
||||||
QVERIFY(QTest::qWaitForWindowExposed(&win));
|
|
||||||
QTest::qWait(100); // let paint settle
|
|
||||||
|
|
||||||
// Grab just the window contents (no DWM shadow)
|
|
||||||
QPixmap px = win.grab();
|
|
||||||
QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32);
|
|
||||||
int W = img.width();
|
|
||||||
int H = img.height();
|
|
||||||
QVERIFY(W > 50);
|
|
||||||
QVERIFY(H > 50);
|
|
||||||
|
|
||||||
// Scan from bottom-right to find the bottommost-rightmost red pixel
|
|
||||||
// (the corner dot of the grip triangle)
|
|
||||||
int gripRight = -1, gripBottom = -1;
|
|
||||||
for (int y = H - 1; y >= H - 40 && gripBottom < 0; --y) {
|
|
||||||
for (int x = W - 1; x >= W - 40; --x) {
|
|
||||||
QColor c(img.pixel(x, y));
|
|
||||||
if (c.red() > 180 && c.green() < 80 && c.blue() < 80) {
|
|
||||||
gripRight = x;
|
|
||||||
gripBottom = y;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (gripBottom >= 0) break;
|
if (foundX < 0) return false;
|
||||||
}
|
gapRight = (W - 1) - foundX;
|
||||||
|
gapBottom = (H - 1) - foundY;
|
||||||
|
|
||||||
QVERIFY2(gripRight >= 0 && gripBottom >= 0,
|
// Save diagnostic image
|
||||||
"Could not find red grip dot in bottom-right corner");
|
|
||||||
|
|
||||||
int gapRight = (W - 1) - gripRight;
|
|
||||||
int gapBottom = (H - 1) - gripBottom;
|
|
||||||
|
|
||||||
// Save diagnostic image with markers
|
|
||||||
{
|
|
||||||
QImage diag = img.copy();
|
QImage diag = img.copy();
|
||||||
QPainter dp(&diag);
|
QPainter dp(&diag);
|
||||||
dp.setPen(QPen(Qt::cyan, 1));
|
dp.setPen(QPen(Qt::cyan, 1));
|
||||||
// Mark the found dot
|
dp.drawRect(foundX - 3, foundY - 3, 6, 6);
|
||||||
dp.drawRect(gripRight - 3, gripBottom - 3, 6, 6);
|
|
||||||
// Draw gap measurement lines
|
|
||||||
dp.setPen(QPen(Qt::yellow, 1));
|
dp.setPen(QPen(Qt::yellow, 1));
|
||||||
dp.drawLine(gripRight, gripBottom, W - 1, gripBottom); // right gap
|
dp.drawLine(foundX, foundY, W - 1, foundY);
|
||||||
dp.drawLine(gripRight, gripBottom, gripRight, H - 1); // bottom gap
|
dp.drawLine(foundX, foundY, foundX, H - 1);
|
||||||
dp.end();
|
dp.end();
|
||||||
diag.save("grip_corner_diag.png");
|
diag.save("grip_corner_diag.png");
|
||||||
}
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
QString msg = QString("gapRight=%1 gapBottom=%2 (diff=%3) gripPos=(%4,%5) winSize=%6x%7")
|
// --- Round 1: default system font ---
|
||||||
.arg(gapRight).arg(gapBottom).arg(qAbs(gapRight - gapBottom))
|
QMainWindow win;
|
||||||
.arg(gripRight).arg(gripBottom).arg(W).arg(H);
|
win.resize(500, 375);
|
||||||
|
|
||||||
// The gaps must be equal (symmetric corner placement)
|
QPalette pal;
|
||||||
QVERIFY2(qAbs(gapRight - gapBottom) <= 1,
|
pal.setColor(QPalette::Window, QColor(30, 30, 30));
|
||||||
qPrintable("Grip not equidistant from edges: " + msg));
|
win.setPalette(pal);
|
||||||
|
win.statusBar()->setPalette(pal);
|
||||||
|
win.statusBar()->setAutoFillBackground(true);
|
||||||
|
|
||||||
// Also log the values even on pass
|
auto* grip = new Grip(&win);
|
||||||
qDebug() << "Grip corner symmetry:" << msg;
|
grip->raise();
|
||||||
|
|
||||||
|
win.show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(&win));
|
||||||
|
grip->reposition();
|
||||||
|
QTest::qWait(100);
|
||||||
|
|
||||||
|
int gapR1 = 0, gapB1 = 0;
|
||||||
|
QVERIFY2(measureGaps(&win, gapR1, gapB1),
|
||||||
|
"Could not find red grip dot (round 1)");
|
||||||
|
QVERIFY2(gapR1 == gapB1,
|
||||||
|
qPrintable(QString("Round 1 asymmetric: gapRight=%1 gapBottom=%2")
|
||||||
|
.arg(gapR1).arg(gapB1)));
|
||||||
|
|
||||||
|
// --- Round 2: large font on status bar (must NOT change grip position) ---
|
||||||
|
QFont bigFont("Arial", 24);
|
||||||
|
win.statusBar()->setFont(bigFont);
|
||||||
|
QTest::qWait(100);
|
||||||
|
grip->reposition();
|
||||||
|
QTest::qWait(100);
|
||||||
|
|
||||||
|
int gapR2 = 0, gapB2 = 0;
|
||||||
|
QVERIFY2(measureGaps(&win, gapR2, gapB2),
|
||||||
|
"Could not find red grip dot (round 2, big font)");
|
||||||
|
QVERIFY2(gapR2 == gapB2,
|
||||||
|
qPrintable(QString("Round 2 asymmetric: gapRight=%1 gapBottom=%2")
|
||||||
|
.arg(gapR2).arg(gapB2)));
|
||||||
|
|
||||||
|
// Gaps must be identical across both font sizes
|
||||||
|
QVERIFY2(gapR1 == gapR2 && gapB1 == gapB2,
|
||||||
|
qPrintable(QString("Font changed grip position: "
|
||||||
|
"round1=(%1,%2) round2=(%3,%4)")
|
||||||
|
.arg(gapR1).arg(gapB1).arg(gapR2).arg(gapB2)));
|
||||||
|
|
||||||
|
qDebug() << "Grip corner symmetry:"
|
||||||
|
<< QString("gapRight=%1 gapBottom=%2 (font-independent)")
|
||||||
|
.arg(gapR1).arg(gapB1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QButtonGroup>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QListView>
|
#include <QListView>
|
||||||
#include <QStringListModel>
|
#include <QStringListModel>
|
||||||
@@ -498,6 +499,7 @@ private slots:
|
|||||||
TypeSpec spec = parseTypeSpec("Ball*");
|
TypeSpec spec = parseTypeSpec("Ball*");
|
||||||
QCOMPARE(spec.baseName, QString("Ball"));
|
QCOMPARE(spec.baseName, QString("Ball"));
|
||||||
QVERIFY(spec.isPointer);
|
QVERIFY(spec.isPointer);
|
||||||
|
QCOMPARE(spec.ptrDepth, 1);
|
||||||
QCOMPARE(spec.arrayCount, 0);
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +507,7 @@ private slots:
|
|||||||
TypeSpec spec = parseTypeSpec("Ball**");
|
TypeSpec spec = parseTypeSpec("Ball**");
|
||||||
QCOMPARE(spec.baseName, QString("Ball"));
|
QCOMPARE(spec.baseName, QString("Ball"));
|
||||||
QVERIFY(spec.isPointer);
|
QVERIFY(spec.isPointer);
|
||||||
|
QCOMPARE(spec.ptrDepth, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void testParseTypeSpecEmpty() {
|
void testParseTypeSpecEmpty() {
|
||||||
@@ -960,6 +963,508 @@ private slots:
|
|||||||
// Restore
|
// Restore
|
||||||
tm.setCurrent(origIdx);
|
tm.setCurrent(origIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── parseTypeSpec: primitive pointer ptrDepth ──
|
||||||
|
|
||||||
|
void testParseTypeSpecPrimitiveStar() {
|
||||||
|
TypeSpec spec = parseTypeSpec("int32_t*");
|
||||||
|
QCOMPARE(spec.baseName, QString("int32_t"));
|
||||||
|
QVERIFY(spec.isPointer);
|
||||||
|
QCOMPARE(spec.ptrDepth, 1);
|
||||||
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecPrimitiveDoubleStar() {
|
||||||
|
TypeSpec spec = parseTypeSpec("f64**");
|
||||||
|
QCOMPARE(spec.baseName, QString("f64"));
|
||||||
|
QVERIFY(spec.isPointer);
|
||||||
|
QCOMPARE(spec.ptrDepth, 2);
|
||||||
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Primitive pointer creation via applyTypePopupResult path ──
|
||||||
|
|
||||||
|
void testPrimitivePointerCreation() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Find the "x" field (Int32) inside Alpha
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
// Simulate the primitive-pointer path: Int32 → Pointer64 + elementKind=Int32 + ptrDepth=1
|
||||||
|
doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
|
||||||
|
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
doc->tree.nodes[idx].elementKind = NodeKind::Int32;
|
||||||
|
doc->tree.nodes[idx].ptrDepth = 1;
|
||||||
|
doc->undoStack.endMacro();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Verify: Pointer64 with elementKind=Int32, ptrDepth=1, refId=0
|
||||||
|
idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
// Undo reverses the macro
|
||||||
|
doc->undoStack.undo();
|
||||||
|
QApplication::processEvents();
|
||||||
|
idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Int32);
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDoublePointerCreation() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Find the "x" field (Int32) inside Alpha
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
// Simulate: Int32 → Pointer64 + elementKind=Double + ptrDepth=2
|
||||||
|
doc->undoStack.beginMacro(QStringLiteral("Change to double pointer"));
|
||||||
|
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
doc->tree.nodes[idx].elementKind = NodeKind::Double;
|
||||||
|
doc->tree.nodes[idx].ptrDepth = 2;
|
||||||
|
doc->undoStack.endMacro();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Verify: Pointer64 with elementKind=Double, ptrDepth=2
|
||||||
|
idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ptrDepth JSON round-trip ──
|
||||||
|
|
||||||
|
void testPtrDepthJsonRoundTrip() {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Pointer64;
|
||||||
|
n.name = "pData";
|
||||||
|
n.elementKind = NodeKind::Float;
|
||||||
|
n.ptrDepth = 2;
|
||||||
|
n.id = 42;
|
||||||
|
|
||||||
|
QJsonObject obj = n.toJson();
|
||||||
|
QCOMPARE(obj["ptrDepth"].toInt(), 2);
|
||||||
|
|
||||||
|
Node restored = Node::fromJson(obj);
|
||||||
|
QCOMPARE(restored.ptrDepth, 2);
|
||||||
|
QCOMPARE(restored.elementKind, NodeKind::Float);
|
||||||
|
QCOMPARE(restored.kind, NodeKind::Pointer64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPtrDepthJsonDefault() {
|
||||||
|
// Nodes without ptrDepth in JSON should default to 0
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Pointer64;
|
||||||
|
n.name = "pVoid";
|
||||||
|
n.id = 99;
|
||||||
|
|
||||||
|
QJsonObject obj = n.toJson();
|
||||||
|
// ptrDepth==0 is not serialized
|
||||||
|
QVERIFY(!obj.contains("ptrDepth"));
|
||||||
|
|
||||||
|
Node restored = Node::fromJson(obj);
|
||||||
|
QCOMPARE(restored.ptrDepth, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setMode always resets modifier buttons ──
|
||||||
|
|
||||||
|
void testSetModeResetsModifierInPointerTargetMode() {
|
||||||
|
TypeSelectorPopup popup;
|
||||||
|
|
||||||
|
// Set FieldType mode and select * modifier
|
||||||
|
popup.setMode(TypePopupMode::FieldType);
|
||||||
|
popup.setModifier(1); // select *
|
||||||
|
|
||||||
|
// Now switch to PointerTarget mode — should reset to plain
|
||||||
|
popup.setMode(TypePopupMode::PointerTarget);
|
||||||
|
|
||||||
|
// Verify: modifier buttons are hidden but internally reset to plain (modId=0)
|
||||||
|
// This means primitives will be visible in applyFilter
|
||||||
|
TypeEntry prim;
|
||||||
|
prim.entryKind = TypeEntry::Primitive;
|
||||||
|
prim.primitiveKind = NodeKind::Int32;
|
||||||
|
prim.displayName = "int32_t";
|
||||||
|
|
||||||
|
TypeEntry voidEntry;
|
||||||
|
voidEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
voidEntry.primitiveKind = NodeKind::Pointer64;
|
||||||
|
voidEntry.displayName = "void";
|
||||||
|
|
||||||
|
popup.setTypes({prim, voidEntry});
|
||||||
|
|
||||||
|
// Both primitives should be visible (not filtered out)
|
||||||
|
auto* listView = popup.findChild<QListView*>();
|
||||||
|
QVERIFY(listView);
|
||||||
|
int rowCount = listView->model()->rowCount();
|
||||||
|
// Should have section header + 2 primitives = at least 3 rows
|
||||||
|
QVERIFY2(rowCount >= 3,
|
||||||
|
qPrintable(QString("Expected >=3 rows (header+2 prims), got %1").arg(rowCount)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setModifier preselection ──
|
||||||
|
|
||||||
|
void testSetModifierPreselects() {
|
||||||
|
TypeSelectorPopup popup;
|
||||||
|
|
||||||
|
// Test * preselection
|
||||||
|
popup.setMode(TypePopupMode::FieldType);
|
||||||
|
popup.setModifier(1);
|
||||||
|
auto* btnGroup = popup.findChild<QButtonGroup*>();
|
||||||
|
QVERIFY(btnGroup);
|
||||||
|
QCOMPARE(btnGroup->checkedId(), 1);
|
||||||
|
|
||||||
|
// Test ** preselection
|
||||||
|
popup.setMode(TypePopupMode::FieldType);
|
||||||
|
popup.setModifier(2);
|
||||||
|
QCOMPARE(btnGroup->checkedId(), 2);
|
||||||
|
|
||||||
|
// Test [n] preselection with count
|
||||||
|
popup.setMode(TypePopupMode::FieldType);
|
||||||
|
popup.setModifier(3, 8);
|
||||||
|
QCOMPARE(btnGroup->checkedId(), 3);
|
||||||
|
auto* countEdit = popup.findChild<QLineEdit*>(QStringLiteral("arrayCountEdit"));
|
||||||
|
// Array count edit may not have objectName set; find via parent
|
||||||
|
// Just verify button group is correct
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── isValidPrimitivePtrTarget ──
|
||||||
|
|
||||||
|
void testIsValidPrimitivePtrTarget() {
|
||||||
|
// Hex types → NOT valid (deref shows same hex as void*)
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex8));
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex16));
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex32));
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex64));
|
||||||
|
|
||||||
|
// Pointer types → NOT valid (use composite * for chains)
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer32));
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer64));
|
||||||
|
|
||||||
|
// Function pointers → NOT valid
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr32));
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr64));
|
||||||
|
|
||||||
|
// Containers → NOT valid
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Struct));
|
||||||
|
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Array));
|
||||||
|
|
||||||
|
// Value types → valid
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Int32));
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UInt64));
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Float));
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Double));
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Bool));
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Vec3));
|
||||||
|
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UTF8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── hex64* falls back to void* ──
|
||||||
|
|
||||||
|
void testHex64StarFallsBackToVoidPointer() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Find the "x" field (Int32)
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
// Build a TypeEntry for hex64
|
||||||
|
TypeEntry hexEntry;
|
||||||
|
hexEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
hexEntry.primitiveKind = NodeKind::Hex64;
|
||||||
|
hexEntry.displayName = "hex64";
|
||||||
|
|
||||||
|
// Apply it with pointer modifier (fullText = "hex64*")
|
||||||
|
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||||
|
hexEntry, QStringLiteral("hex64*"));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Should be a void pointer: Pointer64, ptrDepth=0, refId=0
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testHex8StarFallsBackToVoidPointer() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
TypeEntry hexEntry;
|
||||||
|
hexEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
hexEntry.primitiveKind = NodeKind::Hex8;
|
||||||
|
hexEntry.displayName = "hex8";
|
||||||
|
|
||||||
|
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||||
|
hexEntry, QStringLiteral("hex8*"));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPtr64StarFallsBackToVoidPointer() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
TypeEntry ptrEntry;
|
||||||
|
ptrEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
ptrEntry.primitiveKind = NodeKind::Pointer64;
|
||||||
|
ptrEntry.displayName = "ptr64";
|
||||||
|
|
||||||
|
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||||
|
ptrEntry, QStringLiteral("ptr64*"));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Valid primitive pointers still work ──
|
||||||
|
|
||||||
|
void testInt32StarStillCreatesPrimitivePointer() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
TypeEntry intEntry;
|
||||||
|
intEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
intEntry.primitiveKind = NodeKind::Int32;
|
||||||
|
intEntry.displayName = "int32_t";
|
||||||
|
|
||||||
|
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||||
|
intEntry, QStringLiteral("int32_t*"));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testDoubleDoubleStarStillCreatesPrimitivePointer() {
|
||||||
|
auto* doc = new RcxDocument();
|
||||||
|
buildTwoRootTree(doc->tree);
|
||||||
|
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter();
|
||||||
|
auto* ctrl = new RcxController(doc, nullptr);
|
||||||
|
ctrl->addSplitEditor(splitter);
|
||||||
|
|
||||||
|
splitter->resize(800, 600);
|
||||||
|
splitter->show();
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||||
|
ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int xIdx = -1;
|
||||||
|
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||||
|
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(xIdx >= 0);
|
||||||
|
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||||
|
|
||||||
|
TypeEntry dblEntry;
|
||||||
|
dblEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
dblEntry.primitiveKind = NodeKind::Double;
|
||||||
|
dblEntry.displayName = "double";
|
||||||
|
|
||||||
|
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||||
|
dblEntry, QStringLiteral("double**"));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
int idx = doc->tree.indexOfId(xNodeId);
|
||||||
|
QVERIFY(idx >= 0);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
|
||||||
|
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||||
|
|
||||||
|
delete ctrl;
|
||||||
|
delete splitter;
|
||||||
|
delete doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Defense: compose/format treat invalid ptrDepth as void* ──
|
||||||
|
|
||||||
|
void testComposeShowsVoidPtrForHexPtrDepth() {
|
||||||
|
// If a node somehow has ptrDepth>0 with hex elementKind
|
||||||
|
// (e.g. from old JSON), compose should show "void*" not "hex64*"
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0x1000;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.name = "Test";
|
||||||
|
root.structTypeName = "Test";
|
||||||
|
root.parentId = 0;
|
||||||
|
tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[0].id;
|
||||||
|
|
||||||
|
Node ptr;
|
||||||
|
ptr.kind = NodeKind::Pointer64;
|
||||||
|
ptr.name = "badPtr";
|
||||||
|
ptr.parentId = rootId;
|
||||||
|
ptr.offset = 0;
|
||||||
|
ptr.ptrDepth = 1;
|
||||||
|
ptr.elementKind = NodeKind::Hex64; // invalid target
|
||||||
|
tree.addNode(ptr);
|
||||||
|
|
||||||
|
QByteArray buf(0x100, '\0');
|
||||||
|
BufferProvider prov(buf);
|
||||||
|
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
|
// The composed text should NOT contain "hex64*" — the invalid target
|
||||||
|
// should fall through to normal void pointer display
|
||||||
|
QVERIFY2(!result.text.contains("hex64*"),
|
||||||
|
qPrintable("Should not show 'hex64*', got: " + result.text));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestTypeSelector)
|
QTEST_MAIN(TestTypeSelector)
|
||||||
|
|||||||
Reference in New Issue
Block a user