Compare commits

...

22 Commits

Author SHA1 Message Date
IChooseYou
f981fe456d feat: see-through popup dismiss for disasm/value-history/struct-preview
Override mouseMoveEvent in all three popup classes to forward mouse
position back to viewport hover logic. When the row underneath the
popup represents a different node, the popup dismisses automatically,
allowing rapid swiping through FuncPtr rows.
2026-03-05 18:25:40 -07:00
IChooseYou
877ceea4c1 feat: VS-style dock tabs with middle-elision and full context menu
- Remove stylesheet from dock tab bars; handle all painting in
  MenuBarStyle (CE_TabBarTabShape + CE_TabBarTabLabel) so middle-
  elision actually works (QStyleSheetStyle was intercepting labels)
- Accent line on selected tab, dark background, bottom border
- Tab font synced with editor font for correct sizing
- Full right-click context menu: Close, Close All Tabs, Close All
  But This, Close All But Pinned, Copy Full Path, Open Containing
  Folder, Float/Dock, Pin/Unpin Tab, New Horizontal/Vertical
  Document Group
- Add View → Reset Windows to re-tabify all docks
- Remove old View → Split/Remove Split
- Guard deferred timer lambdas with QPointer<QDockWidget>
- Extract setupDockTabBars() for idempotent tab bar configuration
- Register close-all.svg and split-vertical.svg icons
2026-03-05 15:16:01 -07:00
IChooseYou
4160a229c6 feat: workspace double-click opens struct in new tab + flat tab corners
- Double-clicking a root struct in the workspace tree opens it in a new
  tab (dock) sharing the same document, focused on that struct
- If a tab already views that struct, raises it instead of duplicating
- Child member double-click still navigates within the existing tab
- Doc lifecycle ref-counted: only deleted when last tab referencing it closes
- rebuildAllDocs/rebuildWorkspaceModel deduplicate shared docs
- Removed border-radius from all tab bar stylesheets (flat corners)
2026-03-05 13:49:42 -07:00
Sen66
1e1afc1640 fix: docking of 'project' window 2026-03-05 19:47:18 +01:00
IChooseYou
f0cf6c549a revert: restore .NET CLR hosting description for ReClass.NET plugin 2026-03-05 06:37:56 -07:00
Sen66
683eab16ee fix: better fix to switch to newly created class 2026-03-05 14:25:49 +01:00
Sen66
b53dea8f9f fix crash on application close 2026-03-05 14:25:06 +01:00
Sen66
f06abbab79 fix: on new class, switch to it 2026-03-05 14:23:07 +01:00
Sen66
2477591ed2 fix: assertion due to undo history disabled nullptr 2026-03-05 14:21:07 +01:00
IChooseYou
6c13356d6d docs: trim README plugin descriptions 2026-03-05 06:07:37 -07:00
IChooseYou
3b273a7ab2 fix: don't skip Array in scope width calc — only skip Struct
Array headers like int32_t[10] render in the type column and need
their width accounted for. Only Struct (pointer headers) should be
excluded from inflating sibling column widths.
2026-03-05 06:02:43 -07:00
IChooseYou
3509a0d9dd Merge remote-tracking branch 'origin/floating' 2026-03-05 05:58:18 -07:00
Sen66
43c3f5a842 fix: highlight issue between command row & opening brace 2026-03-05 13:52:40 +01:00
Sen66
0697ce4853 feat: option to have class opening brace on new line 2026-03-05 13:48:26 +01:00
IChooseYou
ed1bfd04cd fix: tighten editor column spacing — skip struct/array in scope width calc
Reduce kMinTypeW from 8 to 7, and exclude Struct/Array children from
per-scope column width measurement so pointer headers don't inflate
sibling hex row padding.
2026-03-05 13:48:26 +01:00
IChooseYou
c275eb33c9 fix: tighten editor column spacing — skip struct/array in scope width calc
Reduce kMinTypeW from 8 to 7, and exclude Struct/Array children from
per-scope column width measurement so pointer headers don't inflate
sibling hex row padding.
2026-03-05 05:46:14 -07:00
Sen66
636176ee8c feat: floating windows like old windbg 2026-03-05 13:23:00 +01:00
IChooseYou
9a716444f4 fix: menu border clipping, context menu cleanup, workspace sort
- Use WA_TranslucentBackground on QMenu popups so DWM doesn't clip
  border edges; draw 1px border at true widget edge via drawLine
- Move Insert 4/8 into Insert submenu, reorder context menu sections
- Sort workspace tree by visible (non-hex-pad) children count
2026-03-05 04:59:25 -07:00
Sen66
a46da4ee16 fix: horizontal scrollbar calculations for C/C++ view
- added msvc define NOMINMAX so we can use std::max
2026-03-05 12:46:55 +01:00
Sen66
cd52451210 fix: Release build configuration on MSVC & add windeployqt post-build 2026-03-05 12:16:11 +01:00
IChooseYou
82bf9118c9 feat: options dialog cleanup, menu/tree styling, light theme contrast
- Remove dead "Safe Mode" option, rename title case to "Uppercase menu items"
- Options tree: icons, themed hover/selection, mouse tracking (matches workspace tree)
- Tree item row padding (+4px) via MenuBarStyle CT_ItemViewItem for all trees
- Titlebar grows 2px when icon shown
- Menu popups: custom separator drawing, opaque background fill, flat hover highlight
- Menu bar/popup hover uses accent color (QPalette::Highlight) instead of grey
- Light theme: bump textMuted/textFaint contrast
- Dock grip widget for workspace and scanner docks
2026-03-04 13:44:42 -07:00
IChooseYou
f4c7e9327d fix: audit cleanup — themed close button, stale popup dismiss, bitfield clamp, scanner guard, process sort 2026-03-04 11:15:04 -07:00
22 changed files with 1772 additions and 662 deletions

View File

@@ -83,7 +83,7 @@ Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, In
## Plugin System
Extensible provider architecture via DLL plugins with `IPlugin` interface, factory function discovery, and auto/manual loading from a Plugins folder.
DLL plugins loaded from a `Plugins` folder, auto or manual.
**Bundled plugins:**

View File

@@ -66,15 +66,26 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Link>
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<ClCompile>
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<PostBuildEvent>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ClCompile>

View File

