feat: fix heatmap false-heat on offset shift, hover flicker, type chooser cleanup

- Clear value history when node offsets change (insert/delete/resize/
  manual offset edit) so stale values from old addresses don't show
  false heat coloring
- Invalidate in-flight async reads (bump refreshGen) when tree layout
  changes, preventing stale snapshot data from re-introducing heat
- Fix command bar hover cursor flicker: remove premature
  applyHoverCursor() from applyDocument() — runs correctly via
  applySelectionOverlays() after text is finalized
- Fix hover indicator survival: reorder refresh() so text-modifying
  passes (updateCommandRow) run before overlay passes
- Guard synthetic Leave events during setText() to preserve hover state
- Remove primitives from type chooser when pointer modifier (* / **)
  is active; remove primitives entirely in Root command bar mode
- Add test_editor and test_controller test coverage for heat clearing,
  hover survival, and mixed hex/non-hex type scenarios
This commit is contained in:
IChooseYou
2026-02-17 11:41:46 -07:00
parent 5ae9ca0979
commit 1c3b4af045
18 changed files with 996 additions and 498 deletions

View File

@@ -436,7 +436,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
m_doc->tree.baseAddress = newBase;
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
resetSnapshot();
emit m_doc->documentChanged();
@@ -634,12 +637,12 @@ void RcxController::refresh() {
}
// Update value history and compute heat levels
// Use the snapshot provider if active; skip entirely if no valid provider
// Only run when a live provider is attached (not for static file/buffer sources)
{
const Provider* prov = nullptr;
if (m_snapshotProv)
if (m_snapshotProv && m_snapshotProv->isLive())
prov = m_snapshotProv.get();
else if (m_doc->provider && m_doc->provider->isValid())
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
prov = m_doc->provider.get();
if (prov) {
@@ -651,11 +654,9 @@ void RcxController::refresh() {
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
// Skip containers — they don't have scalar values
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
// Skip hex preview nodes — they show raw bytes, not a single value
if (isHexPreview(node.kind)) continue;
int64_t nodeOff = m_doc->tree.computeOffset(lm.nodeIdx);
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(nodeOff);
uint64_t addr = static_cast<uint64_t>(nodeOff); // provider-relative
int sz = node.byteSize();
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
@@ -666,17 +667,6 @@ void RcxController::refresh() {
}
}
}
// Apply persisted heat levels even when provider is unavailable
if (!prov) {
for (auto& lm : m_lastResult.meta) {
if (lm.nodeId != 0) {
auto it = m_valueHistory.find(lm.nodeId);
if (it != m_valueHistory.end())
lm.heatLevel = it->heatLevel();
}
}
}
}
// Prune stale selections (nodes removed by undo/redo/delete)
@@ -707,9 +697,11 @@ void RcxController::refresh() {
editor->applyDocument(m_lastResult);
editor->restoreViewState(vs);
}
applySelectionOverlays();
// Text-modifying passes first (command row replaces line 0 text),
// then overlays last so hover indicators survive the refresh.
pushSavedSourcesToEditors();
updateCommandRow();
applySelectionOverlays();
}
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
@@ -906,6 +898,23 @@ void RcxController::materializeRefChildren(int nodeIdx) {
void RcxController::applyCommand(const Command& command, bool isUndo) {
auto& tree = m_doc->tree;
// Clear value history for nodes whose effective offset changed.
// When offsets shift (insert/delete/resize), old recorded values came from
// a different memory address, so keeping them would show false heat.
// Also invalidates any in-flight async read so that stale snapshot data
// from before the offset change doesn't re-introduce false heat.
auto clearHistoryForAdjs = [&](const QVector<cmd::OffsetAdj>& adjs) {
if (adjs.isEmpty()) return;
m_refreshGen++; // discard in-flight async read (stale layout)
for (const auto& adj : adjs) {
// Clear the adjusted node itself
m_valueHistory.remove(adj.nodeId);
// Clear all descendants (their effective address also shifted)
for (int ci : tree.subtreeIndices(adj.nodeId))
m_valueHistory.remove(tree.nodes[ci].id);
}
};
std::visit([&](auto&& c) {
using T = std::decay_t<decltype(c)>;
if constexpr (std::is_same_v<T, cmd::ChangeKind>) {
@@ -917,6 +926,12 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
if (ai >= 0)
tree.nodes[ai].offset = isUndo ? adj.oldOffset : adj.newOffset;
}
// The changed node's value format changed; clear its history.
// If offAdjs is empty (same-size change), still bump gen to
// discard in-flight reads that would record the old format.
if (c.offAdjs.isEmpty()) m_refreshGen++;
m_valueHistory.remove(c.nodeId);
clearHistoryForAdjs(c.offAdjs);
} else if constexpr (std::is_same_v<T, cmd::Rename>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
@@ -945,6 +960,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
}
}
clearHistoryForAdjs(c.offAdjs);
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
if (isUndo) {
// Restore nodes first
@@ -970,6 +986,8 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
}
tree.invalidateIdCache();
}
// Siblings shifted — their old values are from wrong addresses
clearHistoryForAdjs(c.offAdjs);
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress
@@ -1016,6 +1034,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
tree.nodes[idx].offset = isUndo ? c.oldOffset : c.newOffset;
// Node and its descendants read from a different address now
m_refreshGen++; // discard in-flight async read (stale layout)
m_valueHistory.remove(c.nodeId);
for (int ci : tree.subtreeIndices(c.nodeId))
m_valueHistory.remove(tree.nodes[ci].id);
}
}, command);
@@ -1494,8 +1517,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
}
}
applySelectionOverlays();
updateCommandRow();
applySelectionOverlays();
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
@@ -1508,8 +1531,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
void RcxController::clearSelection() {
m_selIds.clear();
m_anchorLine = -1;
applySelectionOverlays();
updateCommandRow();
applySelectionOverlays();
}
void RcxController::applySelectionOverlays() {
@@ -1750,7 +1773,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
switch (mode) {
case TypePopupMode::Root:
addPrimitives(/*enabled=*/false, /*excludeStructArrayPad=*/false);
// No primitives in Root mode only project types are valid roots
addComposites([&](const Node&, const TypeEntry& e) {
return e.structId == m_viewRootId;
});
@@ -2019,7 +2042,10 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
m_doc->tree.baseAddress = newBase;
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
resetSnapshot();
emit m_doc->documentChanged();
refresh();
@@ -2062,9 +2088,15 @@ void RcxController::pushSavedSourcesToEditors() {
// ── Auto-refresh ──
void RcxController::setRefreshInterval(int ms) {
if (m_refreshTimer)
m_refreshTimer->setInterval(qMax(1, ms));
}
void RcxController::setupAutoRefresh() {
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
m_refreshTimer = new QTimer(this);
m_refreshTimer->setInterval(660);
m_refreshTimer->setInterval(qMax(1, ms));
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
m_refreshTimer->start();

View File

@@ -113,6 +113,7 @@ public:
RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
void setRefreshInterval(int ms);
// MCP bridge accessors
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
@@ -121,6 +122,9 @@ public:
int activeSourceIndex() const { return m_activeSourceIdx; }
void switchSource(int idx) { switchToSavedSource(idx); }
// Test accessor
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
signals:
void nodeSelected(int nodeIdx);
void selectionChanged(int count);

View File

@@ -5,6 +5,7 @@
#include <Qsci/qsciscintillabase.h>
#include <Qsci/qscilexercpp.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFont>
#include <QColor>
#include <QKeyEvent>
@@ -15,10 +16,142 @@
#include <QMenu>
#include <QApplication>
#include <QClipboard>
#include <QLabel>
#include <QToolButton>
#include <QScreen>
#include <functional>
#include "themes/thememanager.h"
namespace rcx {
// ── Value history popup (styled like TypeSelectorPopup) ──
class ValueHistoryPopup : public QFrame {
uint64_t m_nodeId = 0;
bool m_hasButtons = false;
QStringList m_values;
QVector<QLabel*> m_labels;
std::function<void(const QString&)> m_onSet;
public:
explicit ValueHistoryPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
}
uint64_t nodeId() const { return m_nodeId; }
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
bool showButtons = false) {
QStringList vals;
hist.forEach([&](const QString& v) { vals.append(v); });
if (nodeId == m_nodeId && vals == m_values
&& showButtons == m_hasButtons && isVisible())
return;
// In-place label update when structure unchanged (avoids flicker)
if (nodeId == m_nodeId && vals.size() == m_values.size()
&& vals.size() == m_labels.size()
&& showButtons == m_hasButtons && isVisible()) {
for (int i = 0; i < vals.size(); i++)
m_labels[i]->setText(vals[i]);
m_values = vals;
return;
}
m_nodeId = nodeId;
m_values = vals;
m_hasButtons = showButtons;
m_labels.clear();
delete layout();
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
const auto& theme = ThemeManager::instance().current();
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
setPalette(pal);
auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6);
vbox->setSpacing(2);
auto* title = new QLabel(QStringLiteral("Previous Values"));
QFont bold = font;
bold.setBold(true);
title->setFont(bold);
title->setStyleSheet(QStringLiteral("color: %1;").arg(theme.text.name()));
vbox->addWidget(title);
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
QPalette sp; sp.setColor(QPalette::WindowText, theme.border);
sep->setPalette(sp);
vbox->addWidget(sep);
for (const QString& v : vals) {
auto* row = new QHBoxLayout;
row->setContentsMargins(0, 1, 0, 1);
row->setSpacing(8);
auto* label = new QLabel(v);
label->setFont(font);
label->setStyleSheet(QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
row->addWidget(label, 1);
m_labels.append(label);
if (showButtons) {
auto* setBtn = new QToolButton;
setBtn->setText(QStringLiteral("Set"));
setBtn->setAutoRaise(true);
setBtn->setCursor(Qt::PointingHandCursor);
setBtn->setFont(font);
setBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 1px 4px; }"
"QToolButton:hover { color: %2; background: %3; }")
.arg(theme.textDim.name(), theme.text.name(), theme.hover.name()));
QString val = v;
QObject::connect(setBtn, &QToolButton::clicked, [this, val]() {
if (m_onSet) m_onSet(val);
});
row->addWidget(setBtn);
}
vbox->addLayout(row);
}
adjustSize();
}
void showAt(const QPoint& globalPos) {
if (isVisible()) return;
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - 4;
move(x, y);
show();
}
void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
m_values.clear();
m_labels.clear();
}
};
static constexpr int IND_EDITABLE = 8;
static constexpr int IND_HEX_DIM = 9;
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
@@ -71,6 +204,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
m_sci->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_sci, &QWidget::customContextMenuRequested,
this, [this](const QPoint& pos) {
// Right-click on offset margin → show margin mode menu
int margin0Width = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
if (pos.x() < margin0Width) {
QMenu menu;
auto* actRel = menu.addAction("Relative Offsets (+0x)");
auto* actAbs = menu.addAction("Absolute Addresses");
actRel->setCheckable(true);
actAbs->setCheckable(true);
actRel->setChecked(m_relativeOffsets);
actAbs->setChecked(!m_relativeOffsets);
QAction* chosen = menu.exec(m_sci->mapToGlobal(pos));
if (chosen == actRel && !m_relativeOffsets) {
m_relativeOffsets = true;
reformatMargins();
} else if (chosen == actAbs && m_relativeOffsets) {
m_relativeOffsets = false;
reformatMargins();
}
return;
}
int line = m_sci->lineAt(pos);
int nodeIdx = -1;
int subLine = 0;
@@ -374,6 +528,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
if (m_editState.active)
endInlineEdit();
// Guard: suppress popup dismiss during setText() which fires synthetic Leave events
m_applyingDocument = true;
// Save hover state — setText() triggers viewport Leave events that would clear it
uint64_t savedHoverId = m_hoveredNodeId;
int savedHoverLine = m_hoveredLine;
@@ -422,6 +579,13 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_hoveredNodeId = savedHoverId;
m_hoveredLine = savedHoverLine;
m_hoverInside = savedHoverInside;
m_applyingDocument = false;
// Re-apply hover markers (setText() clears all Scintilla markers).
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
// composed text that updateCommandRow() will overwrite. The correct call
// happens via applySelectionOverlays() after all text is finalized.
applyHoverHighlight();
}
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
@@ -787,31 +951,35 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
int typeW = lm.effectiveTypeW;
int nameW = lm.effectiveNameW;
// For hex preview nodes: use dataChanged + changedByteIndices (per-byte heat)
if (heat <= 0) continue;
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
// For hex preview nodes: per-byte heat coloring on changed bytes
if (isHexPreview(lm.nodeKind) && lm.dataChanged && !lm.changedByteIndices.isEmpty()) {
// Hex nodes don't track heatLevel (they're skipped in controller).
// Use IND_HEAT_COLD for any changed byte (simple visual feedback).
int ind = kFoldCol + lm.depth * 3;
int asciiStart = ind + typeW + kSepWidth;
int hexStart = asciiStart + nameW + kSepWidth;
for (int byteIdx : lm.changedByteIndices) {
fillIndicatorCols(IND_HEAT_COLD, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
fillIndicatorCols(activeInd, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
int hexCol = hexStart + byteIdx * 3;
fillIndicatorCols(IND_HEAT_COLD, i, hexCol, hexCol + 2);
fillIndicatorCols(activeInd, i, hexCol, hexCol + 2);
}
// Clear the other two heat indicators on this line
for (int hi : heatIndicators) {
if (hi != activeInd)
clearIndicatorLine(hi, i);
}
continue;
}
// Non-hex nodes: apply heat-level indicator to value span
if (heat <= 0) continue;
QString lineText = getLineText(m_sci, i);
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
if (!vs.valid) continue;
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
fillIndicatorCols(activeInd, i, vs.start, vs.end);
// Clear the other two heat indicators on this span to avoid overlap
@@ -1478,6 +1646,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
}
// Track mouse position for cursor updates (both edit and non-edit mode)
if (obj == m_sci->viewport()) {
// Ignore synthetic Leave from setText() during document refresh
if (m_applyingDocument && event->type() == QEvent::Leave)
return true;
if (event->type() == QEvent::MouseMove) {
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
m_hoverInside = true;
@@ -1683,6 +1855,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_hoveredNodeId = 0;
m_hoveredLine = -1;
applyHoverHighlight();
// Dismiss hover popup so it gets recreated with Set buttons once edit starts
if (m_historyPopup)
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
// Clear editable-token color hints (de-emphasize non-active tokens)
clearIndicatorLine(IND_EDITABLE, m_hintLine);
m_hintLine = -1;
@@ -1864,6 +2039,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_sci->viewport()->setCursor(Qt::ArrowCursor);
});
}
// Refresh hover cursor so value history popup appears with Set buttons immediately
if (target == EditTarget::Value)
QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor);
return true;
}
@@ -2216,25 +2394,60 @@ void RcxEditor::applyHoverCursor() {
if (m_editState.active) {
if (m_sci->isListActive()) {
m_sci->viewport()->setCursor(Qt::ArrowCursor);
return;
}
auto h = hitTest(m_lastHoverPos);
if (h.line == m_editState.line &&
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
m_sci->viewport()->setCursor(Qt::IBeamCursor);
} else {
m_sci->viewport()->setCursor(Qt::ArrowCursor);
auto h = hitTest(m_lastHoverPos);
if (h.line == m_editState.line &&
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
m_sci->viewport()->setCursor(Qt::IBeamCursor);
} else {
m_sci->viewport()->setCursor(Qt::ArrowCursor);
}
}
// Value history popup — only during inline value editing on a heated node
{
bool showPopup = false;
if (m_valueHistory && m_editState.target == EditTarget::Value
&& m_editState.line >= 0 && m_editState.line < m_meta.size()) {
const LineMeta& lm = m_meta[m_editState.line];
if (lm.heatLevel > 0 && lm.nodeId != 0) {
auto it = m_valueHistory->find(lm.nodeId);
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
if (!m_historyPopup)
m_historyPopup = new ValueHistoryPopup(this);
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
popup->setOnSet([this](const QString& val) {
if (!m_editState.active) return;
long endPos = posFromCol(m_sci, m_editState.line, editEndCol());
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
m_editState.posStart, endPos);
QByteArray utf8 = val.toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
(uintptr_t)0, utf8.constData());
});
popup->populate(lm.nodeId, *it, editorFont(), true);
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
(unsigned long)0, m_editState.posStart);
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
(unsigned long)0, m_editState.posStart);
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
(unsigned long)m_editState.line);
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
popup->showAt(anchor);
showPopup = true;
}
}
}
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
}
return;
}
// Mouse left viewport - set Arrow, cancel calltip
// Mouse left viewport - set Arrow, dismiss history popup
// (but not during applyDocument — the Leave is synthetic from setText)
if (!m_hoverInside) {
if (m_calltipVisible) {
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPCANCEL);
m_calltipVisible = false;
m_calltipLine = -1;
}
if (m_historyPopup && !m_applyingDocument)
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
m_sci->viewport()->setCursor(Qt::ArrowCursor);
return;
}
@@ -2323,41 +2536,39 @@ void RcxEditor::applyHoverCursor() {
m_hoverSpanLines.append(h.line);
}
// Value history calltip on hover
// Value history popup on hover (read-only, no buttons)
{
bool showCalltip = false;
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size() && !m_editState.active) {
bool showPopup = false;
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size()) {
const LineMeta& lm = m_meta[h.line];
if (lm.heatLevel > 0 && lm.nodeId != 0) {
auto it = m_valueHistory->find(lm.nodeId);
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
// Check cursor is over the value span
QString lineText = getLineText(m_sci, h.line);
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
QString tip = QStringLiteral("Previous Values:");
it->forEach([&](const QString& v) {
tip += QStringLiteral("\n ") + v;
});
if (m_calltipLine != h.line) {
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
if (!m_historyPopup)
m_historyPopup = new ValueHistoryPopup(this);
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
popup->populate(lm.nodeId, *it, editorFont(), false);
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)h.line);
QByteArray tipUtf8 = tip.toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPSHOW,
pos, tipUtf8.constData());
m_calltipLine = h.line;
m_calltipVisible = true;
}
showCalltip = true;
long byteOff = lineText.left(vs.start).toUtf8().size();
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
(unsigned long)0, linePos + byteOff);
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
(unsigned long)0, linePos);
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
(unsigned long)h.line);
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
popup->showAt(anchor);
showPopup = true;
}
}
}
}
if (!showCalltip && m_calltipVisible) {
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPCANCEL);
m_calltipVisible = false;
m_calltipLine = -1;
}
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
}
// Determine cursor shape based on interaction type

