feat: tree lines, scanner improvements, themes, tooltips, README overhaul

- Tree line connectors (Unicode box-drawing ├─ └─ │) at arbitrary depth
- Fix editor overwriting tree chars at depth 2+ (applyMarginText Pass 2)
- Scanner: unknown value scan, comparison rescan modes (Changed/Unchanged/Increased/Decreased)
- New Tailwind theme (tw.json), WCAG contrast fixes for warm/mid themes
- Tooltip system (rcxtooltip.h)
- Comprehensive README rewrite with full feature inventory
- New tests for compose tree lines, scanner, tooltips

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IChooseYou
2026-03-04 09:20:56 -07:00
parent 0dc4af6b1d
commit e999c664b8
25 changed files with 4024 additions and 2492 deletions

View File

@@ -24,6 +24,8 @@ struct ComposeState {
int offsetHexDigits = 8; // hex digit tier for offset margin
bool baseEmitted = false; // only first root struct shows base address
bool compactColumns = false; // compact column mode: cap type width, overflow long types
bool treeLines = false; // draw Unicode tree connectors in indentation
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
// Precomputed for O(1) lookups
@@ -41,6 +43,15 @@ struct ComposeState {
return scopeNameW.value(scopeId, nameW);
}
// Set sibling-continuation flag for children at the given depth.
// childDepth is the depth of the children being iterated.
void setTreeSibling(int childDepth, bool hasMoreSiblings) {
if (!treeLines) return;
int d = childDepth - 1;
while (siblingStack.size() <= d) siblingStack.append(false);
siblingStack[d] = hasMoreSiblings;
}
void emitLine(const QString& lineText, LineMeta lm) {
if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
@@ -52,7 +63,29 @@ struct ComposeState {
text += lm.foldCollapsed ? QStringLiteral(" \u25B8 ") : QStringLiteral(" \u25BE ");
else
text += QStringLiteral(" ");
text += lineText;
// Replace leading indent spaces with Unicode tree connectors
if (treeLines && lm.depth > 0) {
QString treeIndent;
int D = lm.depth;
bool isFooter = (lm.lineKind == LineKind::Footer);
for (int d = 0; d < D; d++) {
bool active = (d < siblingStack.size() && siblingStack[d]);
if (isFooter || d < D - 1) {
// Ancestor continuation or footer's own level
treeIndent += active ? QStringLiteral("\u2502 ")
: QStringLiteral(" ");
} else {
// This node's own connector (non-footer only)
treeIndent += active ? QStringLiteral("\u251C\u2500 ")
: QStringLiteral("\u2514\u2500 ");
}
}
text += treeIndent + lineText.mid(D * 3);
} else {
text += lineText;
}
meta.append(lm);
currentLine++;
}
@@ -305,6 +338,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
});
for (int oi = 0; oi < order.size(); oi++) {
state.setTreeSibling(childDepth, oi < order.size() - 1);
int mi = order[oi];
const auto& m = node.enumMembers[mi];
LineMeta lm;
@@ -353,6 +387,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
maxNameLen = qMax(maxNameLen, (int)m.name.size());
for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) {
state.setTreeSibling(childDepth, mi < node.bitfieldMembers.size() - 1);
const auto& m = node.bitfieldMembers[mi];
uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind,
m.bitOffset, m.bitWidth);
@@ -415,6 +450,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
int eTW = state.effectiveTypeW(node.id);
int eNW = state.effectiveNameW(node.id);
for (int i = 0; i < node.arrayLen; i++) {
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
uint64_t elemAddr = absAddr + i * elemSize;
// Type override: "float[0]", "uint32_t[1]", etc.
@@ -460,6 +496,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
int elemSize = tree.structSpan(node.refId, &state.childMap);
if (elemSize <= 0) elemSize = 1;
for (int i = 0; i < node.arrayLen; i++) {
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
uint64_t elemBase = absAddr + (uint64_t)i * elemSize;
// Use base offset that maps refStruct's children to the right provider address
composeParent(state, tree, prov, refIdx, childDepth, elemBase, node.refId,
@@ -476,7 +513,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
const QVector<int>& refChildren = childIndices(state, node.refId);
// Use the referenced struct's scope widths (children come from there)
uint64_t refScopeId = node.refId;
for (int childIdx : refChildren) {
for (int rci = 0; rci < refChildren.size(); rci++) {
int childIdx = refChildren[rci];
state.setTreeSibling(childDepth, rci < refChildren.size() - 1);
const Node& child = tree.nodes[childIdx];
// Self-referential child → show as collapsed struct (non-expandable)
if (state.visiting.contains(child.id)) {
@@ -514,7 +553,13 @@ void composeParent(ComposeState& state, const NodeTree& tree,
// For arrays, render children as condensed (no header/footer for struct elements)
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
int elementIdx = 0;
for (int childIdx : regular) {
for (int ri = 0; ri < regular.size(); ri++) {
int childIdx = regular[ri];
// A regular child has more siblings if there are more regular children
// or if static fields follow after all regular children
bool hasMore = (ri < regular.size() - 1)
|| (!staticIdxs.isEmpty() && !node.collapsed);
state.setTreeSibling(childDepth, hasMore);
// Pass this container's id as the scope for children (for per-scope widths)
// For array elements, also pass the element index for [N] separator
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
@@ -569,7 +614,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
auto cbs = makeResolver(absAddr);
for (int si : staticIdxs) {
for (int sii = 0; sii < staticIdxs.size(); sii++) {
int si = staticIdxs[sii];
state.setTreeSibling(childDepth, sii < staticIdxs.size() - 1);
const Node& sf = tree.nodes[si];
// Evaluate expression → absolute address
@@ -639,8 +686,18 @@ void composeParent(ComposeState& state, const NodeTree& tree,
// ── Body + children (only when expanded) ──
if (!isCollapsed) {
// Determine if struct children follow the body line
bool hasStructKids = exprOk
&& (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array);
const QVector<int> staticKids = hasStructKids
? childIndices(state, sf.id) : QVector<int>();
hasStructKids = hasStructKids && !staticKids.isEmpty();
// Body line: " return <expr> → 0xADDR"
{
// Body has more siblings if struct children follow
state.setTreeSibling(childDepth + 1, hasStructKids);
QString bodyLine;
if (!sf.offsetExpr.isEmpty()) {
if (exprOk)
@@ -676,10 +733,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
}
// If struct/array, compose children at evaluated address
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
const QVector<int>& staticKids = childIndices(state, sf.id);
for (int sci : staticKids) {
composeNode(state, tree, prov, sci, childDepth + 1,
if (hasStructKids) {
for (int ski = 0; ski < staticKids.size(); ski++) {
state.setTreeSibling(childDepth + 1, ski < staticKids.size() - 1);
composeNode(state, tree, prov, staticKids[ski], childDepth + 1,
staticAddr, sf.id, false, sf.id);
}
}
@@ -818,8 +875,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
// Render materialized children at the pointer target address.
// These are real tree nodes with independent state — use rootId
// so resolveAddr computes offsets relative to the pointer target.
for (int childIdx : ptrChildren) {
composeNode(state, tree, childProv, childIdx, depth + 1,
for (int pci = 0; pci < ptrChildren.size(); pci++) {
state.setTreeSibling(depth + 1, pci < ptrChildren.size() - 1);
composeNode(state, tree, childProv, ptrChildren[pci], depth + 1,
pBase, node.id, false, node.id);
}
} else {
@@ -878,9 +936,10 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
bool compactColumns) {
bool compactColumns, bool treeLines) {
ComposeState state;
state.compactColumns = compactColumns;
state.treeLines = treeLines;
// Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++)
@@ -1026,7 +1085,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
composeNode(state, tree, prov, idx, 0);
}
return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress} };
return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress, treeLines} };
}
QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {

View File

@@ -72,8 +72,9 @@ RcxDocument::RcxDocument(QObject* parent)
});
}
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns);
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
bool treeLines) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines);
}
bool RcxDocument::save(const QString& path) {
@@ -319,7 +320,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
// Regular type change
bool ok;
NodeKind k = kindFromTypeName(text, &ok);
if (ok) {
if (ok && k != NodeKind::Struct && k != NodeKind::Array) {
changeNodeKind(nodeIdx, k);
} else if (nodeIdx < m_doc->tree.nodes.size()) {
// Check if it's a defined struct type name
@@ -546,6 +547,7 @@ void RcxController::resetChangeTracking() {
m_changedOffsets.clear();
m_valueHistory.clear();
m_prevPages.clear();
m_valueTrackCooldown = 5; // suppress tracking for ~1s
for (auto& lm : m_lastResult.meta)
lm.heatLevel = 0;
}
@@ -556,9 +558,9 @@ void RcxController::refresh() {
// Compose against snapshot provider if active, otherwise real provider
if (m_snapshotProv)
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns);
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines);
else
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns);
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines);
s_composeDoc = nullptr;
@@ -602,7 +604,8 @@ void RcxController::refresh() {
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
prov = m_doc->provider.get();
if (m_trackValues && prov) {
if (m_valueTrackCooldown > 0) --m_valueTrackCooldown;
if (m_trackValues && prov && m_valueTrackCooldown <= 0) {
for (auto& lm : m_lastResult.meta) {
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
if (isSyntheticLine(lm) || lm.isContinuation) continue;
@@ -1710,6 +1713,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
m_refreshGen++; // discard in-flight async reads
m_prevPages.clear(); // clean baseline for next read cycle
m_changedOffsets.clear(); // no phantom change indicators
m_valueTrackCooldown = 5; // suppress tracking for ~1s
refresh();
for (auto* editor : m_editors)
editor->dismissHistoryPopup();
@@ -1935,6 +1939,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
m_refreshGen++; // discard in-flight async reads
m_prevPages.clear(); // clean baseline for next read cycle
m_changedOffsets.clear(); // no phantom change indicators
m_valueTrackCooldown = 5; // suppress tracking for ~1s
refresh();
for (auto* editor : m_editors)
editor->dismissHistoryPopup();
@@ -2608,7 +2613,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
break;
case TypePopupMode::FieldType: {
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
bool isPtr = node
&& (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64);
bool isTypedPtr = isPtr && node->refId != 0;
@@ -3181,6 +3186,11 @@ void RcxController::setCompactColumns(bool v) {
refresh();
}
void RcxController::setTreeLines(bool v) {
m_treeLines = v;
refresh();
}
void RcxController::setupAutoRefresh() {
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
m_refreshTimer = new QTimer(this);

View File

@@ -40,7 +40,8 @@ public:
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
bool treeLines = false) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -128,6 +129,7 @@ public:
void setEditorFont(const QString& fontName);
void setRefreshInterval(int ms);
void setCompactColumns(bool v);
void setTreeLines(bool v);
void resetProvider();
// MCP bridge accessors
@@ -151,6 +153,7 @@ public:
// Test accessors
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
const ComposeResult& lastResult() const { return m_lastResult; }
int dataExtent() const { return computeDataExtent(); }
signals:
void nodeSelected(int nodeIdx);
@@ -164,6 +167,7 @@ private:
int m_anchorLine = -1;
bool m_suppressRefresh = false;
bool m_compactColumns = false;
bool m_treeLines = false;
uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ──
@@ -183,6 +187,7 @@ private:
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = true;
int m_valueTrackCooldown = 0; // suppress value recording for N refresh cycles after clear
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;

View File

@@ -86,8 +86,8 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector},
{NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector},
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
{NodeKind::UTF8, "UTF8", "str", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wstr", 2, 1, 2, KF_String},
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
};
@@ -153,14 +153,11 @@ inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
return true;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
inline QStringList allTypeNamesForUI(bool /*stripBrackets*/ = false) {
QStringList out;
out.reserve(std::size(kKindMeta));
for (const auto& m : kKindMeta) {
QString t = QString::fromLatin1(m.typeName);
if (stripBrackets) t.remove(QStringLiteral("[]"));
out << t;
}
for (const auto& m : kKindMeta)
out << QString::fromLatin1(m.typeName);
out.sort(Qt::CaseInsensitive);
out.removeDuplicates();
return out;
@@ -636,6 +633,7 @@ struct LayoutInfo {
int nameW = 22; // Effective name column width (default = kColName)
int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16)
uint64_t baseAddress = 0; // Base address for relative offset computation
bool treeLines = false; // Whether tree line connectors are embedded in the text
};
// ── ComposeResult ──
@@ -1033,6 +1031,6 @@ namespace fmt {
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false);
bool compactColumns = false, bool treeLines = false);
} // namespace rcx

View File

@@ -156,7 +156,7 @@ public:
adjustSize();
}
void showAt(const QPoint& globalPos) {
void showAt(const QPoint& globalPos, int lineHeight = 0) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
@@ -164,7 +164,7 @@ public:
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;
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
@@ -257,7 +257,7 @@ public:
adjustSize();
}
void showAt(const QPoint& globalPos) {
void showAt(const QPoint& globalPos, int lineHeight = 0) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
@@ -265,7 +265,7 @@ public:
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;
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
@@ -354,7 +354,7 @@ public:
adjustSize();
}
void showAt(const QPoint& globalPos) {
void showAt(const QPoint& globalPos, int lineHeight = 0) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
@@ -362,7 +362,7 @@ public:
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;
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
@@ -866,7 +866,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
m_sci->setMarkerBackgroundColor(theme.background, M_CYCLE);
m_sci->setMarkerForegroundColor(theme.background, M_CYCLE);
m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR);
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
m_sci->setMarkerForegroundColor(theme.text, M_ERR);
m_sci->setMarkerBackgroundColor(theme.background, M_STRUCT_BG);
m_sci->setMarkerForegroundColor(theme.text, M_STRUCT_BG);
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
@@ -1061,6 +1061,11 @@ void RcxEditor::reformatMargins() {
}
// ── Pass 2: inline local offsets in the text indent area ──
// Skip when tree lines are active — the compose step already placed
// Unicode tree connectors in the indent area; overwriting with spaces
// or offsets would destroy them.
if (m_layout.treeLines)
return;
m_sci->setReadOnly(false);
for (int i = 0; i < m_meta.size(); i++) {
const auto& lm = m_meta[i];
@@ -2204,11 +2209,25 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
m_hoverInside = true;
} else if (event->type() == QEvent::Leave) {
m_hoverInside = false;
if (!m_editState.active) {
m_hoveredNodeId = 0;
m_hoveredLine = -1;
applyHoverHighlight();
// Don't dismiss if cursor moved onto one of our own popups
QPoint globalCursor = QCursor::pos();
bool onPopup = false;
if (m_historyPopup && m_historyPopup->isVisible()
&& m_historyPopup->geometry().contains(globalCursor))
onPopup = true;
if (m_disasmPopup && m_disasmPopup->isVisible()
&& m_disasmPopup->geometry().contains(globalCursor))
onPopup = true;
if (m_structPreviewPopup && m_structPreviewPopup->isVisible()
&& m_structPreviewPopup->geometry().contains(globalCursor))
onPopup = true;
if (!onPopup) {
m_hoverInside = false;
if (!m_editState.active) {
m_hoveredNodeId = 0;
m_hoveredLine = -1;
applyHoverHighlight();
}
}
} else if (event->type() == QEvent::Wheel) {
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
@@ -2992,7 +3011,7 @@ void RcxEditor::applyHoverCursor() {
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);
popup->showAt(anchor, lh);
showPopup = true;
}
}
@@ -3147,7 +3166,7 @@ void RcxEditor::applyHoverCursor() {
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);
popup->showAt(anchor, lh);
showPopup = true;
}
}
@@ -3240,7 +3259,7 @@ void RcxEditor::applyHoverCursor() {
(unsigned long)h.line);
QPoint anchor = m_sci->viewport()->mapToGlobal(
QPoint(px, py + lh));
popup->showAt(anchor);
popup->showAt(anchor, lh);
showDisasm = true;
// Dismiss value history popup to avoid fighting
if (m_historyPopup && m_historyPopup->isVisible())
@@ -3307,7 +3326,7 @@ void RcxEditor::applyHoverCursor() {
(unsigned long)h.line);
QPoint anchor = m_sci->viewport()->mapToGlobal(
QPoint(px, py + lh));
popup->showAt(anchor);
popup->showAt(anchor, lh);
showPreview = true;
if (m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();

143
src/examples/Demo.rcx Normal file
View File

@@ -0,0 +1,143 @@
{
"baseAddress": "0",
"nextId": "20",
"nodes": [
{
"id": "1",
"kind": "Struct",
"name": "player",
"structTypeName": "PlayerEntity",
"classKeyword": "class",
"parentId": "0",
"offset": 0,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "2",
"kind": "Pointer64",
"name": "__vptr",
"parentId": "1",
"offset": 0,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "3",
"kind": "Int32",
"name": "health",
"parentId": "1",
"offset": 8,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "4",
"kind": "Int32",
"name": "armor",
"parentId": "1",
"offset": 12,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "5",
"kind": "Float",
"name": "pos_x",
"parentId": "1",
"offset": 16,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "6",
"kind": "Float",
"name": "pos_y",
"parentId": "1",
"offset": 20,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "7",
"kind": "Float",
"name": "pos_z",
"parentId": "1",
"offset": 24,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "8",
"kind": "Hex32",
"name": "pad_1C",
"parentId": "1",
"offset": 28,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "9",
"kind": "Pointer64",
"name": "name",
"parentId": "1",
"offset": 32,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64,
"ptrDepth": 1
},
{
"id": "10",
"kind": "UInt64",
"name": "flags",
"parentId": "1",
"offset": 40,
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
},
{
"id": "11",
"kind": "Hex64",
"name": "static_field",
"parentId": "1",
"offset": 0,
"isStatic": true,
"offsetExpr": "base + pos_x",
"collapsed": false,
"refId": "0",
"elementKind": "UInt8",
"arrayLen": 1,
"strLen": 64
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -236,6 +236,14 @@ public:
class MenuBarStyle : public QProxyStyle {
public:
using QProxyStyle::QProxyStyle;
void polish(QWidget* w) override {
// Strip OS window border/shadow from QMenu popups — we draw our own
// 1px border in PE_FrameMenu. Same pattern as TypeSelectorPopup.
if (qobject_cast<QMenu*>(w))
w->setWindowFlag(Qt::FramelessWindowHint, true);
QProxyStyle::polish(w);
}
using QProxyStyle::polish;
QSize sizeFromContents(ContentsType type, const QStyleOption* opt,
const QSize& sz, const QWidget* w) const override {
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
@@ -247,9 +255,12 @@ public:
}
int pixelMetric(PixelMetric metric, const QStyleOption* opt,
const QWidget* w) const override {
// Kill the 1px frame margin Fusion reserves around QMenu contents
// Reserve 1px for our own menu border (drawn in PE_FrameMenu)
if (metric == PM_MenuPanelWidth)
return 0;
return 1;
// Inset menu items from border so hover rect doesn't touch edges
if (metric == PM_MenuHMargin)
return 3;
// Thin draggable separator between dock widgets / central widget
if (metric == PM_DockWidgetSeparatorExtent)
return 1;
@@ -257,9 +268,13 @@ public:
}
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
QPainter* p, const QWidget* w) const override {
// Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough
if (elem == PE_FrameMenu)
// Clean 1px border on QMenu (replaces Fusion's 3D bevel + OS shadow)
if (elem == PE_FrameMenu) {
p->setPen(opt->palette.color(QPalette::Dark));
p->setBrush(Qt::NoBrush);
p->drawRect(opt->rect.adjusted(0, 0, -1, -1));
return;
}
// Kill the status bar item frame and panel border
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
return;
@@ -355,7 +370,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
pal.setColor(QPalette::ToolTipText, theme.text);
pal.setColor(QPalette::Mid, theme.hover);
pal.setColor(QPalette::Dark, theme.background);
pal.setColor(QPalette::Dark, theme.border);
pal.setColor(QPalette::Light, theme.textFaint);
pal.setColor(QPalette::Link, theme.indHoverSpan);
@@ -657,6 +672,15 @@ void MainWindow::createMenus() {
tab.ctrl->setCompactColumns(checked);
});
auto* actTreeLines = view->addAction("&Tree Lines");
actTreeLines->setCheckable(true);
actTreeLines->setChecked(settings.value("treeLines", false).toBool());
connect(actTreeLines, &QAction::triggered, this, [this](bool checked) {
QSettings("Reclass", "Reclass").setValue("treeLines", checked);
for (auto& tab : m_tabs)
tab.ctrl->setTreeLines(checked);
});
auto* actRelOfs = view->addAction("R&elative Offsets");
actRelOfs->setCheckable(true);
actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
@@ -1307,6 +1331,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
// Apply global compact columns setting to new tab
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", false).toBool());
// Give every controller the shared document list for cross-tab type visibility
ctrl->setProjectDocuments(&m_allDocs);
@@ -2977,6 +3002,30 @@ void MainWindow::createScannerDock() {
return ctrl ? ctrl->document()->provider : nullptr;
});
// Wire bounds getter: struct base + size for "Current Struct" filter
m_scannerPanel->setBoundsGetter([this]() -> rcx::ScannerPanel::StructBounds {
auto* ctrl = activeController();
if (!ctrl) return {};
auto& tree = ctrl->document()->tree;
uint64_t base = tree.baseAddress;
uint64_t viewRoot = ctrl->viewRootId();
int span = 0;
if (viewRoot != 0) {
span = tree.structSpan(viewRoot);
} else {
// Compute extent from all top-level nodes
for (int i = 0; i < tree.nodes.size(); i++) {
const auto& n = tree.nodes[i];
int64_t off = tree.computeOffset(i);
int sz = (n.kind == rcx::NodeKind::Struct || n.kind == rcx::NodeKind::Array)
? tree.structSpan(n.id) : n.byteSize();
int64_t end = off + sz;
if (end > span) span = static_cast<int>(end);
}
}
return { base, static_cast<uint64_t>(span) };
});
// Wire "Go to Address" to rebase the active tab
connect(m_scannerPanel, &ScannerPanel::goToAddress, this, [this](uint64_t addr) {
auto* ctrl = activeController();

View File

@@ -1214,9 +1214,13 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
}
if (action == "reset_tracking") {
if (!ctrl) return makeTextResult("No active tab", true);
ctrl->resetChangeTracking();
return makeTextResult("Value tracking reset. All histories cleared.");
int count = m_mainWindow->tabCount();
for (int i = 0; i < count; ++i) {
auto* t = m_mainWindow->tabByIndex(i);
if (t && t->ctrl)
t->ctrl->resetChangeTracking();
}
return makeTextResult(QStringLiteral("Value tracking reset on all %1 tabs.").arg(count));
}
return makeTextResult("Unknown action: " + action, true);

241
src/rcxtooltip.h Normal file
View File

@@ -0,0 +1,241 @@
#pragma once
#include "themes/thememanager.h"
#include <QWidget>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QScreen>
#include <QTimer>
#include <QPropertyAnimation>
#include <QCursor>
#include <cstdio>
#define TIP_LOG(...) do { \
FILE* _f = fopen("E:/game_dev/util/reclass2027-main/build/tip_trace.log", "a"); \
if (_f) { fprintf(_f, __VA_ARGS__); fclose(_f); } \
} while(0)
namespace rcx {
class RcxTooltip : public QWidget {
public:
static RcxTooltip* instance() {
static RcxTooltip* s = nullptr;
if (!s) {
s = new RcxTooltip;
QObject::connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
s, [](const rcx::Theme&) { /* colors read live in paintEvent */ });
}
return s;
}
void showFor(QWidget* trigger, const QString& text) {
if (!trigger || text.isEmpty()) {
TIP_LOG("[TIP] showFor: null trigger or empty text -- dismiss\n");
dismiss(); return;
}
// Same widget+text already showing — do nothing (prevents teleport)
if (m_trigger == trigger && m_text == text && isVisible()) {
TIP_LOG("[TIP] showFor: same widget+text, already visible -- skip\n");
return;
}
TIP_LOG("[TIP] showFor: text='%s' trigger=%p class=%s\n",
qPrintable(text), (void*)trigger, trigger->metaObject()->className());
// Cancel pending dismiss
if (m_dismissTimer) m_dismissTimer->stop();
m_trigger = trigger;
m_text = text;
m_label->setText(text);
m_label->adjustSize();
// ── Size: label + padding + arrow ──
const int pad = 8;
const int vpad = 4;
int bodyW = m_label->sizeHint().width() + pad * 2;
int bodyH = m_label->sizeHint().height() + vpad * 2;
int totalW = bodyW;
int totalH = bodyH + kArrowH;
// ── Position relative to trigger widget ──
QRect trigGlobal = QRect(trigger->mapToGlobal(QPoint(0, 0)), trigger->size());
int trigCenterX = trigGlobal.center().x();
QScreen* screen = QApplication::screenAt(trigGlobal.center());
QRect scr = screen ? screen->availableGeometry() : QRect(0, 0, 1920, 1080);
// Default: above the trigger
m_arrowDown = true;
int x = trigCenterX - totalW / 2;
int y = trigGlobal.top() - totalH - kGap;
// Flip below if not enough room above
if (y < scr.top()) {
m_arrowDown = false;
y = trigGlobal.bottom() + kGap;
}
// Clamp horizontally
if (x < scr.left()) x = scr.left() + 2;
if (x + totalW > scr.right()) x = scr.right() - totalW - 2;
// Arrow X in local coords
m_arrowLocalX = trigCenterX - x;
m_arrowLocalX = qBound(kArrowHalfW + 4, m_arrowLocalX, totalW - kArrowHalfW - 4);
// Position label inside the body
if (m_arrowDown)
m_label->move(pad, vpad);
else
m_label->move(pad, kArrowH + vpad);
m_bodyRect = m_arrowDown
? QRect(0, 0, bodyW, bodyH)
: QRect(0, kArrowH, bodyW, bodyH);
setFixedSize(totalW, totalH);
move(x, y);
if (!isVisible()) {
TIP_LOG("[TIP] showFor: showing at (%d,%d) size=%dx%d arrowDown=%d arrowX=%d\n",
x, y, totalW, totalH, m_arrowDown, m_arrowLocalX);
setWindowOpacity(0.0);
show();
raise();
// Fade in
auto* anim = new QPropertyAnimation(this, "windowOpacity", this);
anim->setDuration(80);
anim->setStartValue(0.0);
anim->setEndValue(1.0);
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->start(QAbstractAnimation::DeleteWhenStopped);
} else {
TIP_LOG("[TIP] showFor: already visible, updating\n");
update();
}
}
void dismiss() {
TIP_LOG("[TIP] dismiss: wasVisible=%d\n", isVisible());
if (m_dismissTimer) m_dismissTimer->stop();
if (isVisible()) hide();
m_trigger = nullptr;
}
// Schedule dismiss with a delay — but only if the cursor has truly
// left the trigger+tooltip zone. Qt fires synthetic Leave events
// when a tooltip window appears above the trigger; we must ignore those.
void scheduleDismiss() {
if (m_trigger) {
QPoint cursor = QCursor::pos();
QRect trigRect(m_trigger->mapToGlobal(QPoint(0, 0)), m_trigger->size());
QRect tipRect(pos(), size());
QRect zone = trigRect.united(tipRect).adjusted(-4, -4, 4, 4);
bool inside = zone.contains(cursor);
TIP_LOG("[TIP] scheduleDismiss: cursor=(%d,%d) zone=(%d,%d %dx%d) inside=%d\n",
cursor.x(), cursor.y(),
zone.x(), zone.y(), zone.width(), zone.height(), inside);
if (inside)
return; // cursor still inside — ignore spurious Leave
}
if (!m_dismissTimer) {
m_dismissTimer = new QTimer(this);
m_dismissTimer->setSingleShot(true);
connect(m_dismissTimer, &QTimer::timeout, this, &RcxTooltip::dismiss);
}
m_dismissTimer->start(100);
}
QWidget* currentTrigger() const { return m_trigger; }
// ── Geometry accessors (for testing) ──
bool arrowPointsDown() const { return m_arrowDown; }
int arrowLocalX() const { return m_arrowLocalX; }
QRect bodyRect() const { return m_bodyRect; }
QString currentText() const { return m_text; }
// Constants exposed for testing
static constexpr int kArrowH = 6;
static constexpr int kArrowHalfW = 6;
static constexpr int kGap = 2;
protected:
void paintEvent(QPaintEvent*) override {
TIP_LOG("[TIP] paintEvent: size=%dx%d bodyRect=(%d,%d %dx%d)\n",
width(), height(),
m_bodyRect.x(), m_bodyRect.y(), m_bodyRect.width(), m_bodyRect.height());
const auto& theme = ThemeManager::instance().current();
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
// Fill entire widget with the tooltip background first
// (no WA_TranslucentBackground, so unpainted areas would be opaque garbage)
p.fillRect(rect(), theme.backgroundAlt);
// Build path: rounded body + triangle arrow
QPainterPath path;
path.addRoundedRect(QRectF(m_bodyRect), 4.0, 4.0);
// Triangle arrow
QPolygonF arrow;
if (m_arrowDown) {
int ay = m_bodyRect.bottom();
arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay)
<< QPointF(m_arrowLocalX, ay + kArrowH)
<< QPointF(m_arrowLocalX + kArrowHalfW, ay);
} else {
int ay = kArrowH;
arrow << QPointF(m_arrowLocalX - kArrowHalfW, ay)
<< QPointF(m_arrowLocalX, 0)
<< QPointF(m_arrowLocalX + kArrowHalfW, ay);
}
QPainterPath arrowPath;
arrowPath.addPolygon(arrow);
arrowPath.closeSubpath();
path = path.united(arrowPath);
// Stroke the shape border
p.setPen(QPen(theme.border, 1.0));
p.setBrush(theme.backgroundAlt);
p.drawPath(path);
}
private:
explicit RcxTooltip()
: QWidget(nullptr, Qt::ToolTip | Qt::FramelessWindowHint)
{
// NOTE: WA_TranslucentBackground removed — it breaks under DWM dark mode
// (DwmSetWindowAttribute DWMWA_USE_IMMERSIVE_DARK_MODE kills layered compositing)
setAttribute(Qt::WA_ShowWithoutActivating);
setAutoFillBackground(false); // we paint everything ourselves in paintEvent
m_label = new QLabel(this);
m_label->setAlignment(Qt::AlignCenter);
updateLabelStyle();
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
this, [this](const rcx::Theme&) { updateLabelStyle(); });
}
void updateLabelStyle() {
const auto& theme = ThemeManager::instance().current();
m_label->setStyleSheet(
QStringLiteral("QLabel { color: %1; background: transparent; padding: 0; }")
.arg(theme.text.name()));
}
QLabel* m_label = nullptr;
QWidget* m_trigger = nullptr;
QString m_text;
QTimer* m_dismissTimer = nullptr;
bool m_arrowDown = true;
int m_arrowLocalX = 0;
QRect m_bodyRect;
};
} // namespace rcx

View File

@@ -489,13 +489,21 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
const char* msk = isUnknown ? nullptr : req.mask.constData();
const int alignment = qMax(1, req.alignment);
const int valSize = isUnknown ? req.valueSize : patternLen;
const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
req.endAddress > req.startAddress;
// Pre-compute total bytes for progress
uint64_t totalBytes = 0;
for (const auto& r : regions) {
if (req.filterExecutable && !r.executable) continue;
if (req.filterWritable && !r.writable) continue;
totalBytes += r.size;
uint64_t rStart = r.base, rEnd = r.base + r.size;
if (hasRange) {
if (rEnd <= req.startAddress || rStart >= req.endAddress) continue;
rStart = qMax(rStart, req.startAddress);
rEnd = qMin(rEnd, req.endAddress);
}
totalBytes += rEnd - rStart;
}
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
@@ -513,21 +521,35 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
if (req.filterExecutable && !region.executable) continue;
if (req.filterWritable && !region.writable) continue;
if ((uint64_t)patternLen > region.size) {
scannedBytes += region.size;
// Clip region to requested address range
uint64_t regStart = region.base;
uint64_t regEnd = region.base + region.size;
if (hasRange) {
if (regEnd <= req.startAddress || regStart >= req.endAddress) {
// Entirely outside range — skip
continue;
}
regStart = qMax(regStart, req.startAddress);
regEnd = qMin(regEnd, req.endAddress);
}
uint64_t regSize = regEnd - regStart;
if ((uint64_t)patternLen > regSize) {
scannedBytes += regSize;
continue;
}
const int overlap = patternLen - 1;
QByteArray chunk(qMin((uint64_t)kChunk, region.size), Qt::Uninitialized);
QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized);
uint64_t regOffset = regStart - region.base; // offset within provider region
for (uint64_t off = 0; off < region.size; ) {
for (uint64_t off = 0; off < regSize; ) {
if (m_abort.load()) break;
uint64_t remaining = region.size - off;
uint64_t remaining = regSize - off;
int readLen = (int)qMin((uint64_t)chunk.size(), remaining);
if (!prov->read(region.base + off, chunk.data(), readLen)) {
if (!prov->read(regStart + off, chunk.data(), readLen)) {
// Skip unreadable chunk
off += readLen;
scannedBytes += readLen;
@@ -541,7 +563,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
// Unknown value: capture every aligned address
for (int i = 0; i <= scanEnd; i += alignment) {
ScanResult r;
r.address = region.base + off + (uint64_t)i;
r.address = regStart + off + (uint64_t)i;
r.scanValue = QByteArray(data + i, valSize);
results.append(r);
@@ -560,7 +582,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
}
if (match) {
ScanResult r;
r.address = region.base + off + (uint64_t)i;
r.address = regStart + off + (uint64_t)i;
r.regionModule = region.moduleName;
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
results.append(r);

View File

@@ -46,6 +46,9 @@ struct ScanRequest {
ScanCondition condition = ScanCondition::ExactValue;
int valueSize = 4; // bytes per value (for unknown scans)
uint64_t startAddress = 0; // 0 = no limit (scan all regions)
uint64_t endAddress = 0; // 0 = no limit (scan all regions)
};
struct ScanResult {

View File

@@ -124,6 +124,9 @@ ScannerPanel::ScannerPanel(QWidget* parent)
m_writeCheck = new QCheckBox(QStringLiteral("Writable"), this);
filterRow->addWidget(m_writeCheck);
m_structOnlyCheck = new QCheckBox(QStringLiteral("Current Struct"), this);
filterRow->addWidget(m_structOnlyCheck);
filterRow->addStretch();
m_scanBtn = new QPushButton(QIcon(QStringLiteral(":/vsicons/search.svg")),
@@ -257,6 +260,10 @@ void ScannerPanel::setProviderGetter(ProviderGetter getter) {
m_providerGetter = std::move(getter);
}
void ScannerPanel::setBoundsGetter(BoundsGetter getter) {
m_boundsGetter = std::move(getter);
}
void ScannerPanel::setEditorFont(const QFont& font) {
m_resultTable->setFont(font);
QFontMetrics fm(font);
@@ -278,6 +285,7 @@ void ScannerPanel::setEditorFont(const QFont& font) {
m_valueLabel->setFont(font);
m_execCheck->setFont(font);
m_writeCheck->setFont(font);
m_structOnlyCheck->setFont(font);
m_updateBtn->setFont(font);
updateComboWidth();
}
@@ -398,6 +406,14 @@ ScanRequest ScannerPanel::buildRequest() {
req.filterExecutable = m_execCheck->isChecked();
req.filterWritable = m_writeCheck->isChecked();
if (m_structOnlyCheck->isChecked() && m_boundsGetter) {
auto bounds = m_boundsGetter();
if (bounds.size > 0) {
req.startAddress = bounds.start;
req.endAddress = bounds.start + bounds.size;
}
}
return req;
}
@@ -750,6 +766,7 @@ void ScannerPanel::applyTheme(const Theme& theme) {
cp.setColor(QPalette::WindowText, theme.textDim);
m_execCheck->setPalette(cp);
m_writeCheck->setPalette(cp);
m_structOnlyCheck->setPalette(cp);
// Buttons
QString btnStyle = QStringLiteral(

View File

@@ -34,6 +34,10 @@ public:
using ProviderGetter = std::function<std::shared_ptr<Provider>()>;
void setProviderGetter(ProviderGetter getter);
struct StructBounds { uint64_t start = 0; uint64_t size = 0; };
using BoundsGetter = std::function<StructBounds()>;
void setBoundsGetter(BoundsGetter getter);
void setEditorFont(const QFont& font);
void applyTheme(const Theme& theme);
@@ -54,6 +58,7 @@ public:
ScanEngine* engine() const { return m_engine; }
QComboBox* condCombo() const { return m_condCombo; }
QLabel* condLabel() const { return m_condLabel; }
QCheckBox* structOnlyCheck() const { return m_structOnlyCheck; }
signals:
void goToAddress(uint64_t address);
@@ -90,6 +95,7 @@ private:
// Filters
QCheckBox* m_execCheck;
QCheckBox* m_writeCheck;
QCheckBox* m_structOnlyCheck;
// Actions
QPushButton* m_scanBtn;
@@ -106,6 +112,7 @@ private:
// Engine
ScanEngine* m_engine;
ProviderGetter m_providerGetter;
BoundsGetter m_boundsGetter;
QVector<ScanResult> m_results;
int m_lastScanMode = 0; // 0=signature, 1=value
ValueType m_lastValueType = ValueType::Int32;

View File

@@ -10,8 +10,8 @@
"textDim": "#505C74",
"textMuted": "#384258",
"textFaint": "#2C3448",
"hover": "#121720",
"selected": "#121720",
"hover": "#181E2A",
"selected": "#1A2D4A",
"selection": "#1A2038",
"syntaxKeyword": "#5688C0",
"syntaxNumber": "#90B480",

View File

@@ -0,0 +1,32 @@
{
"name": "Light",
"background": "#e8e8ec",
"backgroundAlt": "#dcdce0",
"surface": "#d4d4d8",
"border": "#b8b8be",
"borderFocused": "#6870a0",
"button": "#ccccd0",
"text": "#1b1b22",
"textDim": "#5c5c68",
"textMuted": "#84848e",
"textFaint": "#a8a8b0",
"hover": "#d8d8de",
"selected": "#d0d0d8",
"selection": "#b4c8e8",
"syntaxKeyword": "#4455aa",
"syntaxNumber": "#2a7a4c",
"syntaxString": "#9a4040",
"syntaxComment": "#6a7a6a",
"syntaxPreproc": "#787880",
"syntaxType": "#2e7a8a",
"indHoverSpan": "#5a68a0",
"indCmdPill": "#dcdce0",
"indDataChanged": "#2a7a4c",
"indHeatCold": "#6a6a30",
"indHeatWarm": "#a06828",
"indHeatHot": "#b83030",
"indHintGreen": "#387a44",
"markerPtr": "#b83030",
"markerCycle": "#9a7010",
"markerError": "#e8c8c8"
}

View File

@@ -15,8 +15,8 @@
"selection": "#21213A",
"syntaxKeyword": "#AA9565",
"syntaxNumber": "#AAA98C",
"syntaxString": "#6B3B21",
"syntaxComment": "#464646",
"syntaxString": "#C0825A",
"syntaxComment": "#8A8878",
"syntaxPreproc": "#AA9565",
"syntaxType": "#6B959F",
"indHoverSpan": "#AA9565",
@@ -25,8 +25,8 @@
"indHeatCold": "#C4A44A",
"indHeatWarm": "#AA9565",
"indHeatHot": "#A05040",
"indHintGreen": "#464646",
"markerPtr": "#6B3B21",
"indHintGreen": "#688A58",
"markerPtr": "#B85A42",
"markerCycle": "#AA9565",
"markerError": "#3C2121"
}