@@ -25,6 +25,7 @@ struct ComposeState {
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
bool braceWrap = false; // opening brace on its own line
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
@@ -319,7 +320,24 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.effectiveNameW = nameW;
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
}
state.emitLine(headerText, lm);
// Brace wrapping: move trailing '{' to its own line
if (state.braceWrap && !node.collapsed && headerText.endsWith(QChar('{'))) {
headerText.chop(1);
// Remove trailing separator spaces
while (headerText.endsWith(' ')) headerText.chop(1);
state.emitLine(headerText, lm);
// Emit standalone brace line
LineMeta braceLm;
braceLm.nodeIdx = nodeIdx;
braceLm.nodeId = node.id;
braceLm.depth = depth;
braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = computeFoldLevel(depth, true);
braceLm.markerMask = (1u << M_STRUCT_BG);
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
} else {
state.emitLine(headerText, lm);
}
}
if (!node.collapsed || isArrayChild || isRootHeader) {
@@ -840,9 +858,26 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName;
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
prov, absAddr, ptrTypeOverride,
typeW, nameW, state.compactColumns), lm);
{
QString ptrText = fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
prov, absAddr, ptrTypeOverride,
typeW, nameW, state.compactColumns);
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
ptrText.chop(1);
while (ptrText.endsWith(' ')) ptrText.chop(1);
state.emitLine(ptrText, lm);
LineMeta braceLm;
braceLm.nodeIdx = nodeIdx;
braceLm.nodeId = node.id;
braceLm.depth = depth;
braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = computeFoldLevel(depth, true);
braceLm.markerMask = lm.markerMask;
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
} else {
state.emitLine(ptrText, lm);
}
}
}
if (!effectiveCollapsed) {
@@ -936,10 +971,11 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
bool compactColumns, bool treeLines) {
bool compactColumns, bool treeLines, bool braceWrap) {
ComposeState state;
state.compactColumns = compactColumns;
state.treeLines = treeLines;
state.braceWrap = braceWrap;
// Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++)
@@ -1014,6 +1050,9 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
for (int childIdx : state.childMap.value(container.id)) {
const Node& child = tree.nodes[childIdx];
// Skip struct children — pointer headers shouldn't inflate sibling widths
if (child.kind == NodeKind::Struct)
continue;
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex, but include containers)
@@ -1046,6 +1085,9 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
int rootMaxName = kMinNameW;
for (int childIdx : state.childMap.value(0)) {
const Node& child = tree.nodes[childIdx];
// Skip struct children — pointer headers shouldn't inflate sibling widths
if (child.kind == NodeKind::Struct)
continue;
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex, include containers)
@@ -1076,6 +1118,18 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
state.emitLine(cmdRowText, lm);
}
// Brace wrapping: emit standalone "{" after CommandRow
if (state.braceWrap) {
LineMeta braceLm;
braceLm.nodeIdx = -1;
braceLm.nodeId = 0; // not associated with any node (no hover)
braceLm.depth = 0;
braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = SC_FOLDLEVELBASE;
braceLm.markerMask = 0;
state.emitLine(QStringLiteral("{"), braceLm);
}
const QVector<int>& roots = childIndices(state, 0);
for (int idx : roots) {

View File

@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
}
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
bool treeLines) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines);
bool treeLines, bool braceWrap) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap);
}
bool RcxDocument::save(const QString& path) {
@@ -558,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_treeLines);
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
else
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines);
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
s_composeDoc = nullptr;
@@ -1617,17 +1617,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
return indices;
};
// ── Insert shortcuts (always available) ──
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
});
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
});
menu.addSeparator();
// Quick-convert shortcuts when all selected nodes share the same kind
NodeKind commonKind = NodeKind::Hex64;
bool allSame = true;
@@ -1695,31 +1684,19 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
menu.addSeparator();
// ── Insert ► submenu ──
{
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
}
{
auto* act = menu.addAction("Clear Value History");
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
connect(act, &QAction::triggered, this, [this, ids]() {
for (uint64_t id : ids) {
m_valueHistory.remove(id);
for (int ci : m_doc->tree.subtreeIndices(id))
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
}
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();
auto* insertMenu = menu.addMenu(icon("diff-added.svg"), "Insert");
insertMenu->addAction("Insert 4", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
});
insertMenu->addAction("Insert 8", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
});
}
menu.addSeparator();
// Check if all selected nodes share the same parent (required for grouping)
{
@@ -1736,6 +1713,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addAction("Group into Union", [this, ids]() { groupIntoUnion(ids); });
}
menu.addSeparator();
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
@@ -1748,6 +1727,33 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addSeparator();
{
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
}
{
auto* act = menu.addAction("Clear Value History");
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
connect(act, &QAction::triggered, this, [this, ids]() {
for (uint64_t id : ids) {
m_valueHistory.remove(id);
for (int ci : m_doc->tree.subtreeIndices(id))
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
}
m_refreshGen++;
m_prevPages.clear();
m_changedOffsets.clear();
m_valueTrackCooldown = 5;
refresh();
for (auto* editor : m_editors)
editor->dismissHistoryPopup();
});
}
menu.addSeparator();
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
QStringList addrs;
@@ -1760,34 +1766,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QApplication::clipboard()->setText(addrs.join('\n'));
});
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
return;
}
QMenu menu;
// ── Insert shortcuts (at very top) ──
if (hasNode) {
menu.addAction(icon("diff-added.svg"), "Insert 4 Above\tShift+Ins",
[this, nodeIdx]() {
insertNodeAbove(nodeIdx, NodeKind::Hex32, QStringLiteral("field"));
});
menu.addAction(icon("diff-added.svg"), "Insert 8 Above\tIns",
[this, nodeIdx]() {
insertNodeAbove(nodeIdx, NodeKind::Hex64, QStringLiteral("field"));
});
} else {
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
});
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
});
}
menu.addSeparator();
// ── Node-specific actions (only when clicking on a node) ──
if (hasNode) {
const Node& node = m_doc->tree.nodes[nodeIdx];
@@ -1819,7 +1804,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// Fall through to always-available actions
} else {
// Quick-convert suggestions for Hex nodes
// ── Quick-convert suggestions (top-level for fast access) ──
bool addedQuickConvert = false;
if (node.kind == NodeKind::Hex64) {
menu.addAction("Change to uint64_t", [this, nodeId]() {
@@ -1876,35 +1861,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
addedQuickConvert = true;
}
// "Change to ptr*" — convert hex/void-ptr to typed pointer with auto-created class
if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Hex32
|| ((node.kind == NodeKind::Pointer64 || node.kind == NodeKind::Pointer32)
&& node.refId == 0)) {
menu.addAction("Change to ptr*", [this, nodeId]() {
convertToTypedPointer(nodeId);
});
addedQuickConvert = true;
}
// Split hex node into two half-sized hex nodes
if (node.kind == NodeKind::Hex64) {
menu.addAction("Change to hex32+hex32", [this, nodeId]() {
splitHexNode(nodeId);
});
addedQuickConvert = true;
} else if (node.kind == NodeKind::Hex32) {
menu.addAction("Change to hex16+hex16", [this, nodeId]() {
splitHexNode(nodeId);
});
addedQuickConvert = true;
} else if (node.kind == NodeKind::Hex16) {
menu.addAction("Change to hex8+hex8", [this, nodeId]() {
splitHexNode(nodeId);
});
addedQuickConvert = true;
}
if (addedQuickConvert)
menu.addSeparator();
// ── Edit Value / Rename / Change Type ──
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& !isHexNode(node.kind)
&& m_doc->provider->isWritable();
@@ -1923,6 +1883,251 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
menu.addSeparator();
// ── Insert ► submenu ──
{
auto* insertMenu = menu.addMenu(icon("diff-added.svg"), "Insert");
insertMenu->addAction("Insert 4 Above\tShift+Ins",
[this, nodeIdx]() {
insertNodeAbove(nodeIdx, NodeKind::Hex32, QStringLiteral("field"));
});
insertMenu->addAction("Insert 8 Above\tIns",
[this, nodeIdx]() {
insertNodeAbove(nodeIdx, NodeKind::Hex64, QStringLiteral("field"));
});
insertMenu->addSeparator();
insertMenu->addAction("Append bytes...", [this, &menu]() {
bool ok;
QString input = QInputDialog::getText(menu.parentWidget(),
QStringLiteral("Append bytes"),
QStringLiteral("Byte count (decimal or 0x hex):"),
QLineEdit::Normal, QStringLiteral("128"), &ok);
if (!ok || input.trimmed().isEmpty()) return;
QString trimmed = input.trimmed();
int byteCount = 0;
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
byteCount = trimmed.mid(2).toInt(&ok, 16);
else
byteCount = trimmed.toInt(&ok, 10);
if (!ok || byteCount <= 0) return;
uint64_t target = m_viewRootId ? m_viewRootId : 0;
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
int idx = 0;
for (int i = 0; i < hex64Count; i++, idx++)
insertNode(target, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(idx));
for (int i = 0; i < remainBytes; i++, idx++)
insertNode(target, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(idx));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
}
// ── Convert ► submenu ──
{
auto* convertMenu = menu.addMenu(icon("symbol-structure.svg"), "Convert");
bool hasConvert = false;
// "Change to ptr*" — convert hex/void-ptr to typed pointer
if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Hex32
|| ((node.kind == NodeKind::Pointer64 || node.kind == NodeKind::Pointer32)
&& node.refId == 0)) {
convertMenu->addAction("Change to ptr*", [this, nodeId]() {
convertToTypedPointer(nodeId);
});
hasConvert = true;
}
// Split hex node into two half-sized hex nodes
if (node.kind == NodeKind::Hex64) {
convertMenu->addAction("Split to hex32+hex32", [this, nodeId]() {
splitHexNode(nodeId);
});
hasConvert = true;
} else if (node.kind == NodeKind::Hex32) {
convertMenu->addAction("Split to hex16+hex16", [this, nodeId]() {
splitHexNode(nodeId);
});
hasConvert = true;
} else if (node.kind == NodeKind::Hex16) {
convertMenu->addAction("Split to hex8+hex8", [this, nodeId]() {
splitHexNode(nodeId);
});
hasConvert = true;
}
// Convert to Hex nodes (decompose non-hex types)
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
convertMenu->addAction("Convert to &Hex", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
const Node& n = m_doc->tree.nodes[ni];
int totalSize = n.byteSize();
if (totalSize <= 0) return;
uint64_t parentId = n.parentId;
int baseOffset = n.offset;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Convert to Hex"));
QVector<Node> subtree;
subtree.append(n);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{nodeId, subtree, {}}));
int padOffset = baseOffset;
int gap = totalSize;
while (gap > 0) {
NodeKind padKind;
int padSize;
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
else { padKind = NodeKind::Hex8; padSize = 1; }
insertNode(parentId, padOffset, padKind,
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
padOffset += padSize;
gap -= padSize;
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
});
hasConvert = true;
}
if (!hasConvert)
convertMenu->setEnabled(false);
}
// ── Structure ► submenu (only when relevant) ──
{
auto* structMenu = menu.addMenu("Static");
bool hasStructAction = false;
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
structMenu->addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
insertNode(nodeId, 0, NodeKind::Hex64, "newField");
});
structMenu->addAction("Add Static Method (WIP)", [this, nodeId]() {
Node sf;
sf.id = m_doc->tree.m_nextId++;
sf.kind = NodeKind::Hex64;
sf.name = QStringLiteral("static_field");
sf.parentId = nodeId;
sf.offset = 0;
sf.isStatic = true;
sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(this,
cmd::Insert{sf, {}}));
});
if (node.collapsed) {
structMenu->addAction(icon("expand-all.svg"), "&Expand", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) toggleCollapse(ni);
});
} else {
structMenu->addAction(icon("collapse-all.svg"), "&Collapse", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) toggleCollapse(ni);
});
}
hasStructAction = true;
}
// Add Static Field as sibling (for child nodes of a struct)
if (node.parentId != 0 && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
uint64_t pid = node.parentId;
int pi = m_doc->tree.indexOfId(pid);
if (pi >= 0 && (m_doc->tree.nodes[pi].kind == NodeKind::Struct
|| m_doc->tree.nodes[pi].kind == NodeKind::Array)) {
structMenu->addAction("Add Static Method (WIP)", [this, pid]() {
Node sf;
sf.id = m_doc->tree.m_nextId++;
sf.kind = NodeKind::Hex64;
sf.name = QStringLiteral("static_field");
sf.parentId = pid;
sf.offset = 0;
sf.isStatic = true;
sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(this,
cmd::Insert{sf, {}}));
});
hasStructAction = true;
}
}
// Static field: Edit Expression
if (node.isStatic) {
structMenu->addAction("Edit E&xpression", [this, editor, line, nodeId]() {
QStringList completions;
completions << QStringLiteral("base");
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) {
uint64_t pid = m_doc->tree.nodes[ni].parentId;
for (const Node& sib : m_doc->tree.nodes) {
if (sib.parentId == pid && !sib.isStatic && !sib.name.isEmpty())
completions << sib.name;
}
}
editor->setStaticCompletions(completions);
editor->beginInlineEdit(EditTarget::StaticExpr, line);
});
hasStructAction = true;
}
// Dissolve Union
{
uint64_t targetUnionId = 0;
if (node.kind == NodeKind::Struct
&& node.resolvedClassKeyword() == QStringLiteral("union")) {
targetUnionId = nodeId;
} else if (node.parentId != 0) {
int pi = m_doc->tree.indexOfId(node.parentId);
if (pi >= 0 && m_doc->tree.nodes[pi].kind == NodeKind::Struct
&& m_doc->tree.nodes[pi].resolvedClassKeyword() == QStringLiteral("union")) {
targetUnionId = node.parentId;
}
}
if (targetUnionId != 0) {
structMenu->addAction("Dissolve Union", [this, targetUnionId]() {
dissolveUnion(targetUnionId);
});
hasStructAction = true;
}
}
if (!hasStructAction)
structMenu->setEnabled(false);
}
menu.addSeparator();
// ── Duplicate / Delete ──
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) duplicateNode(ni);
});
menu.addAction(icon("trash.svg"), "&Delete\tDelete", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) removeNode(ni);
});
menu.addSeparator();
// ── Tracking ──
{
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
@@ -1936,107 +2141,80 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
m_valueHistory.remove(nodeId);
for (int ci : m_doc->tree.subtreeIndices(nodeId))
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
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
m_refreshGen++;
m_prevPages.clear();
m_changedOffsets.clear();
m_valueTrackCooldown = 5;
refresh();
for (auto* editor : m_editors)
editor->dismissHistoryPopup();
});
}
menu.addSeparator();
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
menu.addAction("Convert to &Hex", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
const Node& n = m_doc->tree.nodes[ni];
int totalSize = n.byteSize();
if (totalSize <= 0) return;
uint64_t parentId = n.parentId;
int baseOffset = n.offset;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Convert to Hex"));
// Remove the original node
QVector<Node> subtree;
subtree.append(n);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{nodeId, subtree, {}}));
// Insert hex nodes to fill the space (largest first)
int padOffset = baseOffset;
int gap = totalSize;
while (gap > 0) {
NodeKind padKind;
int padSize;
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
else { padKind = NodeKind::Hex8; padSize = 1; }
insertNode(parentId, padOffset, padKind,
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
padOffset += padSize;
gap -= padSize;
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
});
}
menu.addSeparator();
} // else (non-member node actions)
}
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
insertNode(nodeId, 0, NodeKind::Hex64, "newField");
});
// Add Static Field — inserts a static field child
menu.addAction("Add Static Field", [this, nodeId]() {
Node sf;
sf.id = m_doc->tree.m_nextId++;
sf.kind = NodeKind::Hex64;
sf.name = QStringLiteral("static_field");
sf.parentId = nodeId;
sf.offset = 0;
sf.isStatic = true;
sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(this,
cmd::Insert{sf, {}}));
});
if (node.collapsed) {
menu.addAction(icon("expand-all.svg"), "&Expand", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) toggleCollapse(ni);
});
} else {
menu.addAction(icon("collapse-all.svg"), "&Collapse", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) toggleCollapse(ni);
});
}
// ── Always-available actions ──
}
if (!hasNode) {
// Insert submenu for empty area
auto* insertMenu = menu.addMenu(icon("diff-added.svg"), "Insert");
insertMenu->addAction("Insert 4", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
});
insertMenu->addAction("Insert 8", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
});
insertMenu->addSeparator();
insertMenu->addAction("Append bytes...", [this, &menu]() {
bool ok;
QString input = QInputDialog::getText(menu.parentWidget(),
QStringLiteral("Append bytes"),
QStringLiteral("Byte count (decimal or 0x hex):"),
QLineEdit::Normal, QStringLiteral("128"), &ok);
if (!ok || input.trimmed().isEmpty()) return;
// Add Static Field as sibling (for child nodes of a struct)
if (node.parentId != 0 && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
uint64_t parentId = node.parentId;
int pi = m_doc->tree.indexOfId(parentId);
if (pi >= 0 && (m_doc->tree.nodes[pi].kind == NodeKind::Struct
|| m_doc->tree.nodes[pi].kind == NodeKind::Array)) {
menu.addAction("Add Static Field", [this, parentId]() {
QString trimmed = input.trimmed();
int byteCount = 0;
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
byteCount = trimmed.mid(2).toInt(&ok, 16);
else
byteCount = trimmed.toInt(&ok, 10);
if (!ok || byteCount <= 0) return;
uint64_t target = m_viewRootId ? m_viewRootId : 0;
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
int idx = 0;
for (int i = 0; i < hex64Count; i++, idx++)
insertNode(target, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(idx));
for (int i = 0; i < remainBytes; i++, idx++)
insertNode(target, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(idx));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
// Add Static Field to current view root
if (m_viewRootId != 0) {
int ri = m_doc->tree.indexOfId(m_viewRootId);
if (ri >= 0 && (m_doc->tree.nodes[ri].kind == NodeKind::Struct
|| m_doc->tree.nodes[ri].kind == NodeKind::Array)) {
uint64_t rootId = m_viewRootId;
menu.addAction("Add Static Method (WIP)", [this, rootId]() {
Node sf;
sf.id = m_doc->tree.m_nextId++;
sf.kind = NodeKind::Hex64;
sf.name = QStringLiteral("static_field");
sf.parentId = parentId;
sf.parentId = rootId;
sf.offset = 0;
sf.isStatic = true;
sf.offsetExpr = QStringLiteral("base");
@@ -2046,122 +2224,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
}
}
// Static field: Edit Expression inline
if (node.isStatic) {
menu.addAction("Edit E&xpression", [this, editor, line, nodeId]() {
// Build completions list: "base" + sibling field names
QStringList completions;
completions << QStringLiteral("base");
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) {
uint64_t parentId = m_doc->tree.nodes[ni].parentId;
for (const Node& sib : m_doc->tree.nodes) {
if (sib.parentId == parentId && !sib.isStatic && !sib.name.isEmpty())
completions << sib.name;
}
}
editor->setStaticCompletions(completions);
editor->beginInlineEdit(EditTarget::StaticExpr, line);
});
}
// Dissolve Union: available on union itself or any of its children
{
uint64_t targetUnionId = 0;
if (node.kind == NodeKind::Struct
&& node.resolvedClassKeyword() == QStringLiteral("union")) {
targetUnionId = nodeId;
} else if (node.parentId != 0) {
int pi = m_doc->tree.indexOfId(node.parentId);
if (pi >= 0 && m_doc->tree.nodes[pi].kind == NodeKind::Struct
&& m_doc->tree.nodes[pi].resolvedClassKeyword() == QStringLiteral("union")) {
targetUnionId = node.parentId;
}
}
if (targetUnionId != 0) {
menu.addAction("Dissolve Union", [this, targetUnionId]() {
dissolveUnion(targetUnionId);
});
}
}
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) duplicateNode(ni);
});
menu.addAction(icon("trash.svg"), "&Delete\tDelete", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) removeNode(ni);
});
menu.addSeparator();
} // else (non-member node actions)
}
// ── Always-available actions ──
// Add Static Field to current view root (struct)
if (m_viewRootId != 0) {
int ri = m_doc->tree.indexOfId(m_viewRootId);
if (ri >= 0 && (m_doc->tree.nodes[ri].kind == NodeKind::Struct
|| m_doc->tree.nodes[ri].kind == NodeKind::Array)) {
uint64_t rootId = m_viewRootId;
menu.addAction("Add Static Field", [this, rootId]() {
Node sf;
sf.id = m_doc->tree.m_nextId++;
sf.kind = NodeKind::Hex64;
sf.name = QStringLiteral("static_field");
sf.parentId = rootId;
sf.offset = 0;
sf.isStatic = true;
sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(this,
cmd::Insert{sf, {}}));
});
}
}
menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
bool ok;
QString input = QInputDialog::getText(menu.parentWidget(),
QStringLiteral("Append bytes"),
QStringLiteral("Byte count (decimal or 0x hex):"),
QLineEdit::Normal, QStringLiteral("128"), &ok);
if (!ok || input.trimmed().isEmpty()) return;
QString trimmed = input.trimmed();
int byteCount = 0;
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
byteCount = trimmed.mid(2).toInt(&ok, 16);
else
byteCount = trimmed.toInt(&ok, 10);
if (!ok || byteCount <= 0) return;
uint64_t target = m_viewRootId ? m_viewRootId : 0;
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
int idx = 0;
for (int i = 0; i < hex64Count; i++, idx++)
insertNode(target, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(idx));
for (int i = 0; i < remainBytes; i++, idx++)
insertNode(target, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(idx));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
menu.addSeparator();
// Only add Track Value Changes here if not already added in node-specific section
if (!hasNode) {
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
menu.addSeparator();
}
@@ -2214,6 +2283,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
});
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
}
@@ -2374,6 +2444,7 @@ void RcxController::updateCommandRow() {
.arg(elide(src, 40), elide(addr, 24));
// Build row 2: root class type + name (uses current view root)
QString brace = m_braceWrap ? QString() : QStringLiteral(" {");
QString row2;
if (m_viewRootId != 0) {
int vi = m_doc->tree.indexOfId(m_viewRootId);
@@ -2381,8 +2452,8 @@ void RcxController::updateCommandRow() {
const auto& n = m_doc->tree.nodes[vi];
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1 %2 {")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
row2 = QStringLiteral("%1 %2%3")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
}
}
if (row2.isEmpty()) {
@@ -2392,14 +2463,14 @@ void RcxController::updateCommandRow() {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1 %2 {")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
row2 = QStringLiteral("%1 %2%3")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
break;
}
}
}
if (row2.isEmpty())
row2 = QStringLiteral("struct NoName {");
row2 = QStringLiteral("struct NoName") + brace;
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
@@ -3191,6 +3262,11 @@ void RcxController::setTreeLines(bool v) {
refresh();
}
void RcxController::setBraceWrap(bool v) {
m_braceWrap = v;
refresh();
}
void RcxController::setupAutoRefresh() {
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
m_refreshTimer = new QTimer(this);

View File

@@ -41,7 +41,7 @@ public:
}
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
bool treeLines = false) const;
bool treeLines = false, bool braceWrap = false) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -130,6 +130,7 @@ public:
void setRefreshInterval(int ms);
void setCompactColumns(bool v);
void setTreeLines(bool v);
void setBraceWrap(bool v);
void resetProvider();
// MCP bridge accessors
@@ -158,6 +159,7 @@ public:
signals:
void nodeSelected(int nodeIdx);
void selectionChanged(int count);
void contextMenuAboutToShow(QMenu* menu, int line);
private:
RcxDocument* m_doc;
@@ -168,6 +170,7 @@ private:
bool m_suppressRefresh = false;
bool m_compactColumns = false;
bool m_treeLines = false;
bool m_braceWrap = false;
uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ──