View File

@@ -132,10 +132,10 @@ private:
// ── Value history ref (owned by controller) ──
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
bool m_calltipVisible = false;
int m_calltipLine = -1;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
// ── Reentrancy guards ──
bool m_applyingDocument = false;
bool m_clampingSelection = false;
bool m_updatingComment = false;

View File

@@ -1,344 +0,0 @@
{
"baseAddress": "400000",
"nextId": "29",
"nodes": [
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "1",
"kind": "Struct",
"name": "aBall",
"offset": 0,
"parentId": "0",
"refId": "0",
"strLen": 64,
"structTypeName": "ball"
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "2",
"kind": "Hex64",
"name": "field_00",
"offset": 0,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "3",
"kind": "Hex64",
"name": "field_08",
"offset": 8,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "4",
"kind": "Vec4",
"name": "position",
"offset": 16,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "5",
"kind": "Vec3",
"name": "velocity",
"offset": 32,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "6",
"kind": "Hex32",
"name": "field_2C",
"offset": 44,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "7",
"kind": "Float",
"name": "speed",
"offset": 48,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "8",
"kind": "UInt32",
"name": "color",
"offset": 52,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "9",
"kind": "Float",
"name": "radius",
"offset": 56,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "10",
"kind": "Hex32",
"name": "field_3C",
"offset": 60,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "11",
"kind": "Float",
"name": "mass",
"offset": 64,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "12",
"kind": "Hex64",
"name": "field_44",
"offset": 68,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "13",
"kind": "Bool",
"name": "bouncy",
"offset": 76,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "14",
"kind": "Hex8",
"name": "field_4D",
"offset": 77,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "15",
"kind": "Hex16",
"name": "field_4E",
"offset": 78,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "16",
"kind": "UInt32",
"name": "color",
"offset": 80,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "17",
"kind": "Hex32",
"name": "field_54",
"offset": 84,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "18",
"kind": "Hex64",
"name": "field_58",
"offset": 88,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "19",
"kind": "Hex64",
"name": "field_60",
"offset": 96,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "20",
"kind": "Struct",
"name": "aPhysics",
"offset": 0,
"parentId": "0",
"refId": "0",
"strLen": 64,
"structTypeName": "Physics"
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "21",
"kind": "Hex64",
"name": "field_00",
"offset": 0,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "22",
"kind": "Hex64",
"name": "field_08",
"offset": 8,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "23",
"kind": "Hex64",
"name": "field_10",
"offset": 16,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "24",
"kind": "Hex64",
"name": "field_18",
"offset": 24,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "25",
"kind": "Hex64",
"name": "field_20",
"offset": 32,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": true,
"elementKind": "UInt8",
"id": "26",
"kind": "Pointer64",
"name": "physics",
"offset": 104,
"parentId": "1",
"refId": "20",
"strLen": 64
},
{
"arrayLen": 4,
"collapsed": false,
"elementKind": "Float",
"id": "27",
"kind": "Array",
"name": "scores",
"offset": 112,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 2,
"collapsed": false,
"elementKind": "Struct",
"id": "28",
"kind": "Array",
"name": "materials",
"offset": 128,
"parentId": "1",
"refId": "20",
"strLen": 64
}
]
}

