mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
16
src/core.h
16
src/core.h
@@ -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
|
||||
|
||||
@@ -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
143
src/examples/Demo.rcx
Normal 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
59
src/main.cpp
59
src/main.cpp
@@ -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();
|
||||
|
||||
@@ -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
241
src/rcxtooltip.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#505C74",
|
||||
"textMuted": "#384258",
|
||||
"textFaint": "#2C3448",
|
||||
"hover": "#121720",
|
||||
"selected": "#121720",
|
||||
"hover": "#181E2A",
|
||||
"selected": "#1A2D4A",
|
||||
"selection": "#1A2038",
|
||||
"syntaxKeyword": "#5688C0",
|
||||
"syntaxNumber": "#90B480",
|
||||
|
||||
32
src/themes/defaults/tw.json
Normal file
32
src/themes/defaults/tw.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user