View File

@@ -303,7 +303,7 @@ struct Node {
QJsonObject bm = v.toObject();
BitfieldMember m;
m.name = bm["name"].toString();
m.bitOffset = (uint8_t)bm["bitOffset"].toInt(0);
m.bitOffset = (uint8_t)qBound(0, bm["bitOffset"].toInt(0), 255);
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
n.bitfieldMembers.append(m);
}
@@ -699,7 +699,7 @@ inline constexpr int kColValue = 96;
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
inline constexpr int kSepWidth = 1;
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
inline constexpr int kMinTypeW = 7; // Minimum type column width (fits "uint8_t")
inline constexpr int kMaxTypeW = 128; // Maximum type column width
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 128; // Maximum name column width
@@ -1031,6 +1031,7 @@ namespace fmt {
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false, bool treeLines = false);
bool compactColumns = false, bool treeLines = false,
bool braceWrap = false);
} // namespace rcx

View File

@@ -23,6 +23,7 @@
#include <QScreen>
#include <QScrollBar>
#include <QDateTime>
#include <algorithm>
#include <functional>
#include "themes/thememanager.h"
@@ -39,18 +40,30 @@ class ValueHistoryPopup : public QFrame {
QStringList m_values;
QVector<QLabel*> m_labels;
std::function<void(const QString&)> m_onSet;
std::function<void(QMouseEvent*)> m_onMouseMove;
public:
explicit ValueHistoryPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setMouseTracking(true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
}
uint64_t nodeId() const { return m_nodeId; }
bool hasButtons() const { return m_hasButtons; }
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
protected:
void mouseMoveEvent(QMouseEvent* e) override {
if (!m_hasButtons && m_onMouseMove)
m_onMouseMove(e);
else
QFrame::mouseMoveEvent(e);
}
public:
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
bool showButtons = false) {
@@ -184,12 +197,14 @@ class DisasmPopup : public QFrame {
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
std::function<void(QMouseEvent*)> m_onMouseMove;
public:
explicit DisasmPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setMouseTracking(true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
@@ -215,8 +230,14 @@ public:
vbox->addWidget(m_bodyLabel);
}
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
uint64_t nodeId() const { return m_nodeId; }
protected:
void mouseMoveEvent(QMouseEvent* e) override {
if (m_onMouseMove) m_onMouseMove(e);
else QFrame::mouseMoveEvent(e);
}
public:
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font) {
if (nodeId == m_nodeId && body == m_body && isVisible())
@@ -282,12 +303,14 @@ class StructPreviewPopup : public QFrame {
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
std::function<void(QMouseEvent*)> m_onMouseMove;
public:
explicit StructPreviewPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setMouseTracking(true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
@@ -314,7 +337,13 @@ public:
}
uint64_t nodeId() const { return m_nodeId; }
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
protected:
void mouseMoveEvent(QMouseEvent* e) override {
if (m_onMouseMove) m_onMouseMove(e);
else QFrame::mouseMoveEvent(e);
}
public:
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font) {
if (nodeId == m_nodeId && body == m_body && isVisible())
@@ -938,9 +967,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
int maxLen = 0;
const QStringList lines = result.text.split(QChar('\n'));
for (const auto& line : lines) {
int len = line.size();
int len = (int)line.size();
while (len > 0 && line[len - 1] == QChar(' ')) --len;
if (len > maxLen) maxLen = len;
maxLen = std::max(len, maxLen);
}
QFontMetrics fm(editorFont());
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
@@ -966,12 +995,20 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
// Reset hint line - applySelectionOverlay will repaint indicators
m_hintLine = -1;
// Restore hover state
// Restore hover state — but clear if the node was deleted
m_hoveredNodeId = savedHoverId;
m_hoveredLine = savedHoverLine;
m_hoverInside = savedHoverInside;
m_applyingDocument = false;
if (m_hoveredNodeId != 0 && !m_nodeLineIndex.contains(m_hoveredNodeId)) {
m_hoveredNodeId = 0;
m_hoveredLine = -1;
dismissHistoryPopup();
if (m_disasmPopup) m_disasmPopup->hide();
if (m_structPreviewPopup) m_structPreviewPopup->hide();
}
// Re-apply hover markers (setText() clears all Scintilla markers).
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
@@ -2539,8 +2576,10 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_editState.commentCol = -1;
}
// Disable Scintilla undo during inline edit
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
// Keep undo collection enabled during inline edit so CellBuffer::DeleteChars
// returns valid text pointers (collectingUndo=false returns nullptr, which
// crashes QsciAccessibleBase::textDeleted). We clear the buffer on edit end.
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)1);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
m_sci->setReadOnly(false);
@@ -2991,8 +3030,26 @@ void RcxEditor::applyHoverCursor() {
if (lm.heatLevel > 0 && lm.nodeId != 0) {
auto it = m_valueHistory->find(lm.nodeId);
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
if (!m_historyPopup)
if (!m_historyPopup) {
m_historyPopup = new ValueHistoryPopup(this);
static_cast<ValueHistoryPopup*>(m_historyPopup)->setOnMouseMove([this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp;
m_hoverInside = m_sci->viewport()->rect().contains(vp);
if (!m_editState.active) {
auto h2 = hitTest(m_lastHoverPos);
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
m_hoveredNodeId = nid;
m_hoveredLine = nln;
applyHoverHighlight();
}
}
applyHoverCursor();
});
}
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
popup->setOnSet([this](const QString& val) {
if (!m_editState.active) return;
@@ -3152,8 +3209,26 @@ void RcxEditor::applyHoverCursor() {
QString lineText = getLineText(m_sci, h.line);
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
if (!m_historyPopup)
if (!m_historyPopup) {
m_historyPopup = new ValueHistoryPopup(this);
static_cast<ValueHistoryPopup*>(m_historyPopup)->setOnMouseMove([this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp;
m_hoverInside = m_sci->viewport()->rect().contains(vp);
if (!m_editState.active) {
auto h2 = hitTest(m_lastHoverPos);
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
m_hoveredNodeId = nid;
m_hoveredLine = nln;
applyHoverHighlight();
}
}
applyHoverCursor();
});
}
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
popup->populate(lm.nodeId, *it, editorFont(), false);
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
@@ -3237,8 +3312,26 @@ void RcxEditor::applyHoverCursor() {
}
}
if (!body.isEmpty()) {
if (!m_disasmPopup)
if (!m_disasmPopup) {
m_disasmPopup = new DisasmPopup(this);
static_cast<DisasmPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp;
m_hoverInside = m_sci->viewport()->rect().contains(vp);
if (!m_editState.active) {
auto h2 = hitTest(m_lastHoverPos);
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
m_hoveredNodeId = nid;
m_hoveredLine = nln;
applyHoverHighlight();
}
}
applyHoverCursor();
});
}
auto* popup = static_cast<DisasmPopup*>(
m_disasmPopup);
popup->populate(lm.nodeId, title, body,
@@ -3306,8 +3399,26 @@ void RcxEditor::applyHoverCursor() {
}
}
if (!body.isEmpty()) {
if (!m_structPreviewPopup)
if (!m_structPreviewPopup) {
m_structPreviewPopup = new StructPreviewPopup(this);
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp;
m_hoverInside = m_sci->viewport()->rect().contains(vp);
if (!m_editState.active) {
auto h2 = hitTest(m_lastHoverPos);
uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0;
int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1;
if (nid != m_hoveredNodeId || nln != m_hoveredLine) {
m_hoveredNodeId = nid;
m_hoveredLine = nln;
applyHoverHighlight();
}
}
applyHoverCursor();
});
}
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
popup->populate(lm.nodeId,
lm.pointerTargetName, body, editorFont());
@@ -3452,14 +3563,8 @@ void RcxEditor::setCommandRowText(const QString& line) {
long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
m_sci->setReadOnly(false);
// Suppress modification notifications during replace to avoid
// QScintilla accessibility crash (textDeleted called with null text).
long savedMask = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODEVENTMASK);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETMODEVENTMASK, 0);
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
QByteArray utf8 = s.toUtf8();
@@ -3468,15 +3573,12 @@ void RcxEditor::setCommandRowText(const QString& line) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
m_sci->SendScintilla(QsciScintillaBase::SCI_SETMODEVENTMASK, savedMask);
// Adjust saved cursor/anchor for length change in line 0
long delta = (long)utf8.size() - oldLen;
if (savedPos > end) savedPos += delta;
if (savedAnchor > end) savedAnchor += delta;
if (wasReadOnly) m_sci->setReadOnly(true);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor);

