mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
5 Commits
snapshot-0
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a716444f4 | ||
|
|
a46da4ee16 | ||
|
|
cd52451210 | ||
|
|
82bf9118c9 | ||
|
|
f4c7e9327d |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -1766,28 +1772,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
|
||||
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 +1803,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 +1860,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 +1882,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 +2140,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 +2223,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -966,12 +966,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
|
||||
|
||||
118
src/main.cpp
118
src/main.cpp
@@ -237,10 +237,14 @@ 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))
|
||||
if (qobject_cast<QMenu*>(w)) {
|
||||
w->setWindowFlag(Qt::FramelessWindowHint, true);
|
||||
// Layered window — gives full pixel control; DWM won't clip edges.
|
||||
// (The DwmSetWindowAttribute conflict noted in RcxTooltip doesn't
|
||||
// apply here: DarkApp::notify only fires on WindowActivate, which
|
||||
// popups never receive.)
|
||||
w->setAttribute(Qt::WA_TranslucentBackground);
|
||||
}
|
||||
QProxyStyle::polish(w);
|
||||
}
|
||||
using QProxyStyle::polish;
|
||||
@@ -251,11 +255,13 @@ public:
|
||||
s.setHeight(s.height() + qRound(s.height() * 0.5));
|
||||
if (type == CT_MenuItem)
|
||||
s = QSize(s.width() + 24, s.height() + 4);
|
||||
if (type == CT_ItemViewItem)
|
||||
s.setHeight(s.height() + 4);
|
||||
return s;
|
||||
}
|
||||
int pixelMetric(PixelMetric metric, const QStyleOption* opt,
|
||||
const QWidget* w) const override {
|
||||
// Reserve 1px for our own menu border (drawn in PE_FrameMenu)
|
||||
// 1px border drawn in PE_FrameMenu
|
||||
if (metric == PM_MenuPanelWidth)
|
||||
return 1;
|
||||
// Inset menu items from border so hover rect doesn't touch edges
|
||||
@@ -268,11 +274,18 @@ public:
|
||||
}
|
||||
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
|
||||
QPainter* p, const QWidget* w) const override {
|
||||
// Clean 1px border on QMenu (replaces Fusion's 3D bevel + OS shadow)
|
||||
// Opaque fill + 1px border at the true widget edge.
|
||||
// WA_TranslucentBackground (set in polish) makes this a layered window,
|
||||
// so DWM doesn't clip any edges.
|
||||
if (elem == PE_FrameMenu) {
|
||||
QRect r = opt->rect;
|
||||
p->fillRect(r, opt->palette.color(QPalette::Window));
|
||||
p->setPen(opt->palette.color(QPalette::Dark));
|
||||
p->setBrush(Qt::NoBrush);
|
||||
p->drawRect(opt->rect.adjusted(0, 0, -1, -1));
|
||||
int x2 = r.right(), y2 = r.bottom();
|
||||
p->drawLine(r.left(), r.top(), x2, r.top()); // top
|
||||
p->drawLine(r.left(), y2, x2, y2); // bottom
|
||||
p->drawLine(r.left(), r.top(), r.left(), y2); // left
|
||||
p->drawLine(x2, r.top(), x2, y2); // right
|
||||
return;
|
||||
}
|
||||
// Kill the status bar item frame and panel border
|
||||
@@ -309,9 +322,9 @@ public:
|
||||
// Only fill background for hover/pressed — non-hovered stays
|
||||
// transparent so the parent's border line shows through.
|
||||
if (sunken)
|
||||
p->fillRect(area, mi->palette.color(QPalette::Mid).darker(130));
|
||||
p->fillRect(area, mi->palette.color(QPalette::Highlight).darker(130));
|
||||
else if (selected)
|
||||
p->fillRect(area, mi->palette.color(QPalette::Mid));
|
||||
p->fillRect(area, mi->palette.color(QPalette::Highlight));
|
||||
|
||||
QColor fg = (selected || sunken)
|
||||
? mi->palette.color(QPalette::Link)
|
||||
@@ -321,15 +334,23 @@ public:
|
||||
return; // never delegate to Fusion
|
||||
}
|
||||
}
|
||||
// Popup menu items — palette patch then delegate to Fusion
|
||||
// Popup menu items
|
||||
if (element == CE_MenuItem) {
|
||||
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
|
||||
if ((mi->state & State_Selected)
|
||||
&& mi->menuItemType != QStyleOptionMenuItem::Separator) {
|
||||
// Subtle separator — single line using surface color
|
||||
if (mi->menuItemType == QStyleOptionMenuItem::Separator) {
|
||||
int y = mi->rect.center().y();
|
||||
p->setPen(mi->palette.color(QPalette::AlternateBase));
|
||||
p->drawLine(mi->rect.left() + 4, y, mi->rect.right() - 4, y);
|
||||
return;
|
||||
}
|
||||
// Hover highlight — flat fill (no Fusion border) then delegate
|
||||
// for text/icon/arrow with Selected cleared
|
||||
if ((mi->state & State_Selected)) {
|
||||
p->fillRect(mi->rect, mi->palette.color(QPalette::Highlight));
|
||||
QStyleOptionMenuItem patched = *mi;
|
||||
patched.palette.setColor(QPalette::Highlight,
|
||||
mi->palette.color(QPalette::Mid)); // theme.hover
|
||||
patched.palette.setColor(QPalette::HighlightedText,
|
||||
patched.state &= ~State_Selected;
|
||||
patched.palette.setColor(QPalette::Text,
|
||||
mi->palette.color(QPalette::Link)); // theme.indHoverSpan
|
||||
QProxyStyle::drawControl(element, &patched, p, w);
|
||||
return;
|
||||
@@ -365,7 +386,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::Highlight, theme.selected);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||
@@ -770,6 +791,34 @@ private:
|
||||
QColor m_color;
|
||||
};
|
||||
|
||||
// ── Dock title-bar grip (VS2022-style dot pattern) ──
|
||||
class DockGripWidget : public QWidget {
|
||||
public:
|
||||
explicit DockGripWidget(QWidget* parent) : QWidget(parent) {
|
||||
setFixedWidth(6);
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
|
||||
m_color = rcx::ThemeManager::instance().current().textFaint;
|
||||
}
|
||||
void setGripColor(const QColor& c) { m_color = c; update(); }
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(m_color);
|
||||
const double r = 0.75, s = 3.0;
|
||||
double cx = width() / 2.0;
|
||||
double cy = height() / 2.0;
|
||||
// 2 columns x 3 rows, centered
|
||||
for (int row = -1; row <= 1; row++) {
|
||||
p.drawEllipse(QPointF(cx - s * 0.5, cy + row * s), r, r);
|
||||
p.drawEllipse(QPointF(cx + s * 0.5, cy + row * s), r, r);
|
||||
}
|
||||
}
|
||||
private:
|
||||
QColor m_color;
|
||||
};
|
||||
|
||||
// ── Custom-painted view tab button (no CSS) ──
|
||||
class ViewTabButton : public QPushButton {
|
||||
public:
|
||||
@@ -1882,6 +1931,8 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
if (m_dockGrip)
|
||||
m_dockGrip->setGripColor(theme.textFaint);
|
||||
|
||||
// Scanner dock
|
||||
if (m_scannerPanel)
|
||||
@@ -1901,6 +1952,8 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
if (m_scanDockGrip)
|
||||
m_scanDockGrip->setGripColor(theme.textFaint);
|
||||
|
||||
// Rendered C/C++ views: update lexer colors, paper, margins
|
||||
for (auto& tab : m_tabs) {
|
||||
@@ -1956,7 +2009,6 @@ void MainWindow::showOptionsDialog() {
|
||||
current.showIcon = m_titleBar
|
||||
? QSettings("Reclass", "Reclass").value("showIcon", false).toBool()
|
||||
: false;
|
||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
current.generatorAsserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||
@@ -1983,9 +2035,6 @@ void MainWindow::showOptionsDialog() {
|
||||
QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon);
|
||||
}
|
||||
|
||||
if (r.safeMode != current.safeMode)
|
||||
QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode);
|
||||
|
||||
if (r.autoStartMcp != current.autoStartMcp)
|
||||
QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp);
|
||||
|
||||
@@ -2224,6 +2273,21 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
|
||||
// Set text
|
||||
pane.rendered->setText(text);
|
||||
|
||||
// Set horizontal scroll width to match the longest line (ignoring trailing spaces)
|
||||
{
|
||||
int maxLen = 0;
|
||||
const QStringList lines = text.split(QChar('\n'));
|
||||
for (const auto& line : lines) {
|
||||
int len = (int)line.size();
|
||||
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
||||
maxLen = std::max(maxLen, len);
|
||||
}
|
||||
QFontMetrics fm(pane.rendered->font());
|
||||
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
||||
pane.rendered->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH,
|
||||
(unsigned long)qMax(1, pixelWidth));
|
||||
}
|
||||
|
||||
// Update margin width for line count
|
||||
int lineCount = pane.rendered->lines();
|
||||
QString marginStr = QString(QString::number(lineCount).size() + 2, '0');
|
||||
@@ -2683,8 +2747,11 @@ void MainWindow::createWorkspaceDock() {
|
||||
titleBar->setPalette(tbPal);
|
||||
}
|
||||
auto* layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(6, 2, 2, 2);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(4, 2, 2, 2);
|
||||
layout->setSpacing(4);
|
||||
|
||||
m_dockGrip = new DockGripWidget(titleBar);
|
||||
layout->addWidget(m_dockGrip);
|
||||
|
||||
m_dockTitleLabel = new QLabel("Project", titleBar);
|
||||
{
|
||||
@@ -2955,8 +3022,11 @@ void MainWindow::createScannerDock() {
|
||||
titleBar->setPalette(tbPal);
|
||||
}
|
||||
auto* layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(6, 2, 2, 2);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(4, 2, 2, 2);
|
||||
layout->setSpacing(4);
|
||||
|
||||
m_scanDockGrip = new DockGripWidget(titleBar);
|
||||
layout->addWidget(m_scanDockGrip);
|
||||
|
||||
m_scanDockTitle = new QLabel("Scanner", titleBar);
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace rcx {
|
||||
|
||||
class McpBridge;
|
||||
class ShimmerLabel;
|
||||
class DockGripWidget;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -158,6 +159,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 +169,7 @@ private:
|
||||
ScannerPanel* m_scannerPanel = nullptr;
|
||||
QLabel* m_scanDockTitle = nullptr;
|
||||
QToolButton* m_scanDockCloseBtn = nullptr;
|
||||
DockGripWidget* m_scanDockGrip = nullptr;
|
||||
void createScannerDock();
|
||||
|
||||
protected:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -111,24 +123,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
visualLayout->addRow(m_showIconCheck);
|
||||
|
||||
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 +130,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 +160,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,7 +209,6 @@ 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();
|
||||
|
||||
@@ -15,7 +15,6 @@ struct OptionsResult {
|
||||
QString fontName;
|
||||
bool menuBarTitleCase = true;
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = true;
|
||||
int refreshMs = 660;
|
||||
bool generatorAsserts = false;
|
||||
@@ -39,7 +38,6 @@ 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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"button": "#ccccd0",
|
||||
"text": "#1b1b22",
|
||||
"textDim": "#5c5c68",
|
||||
"textMuted": "#84848e",
|
||||
"textFaint": "#a8a8b0",
|
||||
"textMuted": "#6a6a78",
|
||||
"textFaint": "#8a8a94",
|
||||
"hover": "#d8d8de",
|
||||
"selected": "#d0d0d8",
|
||||
"selection": "#b4c8e8",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user