View File

@@ -412,6 +412,18 @@ void MainWindow::createMenus() {
Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
// Examples submenu — scan once at init
{
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
if (!rcxFiles.isEmpty()) {
auto* examples = file->addMenu("&Examples");
for (const QString& fn : rcxFiles) {
QString fullPath = exDir.absoluteFilePath(fn);
examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); });
}
}
}
file->addSeparator();
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
@@ -732,77 +744,22 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
return sub;
}
// Build Ball + Material demo structs into a tree
static void buildBallDemo(NodeTree& tree) {
// Ball struct (128 bytes = 0x80)
Node ball;
ball.kind = NodeKind::Struct;
ball.name = "aBall";
ball.structTypeName = "Ball";
ball.parentId = 0;
ball.offset = 0;
int bi = tree.addNode(ball);
uint64_t ballId = tree.nodes[bi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; tree.addNode(n); }
// Material struct (renamed from Physics, 40 bytes = 0x28)
Node mat;
mat.kind = NodeKind::Struct;
mat.name = "aMaterial";
mat.structTypeName = "Material";
mat.parentId = 0;
mat.offset = 0;
int mi = tree.addNode(mat);
uint64_t matId = tree.nodes[mi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = matId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = matId; n.offset = 8; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = matId; n.offset = 16; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = matId; n.offset = 24; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = matId; n.offset = 32; tree.addNode(n); }
// Pointer to Material in Ball struct
{ Node n; n.kind = NodeKind::Pointer64; n.name = "material"; n.parentId = ballId; n.offset = 104; n.refId = matId; n.collapsed = true; tree.addNode(n); }
// float[4] scores at offset 112
{ Node n; n.kind = NodeKind::Array; n.name = "scores"; n.parentId = ballId; n.offset = 112; n.elementKind = NodeKind::Float; n.arrayLen = 4; tree.addNode(n); }
// Material[2] materials at offset 128 (112 + 16 for float[4])
{ Node n; n.kind = NodeKind::Array; n.name = "materials"; n.parentId = ballId; n.offset = 128; n.elementKind = NodeKind::Struct; n.arrayLen = 2; n.refId = matId; tree.addNode(n); }
// Unnamed struct (128 bytes of hex64 fields)
Node unnamed;
unnamed.kind = NodeKind::Struct;
unnamed.name = "instance";
unnamed.structTypeName = "Unnamed";
unnamed.parentId = 0;
unnamed.offset = 0;
int ui = tree.addNode(unnamed);
uint64_t unnamedId = tree.nodes[ui].id;
// Build a minimal empty struct for new documents
static void buildEmptyStruct(NodeTree& tree) {
Node root;
root.kind = NodeKind::Struct;
root.name = "instance";
root.structTypeName = "Unnamed";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
for (int i = 0; i < 16; i++) {
Node n;
n.kind = NodeKind::Hex64;
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
n.parentId = unnamedId;
n.parentId = rootId;
n.offset = i * 8;
tree.addNode(n);
}
@@ -829,14 +786,12 @@ void MainWindow::newDocument() {
doc->typeAliases.clear();
doc->modified = false;
// Build Ball + Material structs
buildBallDemo(doc->tree);
buildEmptyStruct(doc->tree);
// Cross-platform writable buffer, zeroed (256 bytes covers Ball + spare)
QByteArray data(256, '\0');
doc->provider = std::make_shared<BufferProvider>(data);
// Focus on Ball struct
// Focus on first struct
ctrl->setViewRootId(0);
for (const auto& n : doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
@@ -854,7 +809,22 @@ void MainWindow::newDocument() {
}
void MainWindow::selfTest() {
project_new();
// Auto-open KUSER_SHARED_DATA example if available
QString exPath = QCoreApplication::applicationDirPath()
+ "/examples/KUSER_SHARED_DATA.rcx";
if (QFile::exists(exPath)) {
project_open(exPath);
} else {
project_new();
}
// Auto-attach process memory plugin to self
auto* ctrl = activeController();
if (ctrl) {
DWORD pid = GetCurrentProcessId();
QString target = QString("%1:Reclass.exe").arg(pid);
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
}
}
void MainWindow::openFile() {
@@ -1080,6 +1050,7 @@ void MainWindow::showOptionsDialog() {
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
OptionsDialog dlg(current, this);
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
@@ -1107,6 +1078,12 @@ void MainWindow::showOptionsDialog() {
if (r.autoStartMcp != current.autoStartMcp)
QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp);
if (r.refreshMs != current.refreshMs) {
QSettings("Reclass", "Reclass").setValue("refreshMs", r.refreshMs);
for (auto& tab : m_tabs)
tab.ctrl->setRefreshInterval(r.refreshMs);
}
}
void MainWindow::setEditorFont(const QString& fontName) {
@@ -1510,13 +1487,11 @@ void MainWindow::showTypeAliasesDialog() {
QMdiSubWindow* MainWindow::project_new() {
auto* doc = new RcxDocument(this);
// Cross-platform writable buffer, zeroed (256 bytes covers Ball struct + spare)
QByteArray data(256, '\0');
doc->loadData(data);
doc->tree.baseAddress = 0x00400000;
// Build Ball + Material demo structs
buildBallDemo(doc->tree);
buildEmptyStruct(doc->tree);
auto* sub = createTab(doc);
rebuildWorkspaceModel();

View File

@@ -58,6 +58,29 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
generalLayout->setContentsMargins(0, 0, 0, 0);
generalLayout->setSpacing(8);
// Refresh Rate group box
auto* refreshGroup = new QGroupBox("Refresh Rate");
auto* refreshLayout = new QFormLayout(refreshGroup);
refreshLayout->setSpacing(8);
refreshLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_refreshSpin = new QSpinBox;
m_refreshSpin->setRange(1, 60000);
m_refreshSpin->setSingleStep(50);
m_refreshSpin->setValue(current.refreshMs);
m_refreshSpin->setSuffix(" ms");
m_refreshSpin->setObjectName("refreshSpin");
refreshLayout->addRow("Interval:", m_refreshSpin);
auto* refreshDesc = new QLabel(
"How often live memory is re-read and the view is updated, in milliseconds. "
"Lower values give faster updates but use more CPU. Default: 660 ms.");
refreshDesc->setWordWrap(true);
refreshDesc->setContentsMargins(0, 0, 0, 0);
refreshLayout->addRow(refreshDesc);
generalLayout->addWidget(refreshGroup);
// Visual Experience group box
auto* visualGroup = new QGroupBox("Visual Experience");
auto* visualLayout = new QFormLayout(visualGroup);
@@ -184,6 +207,7 @@ OptionsResult OptionsDialog::result() const {
r.showIcon = m_showIconCheck->isChecked();
r.safeMode = m_safeModeCheck->isChecked();
r.autoStartMcp = m_autoMcpCheck->isChecked();
r.refreshMs = m_refreshSpin->value();
return r;
}

View File

@@ -6,6 +6,7 @@
#include <QComboBox>
#include <QCheckBox>
#include <QHash>
#include <QSpinBox>
namespace rcx {
@@ -16,6 +17,7 @@ struct OptionsResult {
bool showIcon = false;
bool safeMode = false;
bool autoStartMcp = false;
int refreshMs = 660;
};
class OptionsDialog : public QDialog {
@@ -38,6 +40,7 @@ private:
QCheckBox* m_showIconCheck = nullptr;
QCheckBox* m_safeModeCheck = nullptr;
QCheckBox* m_autoMcpCheck = nullptr;
QSpinBox* m_refreshSpin = nullptr;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;

View File

@@ -22,7 +22,7 @@
"indHoverSpan": "#E6B450",
"indCmdPill": "#2a2a2a",
"indDataChanged": "#8fbc7a",
"indHeatCold": "#569cd6",
"indHeatCold": "#D4A945",
"indHeatWarm": "#E6B450",
"indHeatHot": "#f44747",
"indHintGreen": "#5a8248",

View File

@@ -22,7 +22,7 @@
"indHoverSpan": "#b180d7",
"indCmdPill": "#2d2d30",
"indDataChanged": "#8fbc7a",
"indHeatCold": "#569cd6",
"indHeatCold": "#D4A945",
"indHeatWarm": "#d69d85",
"indHeatHot": "#f44747",
"indHintGreen": "#5a8248",

View File

@@ -22,7 +22,7 @@
"indHoverSpan": "#AA9565",
"indCmdPill": "#2a2a2a",
"indDataChanged": "#6B959F",
"indHeatCold": "#6B959F",
"indHeatCold": "#C4A44A",
"indHeatWarm": "#AA9565",
"indHeatHot": "#A05040",
"indHintGreen": "#464646",

View File

@@ -54,9 +54,9 @@ Theme Theme::fromJson(const QJsonObject& o) {
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
}
// Derive heat colors from the theme's own palette when keys are absent
// cold = keyword blue, warm = hover/string amber, hot = marker red
// cold = muted yellow, warm = hover/string amber, hot = marker red
if (!t.indHeatCold.isValid())
t.indHeatCold = t.syntaxKeyword;
t.indHeatCold = QColor("#D4A945");
if (!t.indHeatWarm.isValid())
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
if (!t.indHeatHot.isValid())

View File

@@ -336,6 +336,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
m_arrayCountEdit->setVisible(id == 3);
if (id == 3) m_arrayCountEdit->setFocus();
updateModifierPreview();
applyFilter(m_filterEdit->text());
});
connect(m_arrayCountEdit, &QLineEdit::textChanged,
this, [this]() { updateModifierPreview(); });
@@ -562,6 +563,10 @@ 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
QVector<TypeEntry> primitives, composites;
for (const auto& t : m_allTypes) {
@@ -571,9 +576,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
if (!matchesFilter) continue;
if (t.entryKind == TypeEntry::Primitive)
primitives.append(t);
else if (t.entryKind == TypeEntry::Composite)
if (t.entryKind == TypeEntry::Primitive) {
if (!hideprimitives)
primitives.append(t);
} else if (t.entryKind == TypeEntry::Composite)
composites.append(t);
}