View File

@@ -29,6 +29,8 @@ public:
void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; }
QWidget* historyPopup() const { return m_historyPopup; }
QWidget* disasmPopup() const { return m_disasmPopup; }
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,6 @@
#include "pluginmanager.h"
#include "scannerpanel.h"
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QLabel>
#include <QSplitter>
#include <QTabWidget>
@@ -24,6 +22,7 @@ namespace rcx {
class McpBridge;
class ShimmerLabel;
class DockGripWidget;
class MainWindow : public QMainWindow {
Q_OBJECT
@@ -71,22 +70,19 @@ public:
void clearMcpStatus();
// Project Lifecycle API
QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);
QDockWidget* project_new(const QString& classKeyword = QString());
QDockWidget* project_open(const QString& path = {});
bool project_save(QDockWidget* dock = nullptr, bool saveAs = false);
void project_close(QDockWidget* dock = nullptr);
private:
enum ViewMode { VM_Reclass, VM_Rendered };
QMdiArea* m_mdiArea;
QWidget* m_centralPlaceholder;
ShimmerLabel* m_statusLabel;
QString m_appStatus;
bool m_mcpBusy = false;
QTimer* m_mcpClearTimer = nullptr;
QButtonGroup* m_viewBtnGroup = nullptr;
QPushButton* m_btnReclass = nullptr;
QPushButton* m_btnRendered = nullptr;
TitleBarWidget* m_titleBar = nullptr;
QMenuBar* m_menuBar = nullptr;
bool m_menuBarTitleCase = false;
@@ -94,7 +90,6 @@ private:
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
QAction* m_removeSplitAction = nullptr;
QMenu* m_sourceMenu = nullptr;
QMenu* m_recentFilesMenu = nullptr;
@@ -116,7 +111,9 @@ private:
QVector<SplitPane> panes;
int activePaneIdx = 0;
};
QMap<QMdiSubWindow*, TabState> m_tabs;
QMap<QDockWidget*, TabState> m_tabs;
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
void rebuildAllDocs();
@@ -133,8 +130,10 @@ private:
TabState* activeTab();
TabState* tabByIndex(int index);
int tabCount() const { return m_tabs.size(); }
QMdiSubWindow* createTab(RcxDocument* doc);
QDockWidget* createTab(RcxDocument* doc);
void setupDockTabBars();
void updateWindowTitle();
void closeAllDocDocks();
void setViewMode(ViewMode mode);
void updateRenderedView(TabState& tab, SplitPane& pane);
@@ -144,7 +143,6 @@ private:
SplitPane createSplitPane(TabState& tab);
void applyTheme(const Theme& theme);
void styleTabCloseButtons();
void syncViewButtons(ViewMode mode);
SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane();
@@ -158,6 +156,7 @@ private:
QLineEdit* m_workspaceSearch = nullptr;
QLabel* m_dockTitleLabel = nullptr;
QToolButton* m_dockCloseBtn = nullptr;
DockGripWidget* m_dockGrip = nullptr;
void createWorkspaceDock();
void rebuildWorkspaceModel();
void updateBorderColor(const QColor& color);
@@ -167,6 +166,7 @@ private:
ScannerPanel* m_scannerPanel = nullptr;
QLabel* m_scanDockTitle = nullptr;
QToolButton* m_scanDockCloseBtn = nullptr;
DockGripWidget* m_scanDockGrip = nullptr;
void createScannerDock();
protected:

View File

@@ -40,9 +40,21 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_tree->setHeaderHidden(true);
m_tree->setRootIsDecorated(true);
m_tree->setFixedWidth(200);
m_tree->setMouseTracking(true);
m_tree->setIconSize(QSize(16, 16));
{
const auto& t = ThemeManager::instance().current();
QPalette tp = m_tree->palette();
tp.setColor(QPalette::Text, t.textDim);
tp.setColor(QPalette::Highlight, t.hover);
tp.setColor(QPalette::HighlightedText, t.text);
m_tree->setPalette(tp);
}
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
envItem->setIcon(0, QIcon(":/vsicons/folder.svg"));
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
generalItem->setIcon(0, QIcon(":/vsicons/settings-gear.svg"));
m_tree->expandAll();
m_tree->setCurrentItem(generalItem);
leftColumn->addWidget(m_tree, 1);
@@ -102,7 +114,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_fontCombo->setObjectName("fontCombo");
visualLayout->addRow("Editor Font:", m_fontCombo);
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
m_titleCaseCheck = new QCheckBox("Uppercase menu items");
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
visualLayout->addRow(m_titleCaseCheck);
@@ -110,25 +122,11 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_showIconCheck->setChecked(current.showIcon);
visualLayout->addRow(m_showIconCheck);
m_braceWrapCheck = new QCheckBox("Opening brace on new line");
m_braceWrapCheck->setChecked(current.braceWrap);
visualLayout->addRow(m_braceWrapCheck);
generalLayout->addWidget(visualGroup);
// Safe Mode group box
auto* safeModeGroup = new QGroupBox("Preview Features");
auto* safeModeLayout = new QVBoxLayout(safeModeGroup);
safeModeLayout->setSpacing(4);
m_safeModeCheck = new QCheckBox("Safe Mode");
m_safeModeCheck->setChecked(current.safeMode);
safeModeLayout->addWidget(m_safeModeCheck);
auto* safeModeDesc = new QLabel(
"Enable to use the default OS icon for this application and "
"create the window with the name of the executable file.");
safeModeDesc->setWordWrap(true);
safeModeDesc->setContentsMargins(20, 0, 0, 0); // indent under checkbox
safeModeLayout->addWidget(safeModeDesc);
generalLayout->addWidget(safeModeGroup);
generalLayout->addStretch();
m_pages->addWidget(generalPage); // index 0
@@ -136,6 +134,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
// -- AI Features page --
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
aiItem->setIcon(0, QIcon(":/vsicons/remote.svg"));
auto* aiPage = new QWidget;
auto* aiLayout = new QVBoxLayout(aiPage);
@@ -165,6 +164,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
// -- Generator page --
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
generatorItem->setIcon(0, QIcon(":/vsicons/code.svg"));
auto* generatorPage = new QWidget;
auto* generatorLayout = new QVBoxLayout(generatorPage);
@@ -213,10 +213,10 @@ OptionsResult OptionsDialog::result() const {
r.fontName = m_fontCombo->currentText();
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
r.showIcon = m_showIconCheck->isChecked();
r.safeMode = m_safeModeCheck->isChecked();
r.autoStartMcp = m_autoMcpCheck->isChecked();
r.refreshMs = m_refreshSpin->value();
r.generatorAsserts = m_assertCheck->isChecked();
r.braceWrap = m_braceWrapCheck->isChecked();
return r;
}

