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:
IChooseYou
2026-02-20 07:21:02 -07:00
parent c7afe363f3
commit 0e087fa3a4
10 changed files with 797 additions and 113 deletions

View File

@@ -119,8 +119,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString ptrTypeOverride;
QString ptrTargetName;
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
// 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++) {

View File

@@ -223,8 +223,17 @@ void RcxController::connectEditor(RcxEditor* editor) {
TypePopupMode mode = TypePopupMode::FieldType;
if (target == EditTarget::ArrayElementType)
mode = TypePopupMode::ArrayElement;
else if (target == EditTarget::PointerTarget)
mode = TypePopupMode::PointerTarget;
else if (target == EditTarget::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);
});
@@ -1855,6 +1864,8 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
QVector<TypeEntry> entries;
TypeEntry currentEntry;
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) {
for (const auto& m : kKindMeta) {
@@ -1894,10 +1905,43 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
});
break;
case TypePopupMode::FieldType:
case TypePopupMode::FieldType: {
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
if (node) {
// Mark current primitive
bool isPtr = node
&& (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) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
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;
}
case TypePopupMode::ArrayElement:
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
@@ -1994,6 +2044,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
popup->setFont(font);
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
int nodeSize = 0;
if (node) {
@@ -2107,6 +2161,44 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
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 {
if (resolved.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, resolved.primitiveKind);

View File

@@ -142,6 +142,15 @@ inline constexpr bool isMatrixKind(NodeKind k) {
inline constexpr bool isFuncPtr(NodeKind k) {
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) {
QStringList out;
@@ -184,7 +193,8 @@ struct Node {
int strLen = 64;
bool collapsed = false;
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)
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
@@ -217,6 +227,8 @@ struct Node {
o["collapsed"] = collapsed;
o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
return o;
}
static Node fromJson(const QJsonObject& o) {
@@ -233,6 +245,7 @@ struct Node {
n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
return n;
}

View File

@@ -1789,15 +1789,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
// Single-click on editable token of already-selected node → edit
int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
// Type/ArrayElementType/PointerTarget open a dismissible popup
// (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());
if (alreadySelected && plain) {
m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine, tCol);
}

View File

@@ -267,6 +267,30 @@ static QString readValueImpl(const Node& node, const Provider& prov,
}
case NodeKind::Pointer64: {
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);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);

View File

@@ -410,7 +410,7 @@ void MainWindow::createMenus() {
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);
file->addSeparator();
m_sourceMenu = file->addMenu("So&urce");
m_sourceMenu = file->addMenu("Current Tab So&urce");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
file->addSeparator();
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) ──
// 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 {
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) {
setFixedSize(16, 16);
setFixedSize(kSize, kSize);
setCursor(Qt::SizeFDiagCursor);
m_color = rcx::ThemeManager::instance().current().textFaint;
}
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:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
@@ -514,8 +526,11 @@ protected:
p.setPen(Qt::NoPen);
p.setBrush(m_color);
// 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;
double bx = width() - 5, by = height() - 4;
const double inset = 4.0;
double bx = width() - inset;
double by = height() - inset;
// bottom row: 3 dots
p.drawEllipse(QPointF(bx, by), r, r);
p.drawEllipse(QPointF(bx - s, by), r, r);
@@ -539,13 +554,15 @@ private:
void MainWindow::createStatusBar() {
m_statusLabel = new QLabel("Ready");
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()->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);
grip->setObjectName("resizeGrip");
statusBar()->addPermanentWidget(grip);
grip->raise();
{
const auto& t = ThemeManager::instance().current();
@@ -1116,15 +1133,16 @@ void MainWindow::applyTheme(const Theme& theme) {
// Re-style ✕ close buttons on MDI tabs
styleTabCloseButtons();
// Status bar + resize grip
// Status bar
{
QPalette sbPal = statusBar()->palette();
sbPal.setColor(QPalette::Window, theme.background);
sbPal.setColor(QPalette::WindowText, theme.textDim);
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
if (m_workspaceTree) {
@@ -2060,6 +2078,11 @@ void MainWindow::resizeEvent(QResizeEvent* event) {
m_borderOverlay->setGeometry(rect());
m_borderOverlay->raise();
}
auto* grip = findChild<ResizeGrip*>("resizeGrip");
if (grip) {
grip->reposition();
grip->raise();
}
}
void MainWindow::updateBorderColor(const QColor& color) {

View File

@@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
if (s.endsWith('*')) {
spec.isPointer = true;
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();
return spec;
}
@@ -347,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
m_arrayCountEdit->selectAll();
}
updateModifierPreview();
applyFilter(m_filterEdit->text());
});
connect(m_arrayCountEdit, &QLineEdit::textChanged,
this, [this]() { updateModifierPreview(); });
@@ -516,22 +516,32 @@ void TypeSelectorPopup::setTitle(const QString& title) {
void TypeSelectorPopup::setMode(TypePopupMode mode) {
m_mode = mode;
// Show modifier toggles for modes where type modifiers make sense
bool showMods = (mode == TypePopupMode::FieldType
|| mode == TypePopupMode::ArrayElement);
m_modRow->setVisible(showMods);
// Reset to plain when showing
if (showMods) {
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
// Always reset to plain — prevents stale state from leaking across modes
// (PointerTarget hides buttons but applyFilter still reads their state)
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
void TypeSelectorPopup::setCurrentNodeSize(int 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) {
m_allTypes = types;
if (current) {
@@ -541,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
m_currentEntry = TypeEntry{};
m_hasCurrent = false;
}
// Reset modifier toggles
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
// Don't reset modifier buttons here — setMode() already resets to plain,
// and setModifier() may have preselected a button between setMode/setTypes.
m_previewLabel->hide();
m_filterEdit->clear();
@@ -630,23 +638,18 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
QString filterBase = text.trimmed();
// Hide primitives when a pointer modifier (* or **) is active
int modId = m_modGroup->checkedId();
bool hideprimitives = (modId == 1 || modId == 2);
// Separate primitives and composites
// Separate primitives and composites (all types shown regardless of modifier)
QVector<TypeEntry> primitives, composites;
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()
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
if (!matchesFilter) continue;
if (t.entryKind == TypeEntry::Primitive) {
if (!hideprimitives)
primitives.append(t);
} else if (t.entryKind == TypeEntry::Composite)
if (t.entryKind == TypeEntry::Primitive)
primitives.append(t);
else if (t.entryKind == TypeEntry::Composite)
composites.append(t);
}

View File

@@ -40,6 +40,7 @@ struct TypeEntry {
struct TypeSpec {
QString baseName;
bool isPointer = false;
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
int arrayCount = 0; // 0 = not array
};
@@ -57,6 +58,7 @@ public:
void setMode(TypePopupMode mode);
void applyTheme(const Theme& theme);
void setCurrentNodeSize(int bytes);
void setModifier(int modId, int arrayCount = 0);
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
void popup(const QPoint& globalPos);