mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Unified type popup: explicit TypeEntry model, modifier toggles, section headers
- Replace sentinel-id scheme (kPrimBase) with TypeEntry::Kind discriminant - Merge showTypeSelectorPopup + showTypePickerPopup into single showTypePopup - Merge applyTypePickerResult into applyTypePopupResult matching on entryKind - Add modifier toggle buttons (plain, *, **, [n]) with array count input - Add section headers (primitives / project types) with dim centered styling - Root mode shows project types first; non-Root sorts same-size primitives first - Remove popup outside border, flat "Create new type" button - Add parseTypeSpec parser, update tests with new TypeEntry API
This commit is contained in:
@@ -203,13 +203,18 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
// Type selector popup (command row chevron)
|
// Type selector popup (command row chevron)
|
||||||
connect(editor, &RcxEditor::typeSelectorRequested,
|
connect(editor, &RcxEditor::typeSelectorRequested,
|
||||||
this, [this, editor]() {
|
this, [this, editor]() {
|
||||||
showTypeSelectorPopup(editor);
|
showTypePopup(editor, TypePopupMode::Root, -1, QPoint());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type picker popup (array element type / pointer target)
|
// Type picker popup (array element type / pointer target)
|
||||||
connect(editor, &RcxEditor::typePickerRequested,
|
connect(editor, &RcxEditor::typePickerRequested,
|
||||||
this, [this, editor](EditTarget target, int nodeIdx, QPoint globalPos) {
|
this, [this, editor](EditTarget target, int nodeIdx, QPoint globalPos) {
|
||||||
showTypePickerPopup(editor, target, nodeIdx, globalPos);
|
TypePopupMode mode = TypePopupMode::FieldType;
|
||||||
|
if (target == EditTarget::ArrayElementType)
|
||||||
|
mode = TypePopupMode::ArrayElement;
|
||||||
|
else if (target == EditTarget::PointerTarget)
|
||||||
|
mode = TypePopupMode::PointerTarget;
|
||||||
|
showTypePopup(editor, mode, nodeIdx, globalPos);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inline editing signals
|
// Inline editing signals
|
||||||
@@ -1505,20 +1510,118 @@ void RcxController::updateCommandRow() {
|
|||||||
emit selectionChanged(m_selIds.size());
|
emit selectionChanged(m_selIds.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::showTypeSelectorPopup(RcxEditor* editor) {
|
TypeSelectorPopup* RcxController::ensurePopup(RcxEditor* editor) {
|
||||||
// Collect all root-level struct types
|
if (!m_cachedPopup) {
|
||||||
QVector<TypeEntry> types;
|
m_cachedPopup = new TypeSelectorPopup(editor);
|
||||||
for (const auto& n : m_doc->tree.nodes) {
|
// Pre-warm: force native window creation so first visible show is fast
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
m_cachedPopup->warmUp();
|
||||||
TypeEntry entry;
|
}
|
||||||
entry.id = n.id;
|
// Disconnect previous signals so we can reconnect fresh
|
||||||
entry.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
m_cachedPopup->disconnect(this);
|
||||||
entry.classKeyword = n.resolvedClassKeyword();
|
return m_cachedPopup;
|
||||||
types.append(entry);
|
}
|
||||||
|
|
||||||
|
void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||||
|
int nodeIdx, QPoint globalPos) {
|
||||||
|
const Node* node = nullptr;
|
||||||
|
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size())
|
||||||
|
node = &m_doc->tree.nodes[nodeIdx];
|
||||||
|
|
||||||
|
// ── Build entry list based on mode ──
|
||||||
|
QVector<TypeEntry> entries;
|
||||||
|
TypeEntry currentEntry;
|
||||||
|
bool hasCurrent = false;
|
||||||
|
|
||||||
|
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
||||||
|
for (const auto& m : kKindMeta) {
|
||||||
|
if (m.kind == NodeKind::Padding) continue;
|
||||||
|
if (excludeStructArrayPad &&
|
||||||
|
(m.kind == NodeKind::Struct || m.kind == NodeKind::Array))
|
||||||
|
continue;
|
||||||
|
TypeEntry e;
|
||||||
|
e.entryKind = TypeEntry::Primitive;
|
||||||
|
e.primitiveKind = m.kind;
|
||||||
|
e.displayName = QString::fromLatin1(m.typeName);
|
||||||
|
e.enabled = enabled;
|
||||||
|
entries.append(e);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addComposites = [&](const std::function<bool(const Node&, const TypeEntry&)>& isCurrent) {
|
||||||
|
for (const auto& n : m_doc->tree.nodes) {
|
||||||
|
if (n.parentId != 0 || n.kind != NodeKind::Struct) continue;
|
||||||
|
TypeEntry e;
|
||||||
|
e.entryKind = TypeEntry::Composite;
|
||||||
|
e.structId = n.id;
|
||||||
|
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||||
|
e.classKeyword = n.resolvedClassKeyword();
|
||||||
|
entries.append(e);
|
||||||
|
if (!hasCurrent && node && isCurrent(*node, e)) {
|
||||||
|
currentEntry = e;
|
||||||
|
hasCurrent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case TypePopupMode::Root:
|
||||||
|
addPrimitives(/*enabled=*/false, /*excludeStructArrayPad=*/false);
|
||||||
|
addComposites([&](const Node&, const TypeEntry& e) {
|
||||||
|
return e.structId == m_viewRootId;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypePopupMode::FieldType:
|
||||||
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
|
||||||
|
if (node) {
|
||||||
|
// Mark current primitive
|
||||||
|
for (auto& e : entries) {
|
||||||
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
|
||||||
|
currentEntry = e;
|
||||||
|
hasCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addComposites([](const Node&, const TypeEntry&) { return false; });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypePopupMode::ArrayElement:
|
||||||
|
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
|
||||||
|
if (node) {
|
||||||
|
for (auto& e : entries) {
|
||||||
|
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
|
||||||
|
currentEntry = e;
|
||||||
|
hasCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addComposites([](const Node& n, const TypeEntry& e) {
|
||||||
|
return n.elementKind == NodeKind::Struct && n.refId == e.structId;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TypePopupMode::PointerTarget: {
|
||||||
|
// "void" entry as a primitive with a special display
|
||||||
|
TypeEntry voidEntry;
|
||||||
|
voidEntry.entryKind = TypeEntry::Primitive;
|
||||||
|
voidEntry.primitiveKind = NodeKind::Hex8; // unused, but needs a value
|
||||||
|
voidEntry.displayName = QStringLiteral("void");
|
||||||
|
voidEntry.enabled = true;
|
||||||
|
entries.append(voidEntry);
|
||||||
|
if (node && node->refId == 0) {
|
||||||
|
currentEntry = voidEntry;
|
||||||
|
hasCurrent = true;
|
||||||
|
}
|
||||||
|
addComposites([](const Node& n, const TypeEntry& e) {
|
||||||
|
return n.refId == e.structId;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get font with zoom
|
// ── Font with zoom ──
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("ReclassX", "ReclassX");
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||||
QFont font(fontName, 12);
|
QFont font(fontName, 12);
|
||||||
@@ -1527,26 +1630,45 @@ void RcxController::showTypeSelectorPopup(RcxEditor* editor) {
|
|||||||
int zoom = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
int zoom = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
||||||
font.setPointSize(font.pointSize() + zoom);
|
font.setPointSize(font.pointSize() + zoom);
|
||||||
|
|
||||||
// Position: bottom-left of the [▸] span on line 0
|
// ── Position ──
|
||||||
long lineStart = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
QPoint pos = globalPos;
|
||||||
int lineH = (int)sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
if (mode == TypePopupMode::Root) {
|
||||||
int x = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
// Bottom-left of the [▸] span on line 0
|
||||||
0, lineStart);
|
long lineStart = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
||||||
int y = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
int lineH = (int)sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
||||||
0, lineStart);
|
int x = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||||
QPoint pos = sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
0, lineStart);
|
||||||
|
int y = (int)sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||||
|
0, lineStart);
|
||||||
|
pos = sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
||||||
|
}
|
||||||
|
|
||||||
auto* popup = new TypeSelectorPopup(editor);
|
// ── Configure and show popup ──
|
||||||
|
auto* popup = ensurePopup(editor);
|
||||||
popup->setFont(font);
|
popup->setFont(font);
|
||||||
popup->setTypes(types, m_viewRootId);
|
popup->setMode(mode);
|
||||||
|
|
||||||
|
// Pass current node size for same-size sorting
|
||||||
|
int nodeSize = 0;
|
||||||
|
if (node) {
|
||||||
|
if (mode == TypePopupMode::ArrayElement)
|
||||||
|
nodeSize = sizeForKind(node->elementKind);
|
||||||
|
else
|
||||||
|
nodeSize = sizeForKind(node->kind);
|
||||||
|
}
|
||||||
|
popup->setCurrentNodeSize(nodeSize);
|
||||||
|
|
||||||
|
static const char* titles[] = { "Change root", "Change type",
|
||||||
|
"Element type", "Pointer target" };
|
||||||
|
popup->setTitle(QString::fromLatin1(titles[(int)mode]));
|
||||||
|
popup->setTypes(entries, hasCurrent ? ¤tEntry : nullptr);
|
||||||
|
|
||||||
connect(popup, &TypeSelectorPopup::typeSelected,
|
connect(popup, &TypeSelectorPopup::typeSelected,
|
||||||
this, [this](uint64_t structId, const QString&) {
|
this, [this, mode, nodeIdx](const TypeEntry& entry, const QString& fullText) {
|
||||||
setViewRootId(structId);
|
applyTypePopupResult(mode, nodeIdx, entry, fullText);
|
||||||
});
|
});
|
||||||
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
||||||
this, [this]() {
|
this, [this, mode, nodeIdx]() {
|
||||||
// Create a new root struct with no name
|
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Struct;
|
n.kind = NodeKind::Struct;
|
||||||
n.name = QString();
|
n.name = QString();
|
||||||
@@ -1554,144 +1676,57 @@ void RcxController::showTypeSelectorPopup(RcxEditor* editor) {
|
|||||||
n.offset = 0;
|
n.offset = 0;
|
||||||
n.id = m_doc->tree.reserveId();
|
n.id = m_doc->tree.reserveId();
|
||||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
||||||
setViewRootId(n.id);
|
TypeEntry newEntry;
|
||||||
|
newEntry.entryKind = TypeEntry::Composite;
|
||||||
|
newEntry.structId = n.id;
|
||||||
|
applyTypePopupResult(mode, nodeIdx, newEntry, QString());
|
||||||
});
|
});
|
||||||
connect(popup, &TypeSelectorPopup::dismissed,
|
|
||||||
popup, &QObject::deleteLater);
|
|
||||||
|
|
||||||
popup->popup(pos);
|
popup->popup(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::showTypePickerPopup(RcxEditor* editor, EditTarget target,
|
void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
||||||
int nodeIdx, QPoint globalPos) {
|
const TypeEntry& entry, const QString& fullText) {
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (mode == TypePopupMode::Root) {
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
if (entry.entryKind == TypeEntry::Composite)
|
||||||
|
setViewRootId(entry.structId);
|
||||||
QVector<TypeEntry> entries;
|
return;
|
||||||
uint64_t currentId = 0;
|
|
||||||
|
|
||||||
// Sentinel range for primitive entries: UINT64_MAX - kind
|
|
||||||
constexpr uint64_t kPrimBase = UINT64_MAX - 256;
|
|
||||||
constexpr uint64_t kNoSelection = UINT64_MAX;
|
|
||||||
currentId = kNoSelection;
|
|
||||||
|
|
||||||
if (target == EditTarget::ArrayElementType) {
|
|
||||||
// Primitive types (unique synthetic id per kind)
|
|
||||||
for (const auto& m : kKindMeta) {
|
|
||||||
if (m.kind == NodeKind::Struct || m.kind == NodeKind::Array
|
|
||||||
|| m.kind == NodeKind::Padding) continue;
|
|
||||||
TypeEntry e;
|
|
||||||
e.id = kPrimBase - (uint64_t)m.kind;
|
|
||||||
e.displayName = QString::fromLatin1(m.typeName);
|
|
||||||
entries.append(e);
|
|
||||||
if (m.kind == node.elementKind)
|
|
||||||
currentId = e.id;
|
|
||||||
}
|
|
||||||
// Struct types
|
|
||||||
for (const auto& n : m_doc->tree.nodes) {
|
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
||||||
TypeEntry e;
|
|
||||||
e.id = n.id;
|
|
||||||
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
|
||||||
e.classKeyword = n.resolvedClassKeyword();
|
|
||||||
entries.append(e);
|
|
||||||
if (node.elementKind == NodeKind::Struct && n.id == node.refId)
|
|
||||||
currentId = n.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (target == EditTarget::PointerTarget) {
|
|
||||||
// "void" entry
|
|
||||||
{
|
|
||||||
TypeEntry e;
|
|
||||||
e.id = kPrimBase; // unique sentinel for void
|
|
||||||
e.displayName = QStringLiteral("void");
|
|
||||||
entries.append(e);
|
|
||||||
if (node.refId == 0) currentId = e.id;
|
|
||||||
}
|
|
||||||
// Struct types
|
|
||||||
for (const auto& n : m_doc->tree.nodes) {
|
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
||||||
TypeEntry e;
|
|
||||||
e.id = n.id;
|
|
||||||
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
|
||||||
e.classKeyword = n.resolvedClassKeyword();
|
|
||||||
entries.append(e);
|
|
||||||
if (n.id == node.refId) currentId = n.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Font with zoom
|
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
|
||||||
QFont font(fontName, 12);
|
|
||||||
font.setFixedPitch(true);
|
|
||||||
auto* sci = editor->scintilla();
|
|
||||||
int zoom = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
|
|
||||||
font.setPointSize(font.pointSize() + zoom);
|
|
||||||
|
|
||||||
auto* popup = new TypeSelectorPopup(editor);
|
|
||||||
popup->setFont(font);
|
|
||||||
popup->setTitle(target == EditTarget::ArrayElementType
|
|
||||||
? QStringLiteral("Choose element type")
|
|
||||||
: QStringLiteral("Choose pointer target"));
|
|
||||||
popup->setTypes(entries, currentId);
|
|
||||||
|
|
||||||
connect(popup, &TypeSelectorPopup::typeSelected,
|
|
||||||
this, [this, target, nodeIdx](uint64_t id, const QString& displayName) {
|
|
||||||
applyTypePickerResult(target, nodeIdx, id, displayName);
|
|
||||||
});
|
|
||||||
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
|
||||||
this, [this, target, nodeIdx]() {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Struct;
|
|
||||||
n.name = QString();
|
|
||||||
n.parentId = 0;
|
|
||||||
n.offset = 0;
|
|
||||||
n.id = m_doc->tree.reserveId();
|
|
||||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
|
||||||
applyTypePickerResult(target, nodeIdx, n.id, QString());
|
|
||||||
});
|
|
||||||
connect(popup, &TypeSelectorPopup::dismissed,
|
|
||||||
popup, &QObject::deleteLater);
|
|
||||||
|
|
||||||
popup->popup(globalPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RcxController::applyTypePickerResult(EditTarget target, int nodeIdx,
|
|
||||||
uint64_t selectedId, const QString& displayName) {
|
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
|
|
||||||
constexpr uint64_t kPrimBase = UINT64_MAX - 256;
|
// Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*")
|
||||||
|
TypeSpec spec = parseTypeSpec(fullText);
|
||||||
|
|
||||||
if (target == EditTarget::ArrayElementType) {
|
if (mode == TypePopupMode::FieldType) {
|
||||||
if (selectedId >= kPrimBase) {
|
if (entry.entryKind == TypeEntry::Primitive) {
|
||||||
// Primitive type — resolve from displayName
|
if (entry.primitiveKind != node.kind)
|
||||||
bool ok;
|
changeNodeKind(nodeIdx, entry.primitiveKind);
|
||||||
NodeKind elemKind = kindFromTypeName(displayName, &ok);
|
}
|
||||||
if (ok && elemKind != node.elementKind) {
|
} else if (mode == TypePopupMode::ArrayElement) {
|
||||||
|
if (entry.entryKind == TypeEntry::Primitive) {
|
||||||
|
if (entry.primitiveKind != node.elementKind) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeArrayMeta{node.id,
|
cmd::ChangeArrayMeta{node.id,
|
||||||
node.elementKind, elemKind,
|
node.elementKind, entry.primitiveKind,
|
||||||
node.arrayLen, node.arrayLen}));
|
node.arrayLen, node.arrayLen}));
|
||||||
}
|
}
|
||||||
} else {
|
} else if (entry.entryKind == TypeEntry::Composite) {
|
||||||
// Struct type — real node id
|
if (node.elementKind != NodeKind::Struct || node.refId != entry.structId) {
|
||||||
if (node.elementKind != NodeKind::Struct || node.refId != selectedId) {
|
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeArrayMeta{node.id,
|
cmd::ChangeArrayMeta{node.id,
|
||||||
node.elementKind, NodeKind::Struct,
|
node.elementKind, NodeKind::Struct,
|
||||||
node.arrayLen, node.arrayLen}));
|
node.arrayLen, node.arrayLen}));
|
||||||
if (node.refId != selectedId) {
|
if (node.refId != entry.structId) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, node.refId, selectedId}));
|
cmd::ChangePointerRef{node.id, node.refId, entry.structId}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (target == EditTarget::PointerTarget) {
|
} else if (mode == TypePopupMode::PointerTarget) {
|
||||||
// Map void sentinel back to refId 0
|
// "void" entry → refId 0; composite entry → real structId
|
||||||
uint64_t realRefId = (selectedId >= kPrimBase) ? 0 : selectedId;
|
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0;
|
||||||
if (realRefId != node.refId) {
|
if (realRefId != node.refId) {
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangePointerRef{node.id, node.refId, realRefId}));
|
cmd::ChangePointerRef{node.id, node.refId, realRefId}));
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
class RcxController;
|
class RcxController;
|
||||||
|
class TypeSelectorPopup;
|
||||||
|
struct TypeEntry;
|
||||||
|
enum class TypePopupMode;
|
||||||
|
|
||||||
// ── Document ──
|
// ── Document ──
|
||||||
|
|
||||||
@@ -133,6 +136,9 @@ private:
|
|||||||
QVector<SavedSourceEntry> m_savedSources;
|
QVector<SavedSourceEntry> m_savedSources;
|
||||||
int m_activeSourceIdx = -1;
|
int m_activeSourceIdx = -1;
|
||||||
|
|
||||||
|
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||||
|
TypeSelectorPopup* m_cachedPopup = nullptr;
|
||||||
|
|
||||||
// ── Auto-refresh state ──
|
// ── Auto-refresh state ──
|
||||||
QTimer* m_refreshTimer = nullptr;
|
QTimer* m_refreshTimer = nullptr;
|
||||||
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
||||||
@@ -149,9 +155,9 @@ private:
|
|||||||
void performRealignment(uint64_t structId, int targetAlign);
|
void performRealignment(uint64_t structId, int targetAlign);
|
||||||
void switchToSavedSource(int idx);
|
void switchToSavedSource(int idx);
|
||||||
void pushSavedSourcesToEditors();
|
void pushSavedSourcesToEditors();
|
||||||
void showTypeSelectorPopup(RcxEditor* editor);
|
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
|
||||||
void showTypePickerPopup(RcxEditor* editor, EditTarget target, int nodeIdx, QPoint globalPos);
|
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
|
||||||
void applyTypePickerResult(EditTarget target, int nodeIdx, uint64_t selectedId, const QString& displayName);
|
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
|
||||||
|
|
||||||
// ── Auto-refresh methods ──
|
// ── Auto-refresh methods ──
|
||||||
void setupAutoRefresh();
|
void setupAutoRefresh();
|
||||||
|
|||||||
@@ -97,10 +97,6 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
if (m_updatingComment) return; // Skip queuing during comment update
|
if (m_updatingComment) return; // Skip queuing during comment update
|
||||||
if (m_editState.target == EditTarget::Value)
|
if (m_editState.target == EditTarget::Value)
|
||||||
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
||||||
if (m_editState.target == EditTarget::Type || m_editState.target == EditTarget::ArrayElementType)
|
|
||||||
QTimer::singleShot(0, this, &RcxEditor::updateTypeListFilter);
|
|
||||||
if (m_editState.target == EditTarget::PointerTarget)
|
|
||||||
QTimer::singleShot(0, this, &RcxEditor::updatePointerTargetFilter);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_sci, &QsciScintilla::selectionChanged,
|
connect(m_sci, &QsciScintilla::selectionChanged,
|
||||||
@@ -1473,18 +1469,20 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||||
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
|
// Type, array element type and pointer target: handled by TypeSelectorPopup, not inline edit
|
||||||
if (target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) {
|
if (target == EditTarget::Type || target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) {
|
||||||
if (line < 0) {
|
if (line < 0) {
|
||||||
int c;
|
int c;
|
||||||
m_sci->getCursorPosition(&line, &c);
|
m_sci->getCursorPosition(&line, &c);
|
||||||
}
|
}
|
||||||
auto* lm = metaForLine(line);
|
auto* lm = metaForLine(line);
|
||||||
if (!lm) return false;
|
if (!lm) return false;
|
||||||
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
|
// Position popup at the type column start
|
||||||
|
ColumnSpan ts = typeSpan(*lm);
|
||||||
|
long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0);
|
||||||
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, (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 x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, (unsigned long)0, typePos);
|
||||||
int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, (unsigned long)0, lineStart);
|
int y = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION, (unsigned long)0, typePos);
|
||||||
QPoint pos = m_sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
QPoint pos = m_sci->viewport()->mapToGlobal(QPoint(x, y + lineH));
|
||||||
emit typePickerRequested(target, lm->nodeIdx, pos);
|
emit typePickerRequested(target, lm->nodeIdx, pos);
|
||||||
return true;
|
return true;
|
||||||
@@ -1657,12 +1655,10 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
if (target == EditTarget::Value)
|
if (target == EditTarget::Value)
|
||||||
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
||||||
|
|
||||||
if (target == EditTarget::Type || target == EditTarget::ArrayElementType)
|
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
|
||||||
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
|
// and exit early above (never reach here).
|
||||||
if (target == EditTarget::Source)
|
if (target == EditTarget::Source)
|
||||||
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
|
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
|
||||||
if (target == EditTarget::PointerTarget)
|
|
||||||
QTimer::singleShot(0, this, &RcxEditor::showPointerTargetPicker);
|
|
||||||
if (target == EditTarget::RootClassType) {
|
if (target == EditTarget::RootClassType) {
|
||||||
QTimer::singleShot(0, this, [this]() {
|
QTimer::singleShot(0, this, [this]() {
|
||||||
if (!m_editState.active || m_editState.target != EditTarget::RootClassType) return;
|
if (!m_editState.active || m_editState.target != EditTarget::RootClassType) return;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QListView>
|
#include <QListView>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
#include <QButtonGroup>
|
||||||
#include <QStringListModel>
|
#include <QStringListModel>
|
||||||
#include <QStyledItemDelegate>
|
#include <QStyledItemDelegate>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
@@ -14,11 +15,44 @@
|
|||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
#include <QIntValidator>
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
// ── Custom delegate: gutter checkmark + icon + text ──
|
// ── parseTypeSpec ──
|
||||||
|
|
||||||
|
TypeSpec parseTypeSpec(const QString& text) {
|
||||||
|
TypeSpec spec;
|
||||||
|
QString s = text.trimmed();
|
||||||
|
if (s.isEmpty()) return spec;
|
||||||
|
|
||||||
|
// Check for pointer suffix: "Ball*" or "Ball**"
|
||||||
|
if (s.endsWith('*')) {
|
||||||
|
spec.isPointer = true;
|
||||||
|
s.chop(1);
|
||||||
|
if (s.endsWith('*')) s.chop(1); // double pointer
|
||||||
|
spec.baseName = s.trimmed();
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for array suffix: "int32_t[10]"
|
||||||
|
int bracket = s.indexOf('[');
|
||||||
|
if (bracket > 0 && s.endsWith(']')) {
|
||||||
|
spec.baseName = s.left(bracket).trimmed();
|
||||||
|
QString countStr = s.mid(bracket + 1, s.size() - bracket - 2);
|
||||||
|
bool ok;
|
||||||
|
int count = countStr.toInt(&ok);
|
||||||
|
if (ok && count > 0)
|
||||||
|
spec.arrayCount = count;
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.baseName = s;
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Custom delegate: gutter checkmark + icon + text + sections ──
|
||||||
|
|
||||||
class TypeSelectorDelegate : public QStyledItemDelegate {
|
class TypeSelectorDelegate : public QStyledItemDelegate {
|
||||||
public:
|
public:
|
||||||
@@ -26,51 +60,115 @@ public:
|
|||||||
: QStyledItemDelegate(parent), m_popup(popup) {}
|
: QStyledItemDelegate(parent), m_popup(popup) {}
|
||||||
|
|
||||||
void setFont(const QFont& f) { m_font = f; }
|
void setFont(const QFont& f) { m_font = f; }
|
||||||
void setCurrentTypes(const QVector<TypeEntry>* filtered, uint64_t currentId) {
|
void setFilteredTypes(const QVector<TypeEntry>* filtered, const TypeEntry* current, bool hasCurrent) {
|
||||||
m_filtered = filtered;
|
m_filtered = filtered;
|
||||||
m_currentId = currentId;
|
m_current = current;
|
||||||
|
m_hasCurrent = hasCurrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||||
const QModelIndex& index) const override {
|
const QModelIndex& index) const override {
|
||||||
painter->save();
|
painter->save();
|
||||||
|
|
||||||
// Background: themed colors
|
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
if (option.state & QStyle::State_Selected)
|
int row = index.row();
|
||||||
painter->fillRect(option.rect, t.selected);
|
bool isSection = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||||
else if (option.state & QStyle::State_MouseOver)
|
&& (*m_filtered)[row].entryKind == TypeEntry::Section);
|
||||||
painter->fillRect(option.rect, t.hover);
|
bool isDisabled = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||||
|
&& !(*m_filtered)[row].enabled);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (isSection) {
|
||||||
|
// No background highlight for sections
|
||||||
|
} else if (isDisabled) {
|
||||||
|
// Subtle background on hover only
|
||||||
|
if (option.state & QStyle::State_MouseOver)
|
||||||
|
painter->fillRect(option.rect, t.surface);
|
||||||
|
} else {
|
||||||
|
if (option.state & QStyle::State_Selected)
|
||||||
|
painter->fillRect(option.rect, t.selected);
|
||||||
|
else if (option.state & QStyle::State_MouseOver)
|
||||||
|
painter->fillRect(option.rect, t.hover);
|
||||||
|
}
|
||||||
|
|
||||||
int x = option.rect.x();
|
int x = option.rect.x();
|
||||||
int y = option.rect.y();
|
int y = option.rect.y();
|
||||||
int h = option.rect.height();
|
int h = option.rect.height();
|
||||||
|
int w = option.rect.width();
|
||||||
|
|
||||||
|
// Section: centered dim text with horizontal rules
|
||||||
|
if (isSection) {
|
||||||
|
painter->setPen(t.textDim);
|
||||||
|
QFont dimFont = m_font;
|
||||||
|
dimFont.setPointSize(qMax(7, m_font.pointSize() - 1));
|
||||||
|
painter->setFont(dimFont);
|
||||||
|
QFontMetrics fm(dimFont);
|
||||||
|
QString text = index.data().toString();
|
||||||
|
int textW = fm.horizontalAdvance(text);
|
||||||
|
int textX = x + (w - textW) / 2;
|
||||||
|
int lineY = y + h / 2;
|
||||||
|
|
||||||
|
// Left rule
|
||||||
|
if (textX > x + 8)
|
||||||
|
painter->drawLine(x + 8, lineY, textX - 6, lineY);
|
||||||
|
// Text
|
||||||
|
painter->drawText(QRect(textX, y, textW, h), Qt::AlignVCenter, text);
|
||||||
|
// Right rule
|
||||||
|
if (textX + textW + 6 < x + w - 8)
|
||||||
|
painter->drawLine(textX + textW + 6, lineY, x + w - 8, lineY);
|
||||||
|
|
||||||
|
painter->restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 18px gutter: side triangle if current
|
// 18px gutter: side triangle if current
|
||||||
int row = index.row();
|
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
|
||||||
if (m_filtered && row >= 0 && row < m_filtered->size()
|
const TypeEntry& entry = (*m_filtered)[row];
|
||||||
&& (*m_filtered)[row].id == m_currentId) {
|
bool isCurrent = false;
|
||||||
painter->setPen(t.syntaxType);
|
if (m_current->entryKind == TypeEntry::Primitive && entry.entryKind == TypeEntry::Primitive)
|
||||||
QFont checkFont = m_font;
|
isCurrent = (entry.primitiveKind == m_current->primitiveKind);
|
||||||
painter->setFont(checkFont);
|
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
||||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
isCurrent = (entry.structId == m_current->structId);
|
||||||
QString(QChar(0x25B8)));
|
if (isCurrent) {
|
||||||
|
painter->setPen(t.syntaxType);
|
||||||
|
painter->setFont(m_font);
|
||||||
|
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
||||||
|
QString(QChar(0x25B8)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
x += 18;
|
x += 18;
|
||||||
|
|
||||||
// Icon 16x16 — only for struct/class/enum entries (non-empty classKeyword)
|
// Icon 16x16 — only for composite entries
|
||||||
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||||
&& !(*m_filtered)[row].classKeyword.isEmpty());
|
&& (*m_filtered)[row].entryKind == TypeEntry::Composite);
|
||||||
if (hasIcon) {
|
if (hasIcon) {
|
||||||
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
|
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
|
||||||
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
|
QPixmap pm = structIcon.pixmap(16, 16);
|
||||||
|
if (isDisabled) {
|
||||||
|
// Paint dimmed
|
||||||
|
QPixmap dimmed(pm.size());
|
||||||
|
dimmed.fill(Qt::transparent);
|
||||||
|
QPainter p(&dimmed);
|
||||||
|
p.setOpacity(0.35);
|
||||||
|
p.drawPixmap(0, 0, pm);
|
||||||
|
p.end();
|
||||||
|
painter->drawPixmap(x, y + (h - 16) / 2, dimmed);
|
||||||
|
} else {
|
||||||
|
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
x += 20; // reserve space for alignment
|
x += 20;
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
painter->setPen(option.state & QStyle::State_Selected
|
QColor textColor;
|
||||||
? option.palette.color(QPalette::HighlightedText)
|
if (isDisabled)
|
||||||
: option.palette.color(QPalette::Text));
|
textColor = t.textDim;
|
||||||
|
else if (option.state & QStyle::State_Selected)
|
||||||
|
textColor = option.palette.color(QPalette::HighlightedText);
|
||||||
|
else
|
||||||
|
textColor = option.palette.color(QPalette::Text);
|
||||||
|
|
||||||
|
painter->setPen(textColor);
|
||||||
painter->setFont(m_font);
|
painter->setFont(m_font);
|
||||||
painter->drawText(QRect(x, y, option.rect.right() - x, h),
|
painter->drawText(QRect(x, y, option.rect.right() - x, h),
|
||||||
Qt::AlignVCenter | Qt::AlignLeft,
|
Qt::AlignVCenter | Qt::AlignLeft,
|
||||||
@@ -80,16 +178,21 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
|
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
|
||||||
const QModelIndex& /*index*/) const override {
|
const QModelIndex& index) const override {
|
||||||
QFontMetrics fm(m_font);
|
QFontMetrics fm(m_font);
|
||||||
return QSize(200, fm.height() + 8);
|
int row = index.row();
|
||||||
|
bool isSection = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||||
|
&& (*m_filtered)[row].entryKind == TypeEntry::Section);
|
||||||
|
int h = isSection ? fm.height() + 2 : fm.height() + 8;
|
||||||
|
return QSize(200, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TypeSelectorPopup* m_popup = nullptr;
|
TypeSelectorPopup* m_popup = nullptr;
|
||||||
QFont m_font;
|
QFont m_font;
|
||||||
const QVector<TypeEntry>* m_filtered = nullptr;
|
const QVector<TypeEntry>* m_filtered = nullptr;
|
||||||
uint64_t m_currentId = 0;
|
const TypeEntry* m_current = nullptr;
|
||||||
|
bool m_hasCurrent = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── TypeSelectorPopup ──
|
// ── TypeSelectorPopup ──
|
||||||
@@ -99,7 +202,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
{
|
{
|
||||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
|
|
||||||
// Theme palette
|
|
||||||
const auto& theme = ThemeManager::instance().current();
|
const auto& theme = ThemeManager::instance().current();
|
||||||
QPalette pal;
|
QPalette pal;
|
||||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||||
@@ -114,9 +216,8 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
setPalette(pal);
|
setPalette(pal);
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
|
|
||||||
// Thin border
|
setFrameShape(QFrame::NoFrame);
|
||||||
setFrameShape(QFrame::Box);
|
setLineWidth(0);
|
||||||
setLineWidth(1);
|
|
||||||
|
|
||||||
auto* layout = new QVBoxLayout(this);
|
auto* layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(6, 6, 6, 6);
|
layout->setContentsMargins(6, 6, 6, 6);
|
||||||
@@ -126,7 +227,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("Change root"));
|
m_titleLabel = new QLabel(QStringLiteral("Change type"));
|
||||||
m_titleLabel->setPalette(pal);
|
m_titleLabel->setPalette(pal);
|
||||||
QFont bold = m_titleLabel->font();
|
QFont bold = m_titleLabel->font();
|
||||||
bold.setBold(true);
|
bold.setBold(true);
|
||||||
@@ -151,14 +252,17 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
layout->addLayout(row);
|
layout->addLayout(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row 2: + Create new type button
|
// Row 2: + Create new type button (flat, no gradient)
|
||||||
{
|
{
|
||||||
m_createBtn = new QToolButton;
|
m_createBtn = new QToolButton;
|
||||||
m_createBtn->setText(QStringLiteral("+ Create new type\u2026"));
|
m_createBtn->setText(QStringLiteral("+ Create new type\u2026"));
|
||||||
m_createBtn->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
m_createBtn->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||||
m_createBtn->setAutoRaise(true);
|
m_createBtn->setAutoRaise(true);
|
||||||
m_createBtn->setCursor(Qt::PointingHandCursor);
|
m_createBtn->setCursor(Qt::PointingHandCursor);
|
||||||
m_createBtn->setPalette(pal);
|
m_createBtn->setStyleSheet(QStringLiteral(
|
||||||
|
"QToolButton { color: %1; border: none; padding: 3px 6px; }"
|
||||||
|
"QToolButton:hover { color: %2; background: %3; }")
|
||||||
|
.arg(theme.textMuted.name(), theme.text.name(), theme.hover.name()));
|
||||||
connect(m_createBtn, &QToolButton::clicked, this, [this]() {
|
connect(m_createBtn, &QToolButton::clicked, this, [this]() {
|
||||||
emit createNewTypeRequested();
|
emit createNewTypeRequested();
|
||||||
hide();
|
hide();
|
||||||
@@ -178,7 +282,65 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
layout->addWidget(sep);
|
layout->addWidget(sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row 3: Filter
|
// Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ]
|
||||||
|
{
|
||||||
|
m_modRow = new QWidget;
|
||||||
|
auto* modLayout = new QHBoxLayout(m_modRow);
|
||||||
|
modLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
modLayout->setSpacing(3);
|
||||||
|
|
||||||
|
m_modGroup = new QButtonGroup(this);
|
||||||
|
m_modGroup->setExclusive(true);
|
||||||
|
|
||||||
|
QString btnStyle = QStringLiteral(
|
||||||
|
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
|
||||||
|
" padding: 2px 8px; border-radius: 3px; }"
|
||||||
|
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
|
||||||
|
"QToolButton:hover:!checked { background: %6; }")
|
||||||
|
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
|
||||||
|
theme.text.name(), theme.selected.name(), theme.hover.name());
|
||||||
|
|
||||||
|
auto makeToggle = [&](const QString& label, int id) -> QToolButton* {
|
||||||
|
auto* btn = new QToolButton;
|
||||||
|
btn->setText(label);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
btn->setStyleSheet(btnStyle);
|
||||||
|
m_modGroup->addButton(btn, id);
|
||||||
|
modLayout->addWidget(btn);
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
|
||||||
|
m_btnPlain = makeToggle(QStringLiteral("plain"), 0);
|
||||||
|
m_btnPtr = makeToggle(QStringLiteral("*"), 1);
|
||||||
|
m_btnDblPtr = makeToggle(QStringLiteral("**"), 2);
|
||||||
|
m_btnArray = makeToggle(QStringLiteral("[n]"), 3);
|
||||||
|
m_btnPlain->setChecked(true);
|
||||||
|
|
||||||
|
// Array count input (shown only when [n] is active)
|
||||||
|
m_arrayCountEdit = new QLineEdit;
|
||||||
|
m_arrayCountEdit->setPlaceholderText(QStringLiteral("n"));
|
||||||
|
m_arrayCountEdit->setValidator(new QIntValidator(1, 99999, m_arrayCountEdit));
|
||||||
|
m_arrayCountEdit->setFixedWidth(50);
|
||||||
|
m_arrayCountEdit->setPalette(pal);
|
||||||
|
m_arrayCountEdit->hide();
|
||||||
|
modLayout->addWidget(m_arrayCountEdit);
|
||||||
|
|
||||||
|
modLayout->addStretch();
|
||||||
|
layout->addWidget(m_modRow);
|
||||||
|
|
||||||
|
connect(m_modGroup, &QButtonGroup::idToggled,
|
||||||
|
this, [this](int id, bool checked) {
|
||||||
|
if (!checked) return;
|
||||||
|
m_arrayCountEdit->setVisible(id == 3);
|
||||||
|
if (id == 3) m_arrayCountEdit->setFocus();
|
||||||
|
updateModifierPreview();
|
||||||
|
});
|
||||||
|
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||||
|
this, [this]() { updateModifierPreview(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 4: Filter + preview
|
||||||
{
|
{
|
||||||
m_filterEdit = new QLineEdit;
|
m_filterEdit = new QLineEdit;
|
||||||
m_filterEdit->setPlaceholderText(QStringLiteral("Filter types\u2026"));
|
m_filterEdit->setPlaceholderText(QStringLiteral("Filter types\u2026"));
|
||||||
@@ -188,6 +350,13 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
connect(m_filterEdit, &QLineEdit::textChanged,
|
connect(m_filterEdit, &QLineEdit::textChanged,
|
||||||
this, &TypeSelectorPopup::applyFilter);
|
this, &TypeSelectorPopup::applyFilter);
|
||||||
layout->addWidget(m_filterEdit);
|
layout->addWidget(m_filterEdit);
|
||||||
|
|
||||||
|
m_previewLabel = new QLabel;
|
||||||
|
m_previewLabel->setPalette(pal);
|
||||||
|
m_previewLabel->setStyleSheet(QStringLiteral(
|
||||||
|
"QLabel { color: %1; padding: 1px 6px; }").arg(theme.syntaxType.name()));
|
||||||
|
m_previewLabel->hide();
|
||||||
|
layout->addWidget(m_previewLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row 4: List
|
// Row 4: List
|
||||||
@@ -214,6 +383,17 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TypeSelectorPopup::warmUp() {
|
||||||
|
TypeEntry dummy;
|
||||||
|
dummy.entryKind = TypeEntry::Primitive;
|
||||||
|
dummy.primitiveKind = NodeKind::Hex8;
|
||||||
|
dummy.displayName = "warmup";
|
||||||
|
setTypes({dummy});
|
||||||
|
popup(QPoint(-9999, -9999));
|
||||||
|
hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::setFont(const QFont& font) {
|
void TypeSelectorPopup::setFont(const QFont& font) {
|
||||||
m_font = font;
|
m_font = font;
|
||||||
|
|
||||||
@@ -224,35 +404,80 @@ void TypeSelectorPopup::setFont(const QFont& font) {
|
|||||||
m_createBtn->setFont(font);
|
m_createBtn->setFont(font);
|
||||||
m_filterEdit->setFont(font);
|
m_filterEdit->setFont(font);
|
||||||
m_listView->setFont(font);
|
m_listView->setFont(font);
|
||||||
|
m_previewLabel->setFont(font);
|
||||||
|
|
||||||
|
QFont smallFont = font;
|
||||||
|
smallFont.setPointSize(qMax(7, font.pointSize() - 1));
|
||||||
|
m_btnPlain->setFont(smallFont);
|
||||||
|
m_btnPtr->setFont(smallFont);
|
||||||
|
m_btnDblPtr->setFont(smallFont);
|
||||||
|
m_btnArray->setFont(smallFont);
|
||||||
|
m_arrayCountEdit->setFont(smallFont);
|
||||||
|
|
||||||
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
|
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
|
||||||
if (delegate)
|
if (delegate)
|
||||||
delegate->setFont(font);
|
delegate->setFont(font);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, uint64_t currentId) {
|
void TypeSelectorPopup::setTitle(const QString& title) {
|
||||||
|
m_titleLabel->setText(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
|
||||||
|
m_currentNodeSize = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
|
||||||
m_allTypes = types;
|
m_allTypes = types;
|
||||||
m_currentId = currentId;
|
if (current) {
|
||||||
|
m_currentEntry = *current;
|
||||||
|
m_hasCurrent = true;
|
||||||
|
} else {
|
||||||
|
m_currentEntry = TypeEntry{};
|
||||||
|
m_hasCurrent = false;
|
||||||
|
}
|
||||||
|
// Reset modifier toggles
|
||||||
|
m_btnPlain->setChecked(true);
|
||||||
|
m_arrayCountEdit->clear();
|
||||||
|
m_arrayCountEdit->hide();
|
||||||
|
m_previewLabel->hide();
|
||||||
|
|
||||||
m_filterEdit->clear();
|
m_filterEdit->clear();
|
||||||
applyFilter(QString());
|
applyFilter(QString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||||
// Size: width based on longest entry, height based on count
|
|
||||||
QFontMetrics fm(m_font);
|
QFontMetrics fm(m_font);
|
||||||
int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element 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.isEmpty()
|
||||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16; // gutter + icon + text + pad
|
? t.displayName
|
||||||
|
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
||||||
|
int w = 18 + 20 + fm.horizontalAdvance(text) + 16;
|
||||||
if (w > maxTextW) maxTextW = w;
|
if (w > maxTextW) maxTextW = w;
|
||||||
}
|
}
|
||||||
int popupW = qBound(250, maxTextW + 24, 500); // +margins
|
int popupW = qBound(280, maxTextW + 24, 500);
|
||||||
int rowH = fm.height() + 8;
|
int rowH = fm.height() + 8;
|
||||||
int headerH = rowH * 3 + 30; // title + button + filter + separators/margins
|
int headerH = rowH * 3 + 30;
|
||||||
int listH = qBound(rowH * 3, rowH * (int)m_allTypes.size(), rowH * 12);
|
if (m_modRow->isVisible())
|
||||||
|
headerH += rowH + 4; // extra row for modifier toggles
|
||||||
|
int listH = qBound(rowH * 3, rowH * (int)m_filteredTypes.size(), rowH * 14);
|
||||||
int popupH = headerH + listH;
|
int popupH = headerH + listH;
|
||||||
|
|
||||||
// Clamp to screen
|
|
||||||
QScreen* screen = QApplication::screenAt(globalPos);
|
QScreen* screen = QApplication::screenAt(globalPos);
|
||||||
if (screen) {
|
if (screen) {
|
||||||
QRect avail = screen->availableGeometry();
|
QRect avail = screen->availableGeometry();
|
||||||
@@ -270,40 +495,125 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
|||||||
m_filterEdit->setFocus();
|
m_filterEdit->setFocus();
|
||||||
|
|
||||||
// Pre-select current type in list
|
// Pre-select current type in list
|
||||||
for (int i = 0; i < m_filteredTypes.size(); i++) {
|
if (m_hasCurrent) {
|
||||||
if (m_filteredTypes[i].id == m_currentId) {
|
for (int i = 0; i < m_filteredTypes.size(); i++) {
|
||||||
m_listView->setCurrentIndex(m_model->index(i));
|
const auto& entry = m_filteredTypes[i];
|
||||||
break;
|
if (entry.entryKind == TypeEntry::Section) continue;
|
||||||
|
bool match = false;
|
||||||
|
if (m_currentEntry.entryKind == TypeEntry::Primitive && entry.entryKind == TypeEntry::Primitive)
|
||||||
|
match = (entry.primitiveKind == m_currentEntry.primitiveKind);
|
||||||
|
else if (m_currentEntry.entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
||||||
|
match = (entry.structId == m_currentEntry.structId);
|
||||||
|
if (match) {
|
||||||
|
m_listView->setCurrentIndex(m_model->index(i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TypeSelectorPopup::updateModifierPreview() {
|
||||||
|
int modId = m_modGroup->checkedId();
|
||||||
|
if (modId <= 0) {
|
||||||
|
m_previewLabel->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString suffix;
|
||||||
|
if (modId == 1) suffix = QStringLiteral("*");
|
||||||
|
else if (modId == 2) suffix = QStringLiteral("**");
|
||||||
|
else if (modId == 3) {
|
||||||
|
QString countText = m_arrayCountEdit->text().trimmed();
|
||||||
|
suffix = countText.isEmpty()
|
||||||
|
? QStringLiteral("[n]")
|
||||||
|
: QStringLiteral("[%1]").arg(countText);
|
||||||
|
}
|
||||||
|
m_previewLabel->setText(QStringLiteral("\u2192 <type>%1").arg(suffix));
|
||||||
|
m_previewLabel->show();
|
||||||
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::applyFilter(const QString& text) {
|
void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||||
m_filteredTypes.clear();
|
m_filteredTypes.clear();
|
||||||
QStringList displayStrings;
|
QStringList displayStrings;
|
||||||
|
|
||||||
|
QString filterBase = text.trimmed();
|
||||||
|
|
||||||
|
// Separate primitives and composites
|
||||||
|
QVector<TypeEntry> primitives, composites;
|
||||||
for (const auto& t : m_allTypes) {
|
for (const auto& t : m_allTypes) {
|
||||||
if (text.isEmpty()
|
if (t.entryKind == TypeEntry::Section) continue; // skip stale sections
|
||||||
|| t.displayName.contains(text, Qt::CaseInsensitive)
|
bool matchesFilter = filterBase.isEmpty()
|
||||||
|| t.classKeyword.contains(text, Qt::CaseInsensitive)) {
|
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|
||||||
m_filteredTypes.append(t);
|
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||||
if (t.classKeyword.isEmpty())
|
if (!matchesFilter) continue;
|
||||||
displayStrings << t.displayName;
|
|
||||||
|
if (t.entryKind == TypeEntry::Primitive)
|
||||||
|
primitives.append(t);
|
||||||
|
else if (t.entryKind == TypeEntry::Composite)
|
||||||
|
composites.append(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-Root modes, sort primitives: same-size first, then rest
|
||||||
|
if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) {
|
||||||
|
QVector<TypeEntry> sameSize, other;
|
||||||
|
for (const auto& p : primitives) {
|
||||||
|
if (sizeForKind(p.primitiveKind) == m_currentNodeSize)
|
||||||
|
sameSize.append(p);
|
||||||
else
|
else
|
||||||
displayStrings << (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
other.append(p);
|
||||||
}
|
}
|
||||||
|
primitives = sameSize + other;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper lambdas for appending sections
|
||||||
|
auto appendPrimitives = [&]() {
|
||||||
|
if (primitives.isEmpty()) return;
|
||||||
|
TypeEntry sec;
|
||||||
|
sec.entryKind = TypeEntry::Section;
|
||||||
|
sec.displayName = QStringLiteral("primitives");
|
||||||
|
sec.enabled = false;
|
||||||
|
m_filteredTypes.append(sec);
|
||||||
|
displayStrings << sec.displayName;
|
||||||
|
for (const auto& p : primitives) {
|
||||||
|
m_filteredTypes.append(p);
|
||||||
|
displayStrings << p.displayName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto appendComposites = [&]() {
|
||||||
|
if (composites.isEmpty()) return;
|
||||||
|
TypeEntry sec;
|
||||||
|
sec.entryKind = TypeEntry::Section;
|
||||||
|
sec.displayName = QStringLiteral("project types");
|
||||||
|
sec.enabled = false;
|
||||||
|
m_filteredTypes.append(sec);
|
||||||
|
displayStrings << sec.displayName;
|
||||||
|
for (const auto& c : composites) {
|
||||||
|
m_filteredTypes.append(c);
|
||||||
|
QString label = c.classKeyword.isEmpty()
|
||||||
|
? c.displayName
|
||||||
|
: (c.classKeyword + QStringLiteral(" ") + c.displayName);
|
||||||
|
displayStrings << label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Root mode: project types first (composites are the primary selection)
|
||||||
|
if (m_mode == TypePopupMode::Root) {
|
||||||
|
appendComposites();
|
||||||
|
appendPrimitives();
|
||||||
|
} else {
|
||||||
|
appendPrimitives();
|
||||||
|
appendComposites();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_model->setStringList(displayStrings);
|
m_model->setStringList(displayStrings);
|
||||||
|
|
||||||
// Update delegate data
|
|
||||||
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
|
auto* delegate = static_cast<TypeSelectorDelegate*>(m_listView->itemDelegate());
|
||||||
if (delegate)
|
if (delegate)
|
||||||
delegate->setCurrentTypes(&m_filteredTypes, m_currentId);
|
delegate->setFilteredTypes(&m_filteredTypes, &m_currentEntry, m_hasCurrent);
|
||||||
|
|
||||||
// Select first match
|
// Select first selectable item
|
||||||
if (!m_filteredTypes.isEmpty())
|
int first = nextSelectableRow(0, 1);
|
||||||
m_listView->setCurrentIndex(m_model->index(0));
|
if (first >= 0)
|
||||||
|
m_listView->setCurrentIndex(m_model->index(first));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TypeSelectorPopup::acceptCurrent() {
|
void TypeSelectorPopup::acceptCurrent() {
|
||||||
@@ -312,16 +622,40 @@ 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, m_filteredTypes[row].displayName);
|
const TypeEntry& entry = m_filteredTypes[row];
|
||||||
|
if (entry.entryKind == TypeEntry::Section) return;
|
||||||
|
if (!entry.enabled) return;
|
||||||
|
|
||||||
|
// Build full text with modifier from toggle buttons
|
||||||
|
int modId = m_modGroup->checkedId();
|
||||||
|
QString fullText = entry.displayName;
|
||||||
|
if (modId == 1)
|
||||||
|
fullText += QStringLiteral("*");
|
||||||
|
else if (modId == 2)
|
||||||
|
fullText += QStringLiteral("**");
|
||||||
|
else if (modId == 3) {
|
||||||
|
QString countText = m_arrayCountEdit->text().trimmed();
|
||||||
|
if (!countText.isEmpty())
|
||||||
|
fullText += QStringLiteral("[%1]").arg(countText);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit typeSelected(entry, fullText);
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int TypeSelectorPopup::nextSelectableRow(int from, int direction) const {
|
||||||
|
int i = from;
|
||||||
|
while (i >= 0 && i < m_filteredTypes.size()) {
|
||||||
|
const auto& e = m_filteredTypes[i];
|
||||||
|
if (e.entryKind != TypeEntry::Section && e.enabled)
|
||||||
|
return i;
|
||||||
|
i += direction;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) {
|
bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) {
|
||||||
if (event->type() == QEvent::KeyPress) {
|
if (event->type() == QEvent::KeyPress) {
|
||||||
auto* ke = static_cast<QKeyEvent*>(event);
|
auto* ke = static_cast<QKeyEvent*>(event);
|
||||||
@@ -334,8 +668,11 @@ bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
if (obj == m_filterEdit) {
|
if (obj == m_filterEdit) {
|
||||||
if (ke->key() == Qt::Key_Down) {
|
if (ke->key() == Qt::Key_Down) {
|
||||||
m_listView->setFocus();
|
m_listView->setFocus();
|
||||||
if (!m_listView->currentIndex().isValid() && m_model->rowCount() > 0)
|
QModelIndex cur = m_listView->currentIndex();
|
||||||
m_listView->setCurrentIndex(m_model->index(0));
|
int startRow = cur.isValid() ? cur.row() : 0;
|
||||||
|
int next = nextSelectableRow(startRow, 1);
|
||||||
|
if (next >= 0)
|
||||||
|
m_listView->setCurrentIndex(m_model->index(next));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
|
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
|
||||||
@@ -351,11 +688,33 @@ bool TypeSelectorPopup::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
m_filterEdit->setFocus();
|
m_filterEdit->setFocus();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Skip sections and disabled entries
|
||||||
|
int prev = nextSelectableRow(cur.row() - 1, -1);
|
||||||
|
if (prev < 0) {
|
||||||
|
m_filterEdit->setFocus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_listView->setCurrentIndex(m_model->index(prev));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (ke->key() == Qt::Key_Down) {
|
||||||
|
QModelIndex cur = m_listView->currentIndex();
|
||||||
|
int startRow = cur.isValid() ? cur.row() + 1 : 0;
|
||||||
|
int next = nextSelectableRow(startRow, 1);
|
||||||
|
if (next >= 0)
|
||||||
|
m_listView->setCurrentIndex(m_model->index(next));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
|
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
|
||||||
acceptCurrent();
|
acceptCurrent();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Forward printable keys to filter edit for type-to-filter
|
||||||
|
if (!ke->text().isEmpty() && ke->text()[0].isPrint()) {
|
||||||
|
m_filterEdit->setFocus();
|
||||||
|
m_filterEdit->setText(m_filterEdit->text() + ke->text());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,47 @@
|
|||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "core.h"
|
||||||
|
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
class QListView;
|
class QListView;
|
||||||
class QStringListModel;
|
class QStringListModel;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
|
class QButtonGroup;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
|
// ── Popup mode ──
|
||||||
|
|
||||||
|
enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
|
||||||
|
|
||||||
|
// ── Type entry (explicit discriminant — no sentinel IDs) ──
|
||||||
|
|
||||||
struct TypeEntry {
|
struct TypeEntry {
|
||||||
uint64_t id = 0;
|
enum Kind { Primitive, Composite, Section };
|
||||||
QString displayName;
|
|
||||||
QString classKeyword; // "struct", "class", or "enum"
|
Kind entryKind = Primitive;
|
||||||
|
NodeKind primitiveKind = NodeKind::Hex8; // valid when entryKind==Primitive
|
||||||
|
uint64_t structId = 0; // valid when entryKind==Composite
|
||||||
|
QString displayName;
|
||||||
|
QString classKeyword; // "struct", "class", "enum" (Composite only)
|
||||||
|
bool enabled = true; // false = grayed out (visible but not selectable)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Parsed type spec (shared between popup filter and inline edit) ──
|
||||||
|
|
||||||
|
struct TypeSpec {
|
||||||
|
QString baseName;
|
||||||
|
bool isPointer = false;
|
||||||
|
int arrayCount = 0; // 0 = not array
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeSpec parseTypeSpec(const QString& text);
|
||||||
|
|
||||||
|
// ── Popup widget ──
|
||||||
|
|
||||||
class TypeSelectorPopup : public QFrame {
|
class TypeSelectorPopup : public QFrame {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@@ -26,11 +52,16 @@ public:
|
|||||||
|
|
||||||
void setFont(const QFont& font);
|
void setFont(const QFont& font);
|
||||||
void setTitle(const QString& title);
|
void setTitle(const QString& title);
|
||||||
void setTypes(const QVector<TypeEntry>& types, uint64_t currentId);
|
void setMode(TypePopupMode mode);
|
||||||
|
void setCurrentNodeSize(int bytes);
|
||||||
|
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
||||||
void popup(const QPoint& globalPos);
|
void popup(const QPoint& globalPos);
|
||||||
|
|
||||||
|
/// Force native window creation to avoid cold-start delay.
|
||||||
|
void warmUp();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void typeSelected(uint64_t id, const QString& displayName);
|
void typeSelected(const TypeEntry& entry, const QString& fullText);
|
||||||
void createNewTypeRequested();
|
void createNewTypeRequested();
|
||||||
void dismissed();
|
void dismissed();
|
||||||
|
|
||||||
@@ -43,17 +74,32 @@ private:
|
|||||||
QToolButton* m_escLabel = nullptr;
|
QToolButton* m_escLabel = nullptr;
|
||||||
QToolButton* m_createBtn = nullptr;
|
QToolButton* m_createBtn = nullptr;
|
||||||
QLineEdit* m_filterEdit = nullptr;
|
QLineEdit* m_filterEdit = nullptr;
|
||||||
|
QLabel* m_previewLabel = nullptr;
|
||||||
QListView* m_listView = nullptr;
|
QListView* m_listView = nullptr;
|
||||||
QStringListModel* m_model = nullptr;
|
QStringListModel* m_model = nullptr;
|
||||||
|
|
||||||
|
// Modifier toggles
|
||||||
|
QWidget* m_modRow = nullptr;
|
||||||
|
QToolButton* m_btnPlain = nullptr;
|
||||||
|
QToolButton* m_btnPtr = nullptr;
|
||||||
|
QToolButton* m_btnDblPtr = nullptr;
|
||||||
|
QToolButton* m_btnArray = nullptr;
|
||||||
|
QLineEdit* m_arrayCountEdit = nullptr;
|
||||||
|
QButtonGroup* m_modGroup = nullptr;
|
||||||
|
|
||||||
QVector<TypeEntry> m_allTypes;
|
QVector<TypeEntry> m_allTypes;
|
||||||
QVector<TypeEntry> m_filteredTypes;
|
QVector<TypeEntry> m_filteredTypes;
|
||||||
uint64_t m_currentId = 0;
|
TypeEntry m_currentEntry;
|
||||||
|
bool m_hasCurrent = false;
|
||||||
|
TypePopupMode m_mode = TypePopupMode::FieldType;
|
||||||
|
int m_currentNodeSize = 0;
|
||||||
QFont m_font;
|
QFont m_font;
|
||||||
|
|
||||||
void applyFilter(const QString& text);
|
void applyFilter(const QString& text);
|
||||||
|
void updateModifierPreview();
|
||||||
void acceptCurrent();
|
void acceptCurrent();
|
||||||
void acceptIndex(int row);
|
void acceptIndex(int row);
|
||||||
|
int nextSelectableRow(int from, int direction) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
#include <QtTest/QSignalSpy>
|
#include <QtTest/QSignalSpy>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListView>
|
||||||
|
#include <QStringListModel>
|
||||||
#include <Qsci/qsciscintilla.h>
|
#include <Qsci/qsciscintilla.h>
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "typeselectorpopup.h"
|
#include "typeselectorpopup.h"
|
||||||
|
#include "themes/thememanager.h"
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
Q_DECLARE_METATYPE(rcx::TypeEntry)
|
||||||
Q_DECLARE_METATYPE(uint64_t)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace rcx;
|
using namespace rcx;
|
||||||
|
|
||||||
@@ -49,9 +54,7 @@ class TestTypeSelector : public QObject {
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase() {
|
void initTestCase() {
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
qRegisterMetaType<TypeEntry>("TypeEntry");
|
||||||
qRegisterMetaType<uint64_t>("uint64_t");
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Chevron span detection ──
|
// ── Chevron span detection ──
|
||||||
@@ -89,6 +92,112 @@ private slots:
|
|||||||
QCOMPARE(text.mid(rootName.start, rootName.end - rootName.start).trimmed(), QString("Alpha"));
|
QCOMPARE(text.mid(rootName.start, rootName.end - rootName.start).trimmed(), QString("Alpha"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Benchmark: warmUp() + cached reuse vs cold new/delete ──
|
||||||
|
|
||||||
|
void benchmarkPopupOpen() {
|
||||||
|
auto makeComposite = [](uint64_t id, const QString& name, const QString& kw) {
|
||||||
|
TypeEntry e;
|
||||||
|
e.entryKind = TypeEntry::Composite;
|
||||||
|
e.structId = id;
|
||||||
|
e.displayName = name;
|
||||||
|
e.classKeyword = kw;
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
QVector<TypeEntry> types;
|
||||||
|
types.append(makeComposite(1, "Alpha", "struct"));
|
||||||
|
types.append(makeComposite(2, "Bravo", "struct"));
|
||||||
|
types.append(makeComposite(3, "Charlie", "struct"));
|
||||||
|
types.append(makeComposite(4, "Delta", "class"));
|
||||||
|
|
||||||
|
TypeEntry cur1 = makeComposite(1, "Alpha", "struct");
|
||||||
|
TypeEntry cur2 = makeComposite(2, "Bravo", "struct");
|
||||||
|
|
||||||
|
QFont font("Consolas", 12);
|
||||||
|
font.setFixedPitch(true);
|
||||||
|
|
||||||
|
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
|
||||||
|
|
||||||
|
// --- Measure cold path: new popup, first show ever ---
|
||||||
|
{
|
||||||
|
QElapsedTimer total;
|
||||||
|
total.start();
|
||||||
|
auto* popup = new TypeSelectorPopup();
|
||||||
|
popup->setFont(font);
|
||||||
|
popup->setTypes(types, &cur1);
|
||||||
|
popup->popup(QPoint(100, 100));
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 tCold = total.nsecsElapsed();
|
||||||
|
popup->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== COLD (new popup, no warmUp) ===");
|
||||||
|
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tCold));
|
||||||
|
|
||||||
|
// --- Measure cached reuse of same instance ---
|
||||||
|
{
|
||||||
|
QElapsedTimer t2;
|
||||||
|
t2.start();
|
||||||
|
popup->setTypes(types, &cur2);
|
||||||
|
popup->popup(QPoint(100, 100));
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 tReuse = t2.nsecsElapsed();
|
||||||
|
popup->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== WARM (reuse same popup) ===");
|
||||||
|
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tReuse));
|
||||||
|
}
|
||||||
|
|
||||||
|
delete popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Measure warmUp() approach ---
|
||||||
|
{
|
||||||
|
QElapsedTimer tWarmup;
|
||||||
|
tWarmup.start();
|
||||||
|
auto* popup2 = new TypeSelectorPopup();
|
||||||
|
popup2->warmUp();
|
||||||
|
qint64 tWarmMs = tWarmup.nsecsElapsed();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== warmUp() cost (constructor + hidden show/hide) ===");
|
||||||
|
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tWarmMs));
|
||||||
|
|
||||||
|
// First user-visible show after warmUp
|
||||||
|
QElapsedTimer t3;
|
||||||
|
t3.start();
|
||||||
|
popup2->setFont(font);
|
||||||
|
popup2->setTypes(types, &cur1);
|
||||||
|
popup2->popup(QPoint(100, 100));
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 tFirst = t3.nsecsElapsed();
|
||||||
|
popup2->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== FIRST visible show after warmUp() ===");
|
||||||
|
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tFirst));
|
||||||
|
|
||||||
|
// Second show (fully warm)
|
||||||
|
QElapsedTimer t4;
|
||||||
|
t4.start();
|
||||||
|
popup2->setTypes(types, &cur2);
|
||||||
|
popup2->popup(QPoint(100, 100));
|
||||||
|
QApplication::processEvents();
|
||||||
|
qint64 tSecond = t4.nsecsElapsed();
|
||||||
|
popup2->hide();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
qDebug() << "";
|
||||||
|
qDebug().noquote() << QString("=== SECOND visible show after warmUp() ===");
|
||||||
|
qDebug().noquote() << QString(" Total: %1 ms").arg(ms(tSecond));
|
||||||
|
|
||||||
|
delete popup2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Popup data model ──
|
// ── Popup data model ──
|
||||||
|
|
||||||
void testPopupListsRootStructs() {
|
void testPopupListsRootStructs() {
|
||||||
@@ -98,8 +207,12 @@ private slots:
|
|||||||
QVector<TypeEntry> types;
|
QVector<TypeEntry> types;
|
||||||
for (const auto& n : tree.nodes) {
|
for (const auto& n : tree.nodes) {
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||||
types.append({n.id, n.structTypeName.isEmpty() ? n.name : n.structTypeName,
|
TypeEntry e;
|
||||||
n.resolvedClassKeyword()});
|
e.entryKind = TypeEntry::Composite;
|
||||||
|
e.structId = n.id;
|
||||||
|
e.displayName = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||||
|
e.classKeyword = n.resolvedClassKeyword();
|
||||||
|
types.append(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +225,28 @@ private slots:
|
|||||||
|
|
||||||
void testPopupSignals() {
|
void testPopupSignals() {
|
||||||
TypeSelectorPopup popup;
|
TypeSelectorPopup popup;
|
||||||
popup.setTypes({{1, "A", "struct"}, {2, "B", "struct"}}, 1);
|
|
||||||
|
TypeEntry eA;
|
||||||
|
eA.entryKind = TypeEntry::Composite;
|
||||||
|
eA.structId = 1;
|
||||||
|
eA.displayName = "A";
|
||||||
|
eA.classKeyword = "struct";
|
||||||
|
TypeEntry eB;
|
||||||
|
eB.entryKind = TypeEntry::Composite;
|
||||||
|
eB.structId = 2;
|
||||||
|
eB.displayName = "B";
|
||||||
|
eB.classKeyword = "struct";
|
||||||
|
QVector<TypeEntry> types;
|
||||||
|
types.append(eA);
|
||||||
|
types.append(eB);
|
||||||
|
popup.setTypes(types, &eA);
|
||||||
|
|
||||||
QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected);
|
QSignalSpy typeSpy(&popup, &TypeSelectorPopup::typeSelected);
|
||||||
QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested);
|
QSignalSpy createSpy(&popup, &TypeSelectorPopup::createNewTypeRequested);
|
||||||
|
|
||||||
emit popup.typeSelected(2, QStringLiteral("B"));
|
emit popup.typeSelected(eB, QStringLiteral("B"));
|
||||||
QCOMPARE(typeSpy.count(), 1);
|
QCOMPARE(typeSpy.count(), 1);
|
||||||
QCOMPARE(typeSpy.at(0).at(0).toULongLong(), (uint64_t)2);
|
// Verify the entry came through — check the fullText (second arg)
|
||||||
QCOMPARE(typeSpy.at(0).at(1).toString(), QStringLiteral("B"));
|
QCOMPARE(typeSpy.at(0).at(1).toString(), QStringLiteral("B"));
|
||||||
|
|
||||||
emit popup.createNewTypeRequested();
|
emit popup.createNewTypeRequested();
|
||||||
@@ -227,6 +354,85 @@ private slots:
|
|||||||
delete splitter;
|
delete splitter;
|
||||||
delete doc;
|
delete doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── parseTypeSpec tests ──
|
||||||
|
|
||||||
|
void testParseTypeSpecPlain() {
|
||||||
|
TypeSpec spec = parseTypeSpec("int32_t");
|
||||||
|
QCOMPARE(spec.baseName, QString("int32_t"));
|
||||||
|
QVERIFY(!spec.isPointer);
|
||||||
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecArray() {
|
||||||
|
TypeSpec spec = parseTypeSpec("int32_t[10]");
|
||||||
|
QCOMPARE(spec.baseName, QString("int32_t"));
|
||||||
|
QVERIFY(!spec.isPointer);
|
||||||
|
QCOMPARE(spec.arrayCount, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecPointer() {
|
||||||
|
TypeSpec spec = parseTypeSpec("Ball*");
|
||||||
|
QCOMPARE(spec.baseName, QString("Ball"));
|
||||||
|
QVERIFY(spec.isPointer);
|
||||||
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecDoublePointer() {
|
||||||
|
TypeSpec spec = parseTypeSpec("Ball**");
|
||||||
|
QCOMPARE(spec.baseName, QString("Ball"));
|
||||||
|
QVERIFY(spec.isPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecEmpty() {
|
||||||
|
TypeSpec spec = parseTypeSpec("");
|
||||||
|
QVERIFY(spec.baseName.isEmpty());
|
||||||
|
QVERIFY(!spec.isPointer);
|
||||||
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecWhitespace() {
|
||||||
|
TypeSpec spec = parseTypeSpec(" Ball * ");
|
||||||
|
// trimmed → "Ball *", ends with '*'
|
||||||
|
QCOMPARE(spec.baseName, QString("Ball"));
|
||||||
|
QVERIFY(spec.isPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testParseTypeSpecArrayZero() {
|
||||||
|
// [0] parses baseName but arrayCount stays 0 (invalid count)
|
||||||
|
TypeSpec spec = parseTypeSpec("int32_t[0]");
|
||||||
|
QCOMPARE(spec.baseName, QString("int32_t"));
|
||||||
|
QCOMPARE(spec.arrayCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Section headers in filtered list ──
|
||||||
|
|
||||||
|
void testSectionHeadersPresent() {
|
||||||
|
TypeSelectorPopup popup;
|
||||||
|
|
||||||
|
// Build entries with both primitives and composites
|
||||||
|
QVector<TypeEntry> types;
|
||||||
|
TypeEntry prim;
|
||||||
|
prim.entryKind = TypeEntry::Primitive;
|
||||||
|
prim.primitiveKind = NodeKind::Int32;
|
||||||
|
prim.displayName = "int32_t";
|
||||||
|
types.append(prim);
|
||||||
|
|
||||||
|
TypeEntry comp;
|
||||||
|
comp.entryKind = TypeEntry::Composite;
|
||||||
|
comp.structId = 42;
|
||||||
|
comp.displayName = "MyStruct";
|
||||||
|
comp.classKeyword = "struct";
|
||||||
|
types.append(comp);
|
||||||
|
|
||||||
|
popup.setTypes(types);
|
||||||
|
// After setTypes, the internal filtered list should have section headers
|
||||||
|
// We can verify this indirectly by checking the model row count
|
||||||
|
// (should be > 2 due to section headers)
|
||||||
|
auto* listView = popup.findChild<QListView*>();
|
||||||
|
QVERIFY(listView);
|
||||||
|
QVERIFY(listView->model()->rowCount() > 2);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestTypeSelector)
|
QTEST_MAIN(TestTypeSelector)
|
||||||
|
|||||||
Reference in New Issue
Block a user