View File

@@ -15,10 +15,10 @@ struct OptionsResult {
QString fontName;
bool menuBarTitleCase = true;
bool showIcon = false;
bool safeMode = false;
bool autoStartMcp = true;
int refreshMs = 660;
bool generatorAsserts = false;
bool braceWrap = false;
};
class OptionsDialog : public QDialog {
@@ -39,10 +39,10 @@ private:
QComboBox* m_fontCombo = nullptr;
QCheckBox* m_titleCaseCheck = nullptr;
QCheckBox* m_showIconCheck = nullptr;
QCheckBox* m_safeModeCheck = nullptr;
QCheckBox* m_autoMcpCheck = nullptr;
QSpinBox* m_refreshSpin = nullptr;
QCheckBox* m_assertCheck = nullptr;
QCheckBox* m_braceWrapCheck = nullptr;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;

View File

@@ -55,6 +55,7 @@ void ProcessPicker::initUi()
ui->processTable->setColumnWidth(0, 80); // PID column
ui->processTable->setColumnWidth(1, 200); // Name column
ui->processTable->horizontalHeader()->setStretchLastSection(true);
ui->processTable->setSortingEnabled(true);
ui->processTable->setWordWrap(false);
ui->processTable->setTextElideMode(Qt::ElideLeft);
ui->processTable->setShowGrid(false);
@@ -329,6 +330,9 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
pathItem->setToolTip(proc.path); // Show full path on hover
ui->processTable->setItem(i, 2, pathItem);
}
// Default sort: highest PID first (most recently launched processes on top)
ui->processTable->sortItems(0, Qt::DescendingOrder);
}
void ProcessPicker::filterProcesses(const QString& text)

View File

@@ -60,5 +60,9 @@
<file alias="search.svg">vsicons/search.svg</file>
<file alias="regex.svg">vsicons/regex.svg</file>
<file alias="refresh.svg">vsicons/refresh.svg</file>
<file alias="pin.svg">vsicons/pin.svg</file>
<file alias="pinned.svg">vsicons/pinned.svg</file>
<file alias="close-all.svg">vsicons/close-all.svg</file>
<file alias="split-vertical.svg">vsicons/split-vertical.svg</file>
</qresource>
</RCC>

View File

@@ -533,6 +533,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
regEnd = qMin(regEnd, req.endAddress);
}
uint64_t regSize = regEnd - regStart;
if (regSize == 0) continue;
if ((uint64_t)patternLen > regSize) {
scannedBytes += regSize;

View File

@@ -183,6 +183,7 @@ ScannerPanel::ScannerPanel(QWidget* parent)
QStringLiteral("Copy Address"), this);
m_copyBtn->setEnabled(false);
actionRow->addWidget(m_copyBtn);
actionRow->addSpacing(20); // room for resize grip when floating
mainLayout->addLayout(actionRow);

View File

@@ -8,8 +8,8 @@
"button": "#ccccd0",
"text": "#1b1b22",
"textDim": "#5c5c68",
"textMuted": "#84848e",
"textFaint": "#a8a8b0",
"textMuted": "#6a6a78",
"textFaint": "#8a8a94",
"hover": "#d8d8de",
"selected": "#d0d0d8",
"selection": "#b4c8e8",

View File

@@ -64,7 +64,8 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
// ── File info ──
m_fileInfoLabel = new QLabel;
m_fileInfoLabel->setStyleSheet(QStringLiteral("color: #666; font-size: 10px; padding: 0 0 4px 0;"));
m_fileInfoLabel->setStyleSheet(QStringLiteral("color: %1; font-size: 10px; padding: 0 0 4px 0;")
.arg(tm.current().textDim.name()));
QString path = tm.themeFilePath(themeIndex);
m_fileInfoLabel->setText(path.isEmpty()
? QStringLiteral("Built-in theme (edits save as user copy)")
@@ -109,7 +110,8 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
auto* hexLbl = new QLabel;
hexLbl->setFixedWidth(60);
hexLbl->setStyleSheet(QStringLiteral("color: #aaa; font-size: 10px;"));
hexLbl->setStyleSheet(QStringLiteral("color: %1; font-size: 10px;")
.arg(tm.current().textMuted.name()));
row->addWidget(hexLbl);
row->addStretch();

View File

@@ -95,10 +95,10 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
m_btnMin->setStyleSheet(btnStyle);
m_btnMax->setStyleSheet(btnStyle);
// Close button: red hover
// Close button: themed red hover
m_btnClose->setStyleSheet(QStringLiteral(
"QToolButton { background: transparent; border: none; }"
"QToolButton:hover { background: #c42b1c; }"));
"QToolButton:hover { background: %1; }").arg(theme.indHeatHot.name()));
update();
}
@@ -107,12 +107,14 @@ void TitleBarWidget::setShowIcon(bool show) {
if (show) {
m_appLabel->setText(QString());
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(24, 24));
setFixedHeight(34);
} else {
m_appLabel->setPixmap(QPixmap());
m_appLabel->setText(QStringLiteral("Reclass"));
m_appLabel->setStyleSheet(
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
.arg(m_theme.text.name()));
setFixedHeight(32);
}
}

View File

@@ -10,7 +10,7 @@ namespace rcx {
struct TabInfo {
const NodeTree* tree;
QString name;
void* subPtr; // QMdiSubWindow* as void*
void* subPtr; // QDockWidget* as void*
};
// Sentinel value stored in UserRole+1 to mark the Project group node.
@@ -46,18 +46,12 @@ inline void buildProjectExplorer(QStandardItemModel* model,
auto nameOf = [](const Node* n) {
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
};
// Sort structs by children count descending (most fields first)
auto cmpChildren = [&](const Entry& a, const Entry& b) {
int ca = a.tree->childrenOf(a.node->id).size();
int cb = b.tree->childrenOf(b.node->id).size();
if (ca != cb) return ca > cb;
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
// Helper: is a Hex padding node
auto isHexPad = [](NodeKind k) {
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
};
std::sort(types.begin(), types.end(), cmpChildren);
auto cmpName = [&](const Entry& a, const Entry& b) {
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
};
std::sort(enums.begin(), enums.end(), cmpName);
// Helper: type display string for a member node
auto memberTypeName = [](const Node& m) -> QString {
@@ -69,11 +63,24 @@ inline void buildProjectExplorer(QStandardItemModel* model,
return QString::fromLatin1(kindToString(m.kind));
};
// Helper: is a Hex padding node
auto isHexPad = [](NodeKind k) {
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
// Sort structs by visible children count descending (most fields first)
auto countVisible = [&](const Entry& e) {
int n = 0;
for (int idx : e.tree->childrenOf(e.node->id))
if (!isHexPad(e.tree->nodes[idx].kind)) ++n;
return n;
};
auto cmpChildren = [&](const Entry& a, const Entry& b) {
int ca = countVisible(a);
int cb = countVisible(b);
if (ca != cb) return ca > cb;
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
};
std::sort(types.begin(), types.end(), cmpChildren);
auto cmpName = [&](const Entry& a, const Entry& b) {
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
};
std::sort(enums.begin(), enums.end(), cmpName);
for (const auto& e : types) {
QVector<int> members = e.tree->childrenOf(e.node->id);

View File

@@ -2768,6 +2768,125 @@ private slots:
"Static fields should not have a separator line");
}
}
// ── Test: disasm popup dismisses when mouse moves onto it ("see-through") ──
//
// Scenario: hover a FuncPtr row → disasm popup appears below the row.
// User moves mouse down onto the popup. The popup covers rows behind it
// but the mouse position maps to a different node's row in the viewport
// underneath, so the popup must dismiss.
void testDisasmPopupDismissesOnMouseMoveThrough() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "TestClass";
root.name = "TestClass";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// FuncPtr64 at offset 0 — its value points to "code" at byte 256
Node fp;
fp.kind = NodeKind::FuncPtr64;
fp.name = "VFunc1";
fp.parentId = rootId;
fp.offset = 0;
tree.addNode(fp);
// A plain UInt64 after it so there's a non-FuncPtr row below
Node pad;
pad.kind = NodeKind::UInt64;
pad.name = "padding";
pad.parentId = rootId;
pad.offset = 8;
tree.addNode(pad);
// Buffer layout:
// [0..7] FuncPtr value = 256 (points to code bytes)
// [8..15] padding field value
// [256..383] x86 code bytes (push rbp; mov rbp,rsp; nop...; ret)
QByteArray data(512, '\0');
uint64_t codeAddr = 256;
memcpy(data.data(), &codeAddr, 8);
const uint8_t code[] = {
0x55, // push rbp
0x48, 0x89, 0xE5, // mov rbp, rsp
0x90, // nop
0x90, // nop
0x5D, // pop rbp
0xC3 // ret
};
memcpy(data.data() + 256, code, sizeof(code));
BufferProvider prov(data, "test_disasm_dismiss");
ComposeResult cr = compose(tree, prov);
m_editor->applyDocument(cr);
m_editor->setProviderRef(&prov, nullptr, &tree);
QApplication::processEvents();
// Find the FuncPtr line
int fpLine = -1;
for (int i = 0; i < cr.meta.size(); ++i) {
if (isFuncPtr(cr.meta[i].nodeKind)) {
fpLine = i;
break;
}
}
QVERIFY2(fpLine >= 0, "Could not find FuncPtr64 line in compose output");
// Hover over the FuncPtr value column to trigger the disasm popup
const LineMeta& lm = cr.meta[fpLine];
QString lineText;
{
long len = m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)fpLine);
QByteArray buf(len + 1, '\0');
m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_GETLINE, (uintptr_t)fpLine,
static_cast<const char*>(buf.data()));
lineText = QString::fromUtf8(buf.left(len));
}
ColumnSpan vs = m_editor->valueSpan(lm, lineText.size(),
lm.effectiveTypeW, lm.effectiveNameW);
QVERIFY2(vs.valid, "Value span for FuncPtr line is not valid");
int hoverCol = (vs.start + vs.end) / 2;
QPoint vpFP = colToViewport(m_editor->scintilla(), fpLine, hoverCol);
sendMouseMove(m_editor->scintilla()->viewport(), vpFP);
QApplication::processEvents();
QWidget* popup = m_editor->disasmPopup();
QVERIFY2(popup && popup->isVisible(),
"Disasm popup should be visible after hovering the FuncPtr value");
// See-through behavior: when the user moves the mouse down from the
// viewport onto the popup, the popup's mouseMoveEvent override forwards
// the global position back to the viewport hover logic. If the row
// underneath the popup represents a different node, the popup dismisses.
//
// Simulate by sending a MouseMove event to the popup at a global
// position that maps to the CommandRow (line 0) — a non-FuncPtr row.
// sendEvent triggers the virtual mouseMoveEvent directly.
QPoint vpCmdRow = colToViewport(m_editor->scintilla(), 0, hoverCol);
QPoint globalCmdRow = m_editor->scintilla()->viewport()->mapToGlobal(vpCmdRow);
QPoint localOnPopup = popup->mapFromGlobal(globalCmdRow);
QMouseEvent moveOnPopup(QEvent::MouseMove,
QPointF(localOnPopup), QPointF(globalCmdRow),
Qt::NoButton, Qt::NoButton, Qt::NoModifier);
QApplication::sendEvent(popup, &moveOnPopup);
QApplication::processEvents();
QVERIFY2(!popup->isVisible(),
"Disasm popup must dismiss when mouseMoveEvent forwards "
"to a non-FuncPtr row underneath (see-through behavior)");
// Restore
m_editor->setProviderRef(nullptr, nullptr, nullptr);
m_editor->applyDocument(m_result);
}
};
QTEST_MAIN(TestEditor)

View File

@@ -59,7 +59,6 @@ private slots:
defaults.themeIndex = 0;
defaults.fontName = "JetBrains Mono";
defaults.menuBarTitleCase = true;
defaults.safeMode = false;
defaults.autoStartMcp = false;
OptionsDialog dlg(defaults);
@@ -93,7 +92,6 @@ private slots:
input.themeIndex = 1;
input.fontName = "Consolas";
input.menuBarTitleCase = false;
input.safeMode = true;
input.autoStartMcp = true;
OptionsDialog dlg(input);
@@ -102,7 +100,6 @@ private slots:
QCOMPARE(r.themeIndex, 1);
QCOMPARE(r.fontName, QString("Consolas"));
QCOMPARE(r.menuBarTitleCase, false);
QCOMPARE(r.safeMode, true);
QCOMPARE(r.autoStartMcp, true);
}