mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
29 Commits
snapshot-0
...
msvc-fix-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
955db3813a | ||
|
|
f4f203e0f0 | ||
|
|
1d3f1a672a | ||
|
|
da29206bdb | ||
|
|
4986893fca | ||
|
|
17a1fb032e | ||
|
|
8d92957837 | ||
|
|
f981fe456d | ||
|
|
877ceea4c1 | ||
|
|
4160a229c6 | ||
|
|
1e1afc1640 | ||
|
|
f0cf6c549a | ||
|
|
683eab16ee | ||
|
|
b53dea8f9f | ||
|
|
f06abbab79 | ||
|
|
2477591ed2 | ||
|
|
6c13356d6d | ||
|
|
3b273a7ab2 | ||
|
|
3509a0d9dd | ||
|
|
43c3f5a842 | ||
|
|
0697ce4853 | ||
|
|
ed1bfd04cd | ||
|
|
c275eb33c9 | ||
|
|
636176ee8c | ||
|
|
9a716444f4 | ||
|
|
a46da4ee16 | ||
|
|
cd52451210 | ||
|
|
82bf9118c9 | ||
|
|
f4c7e9327d |
@@ -109,6 +109,8 @@ add_executable(Reclass
|
|||||||
src/scannerpanel.h
|
src/scannerpanel.h
|
||||||
src/scannerpanel.cpp
|
src/scannerpanel.cpp
|
||||||
src/mainwindow.h
|
src/mainwindow.h
|
||||||
|
src/startpage.h
|
||||||
|
src/dock_tab_buttons.h
|
||||||
src/optionsdialog.h
|
src/optionsdialog.h
|
||||||
src/optionsdialog.cpp
|
src/optionsdialog.cpp
|
||||||
src/titlebar.h
|
src/titlebar.h
|
||||||
@@ -344,6 +346,11 @@ if(BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
add_test(NAME test_controller COMMAND test_controller)
|
add_test(NAME test_controller COMMAND test_controller)
|
||||||
|
|
||||||
|
add_executable(grab_tabs tests/grab_tabs.cpp
|
||||||
|
src/themes/theme.cpp src/themes/thememanager.cpp src/resources.qrc)
|
||||||
|
target_include_directories(grab_tabs PRIVATE src)
|
||||||
|
target_link_libraries(grab_tabs PRIVATE ${QT}::Widgets ${QT}::Svg ${QT}::Test)
|
||||||
|
|
||||||
add_executable(test_validation tests/test_validation.cpp
|
add_executable(test_validation tests/test_validation.cpp
|
||||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, In
|
|||||||
|
|
||||||
## Plugin System
|
## 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:**
|
**Bundled plugins:**
|
||||||
|
|
||||||
|
|||||||
@@ -66,15 +66,26 @@
|
|||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
||||||
|
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
</Link>
|
</Link>
|
||||||
|
<PostBuildEvent>
|
||||||
|
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
|
||||||
|
</PostBuildEvent>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
</Link>
|
</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>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
@@ -129,10 +140,12 @@
|
|||||||
<ClInclude Include="..\src\addressparser.h" />
|
<ClInclude Include="..\src\addressparser.h" />
|
||||||
<ClInclude Include="..\src\core.h" />
|
<ClInclude Include="..\src\core.h" />
|
||||||
<ClInclude Include="..\src\disasm.h" />
|
<ClInclude Include="..\src\disasm.h" />
|
||||||
|
<QtMoc Include="..\src\dock_tab_buttons.h" />
|
||||||
<ClInclude Include="..\src\generator.h" />
|
<ClInclude Include="..\src\generator.h" />
|
||||||
<ClInclude Include="..\src\iplugin.h" />
|
<ClInclude Include="..\src\iplugin.h" />
|
||||||
<ClInclude Include="..\src\pluginmanager.h" />
|
<ClInclude Include="..\src\pluginmanager.h" />
|
||||||
<ClInclude Include="..\src\providerregistry.h" />
|
<ClInclude Include="..\src\providerregistry.h" />
|
||||||
|
<QtMoc Include="..\src\startpage.h" />
|
||||||
<ClInclude Include="..\src\workspace_model.h" />
|
<ClInclude Include="..\src\workspace_model.h" />
|
||||||
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
|
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
|
||||||
<ClInclude Include="..\src\imports\import_pdb.h" />
|
<ClInclude Include="..\src\imports\import_pdb.h" />
|
||||||
@@ -152,7 +165,12 @@
|
|||||||
<ClCompile Include="..\src\editor.cpp" />
|
<ClCompile Include="..\src\editor.cpp" />
|
||||||
<ClCompile Include="..\src\format.cpp" />
|
<ClCompile Include="..\src\format.cpp" />
|
||||||
<ClCompile Include="..\src\generator.cpp" />
|
<ClCompile Include="..\src\generator.cpp" />
|
||||||
<ClCompile Include="..\src\main.cpp" />
|
<ClCompile Include="..\src\main.cpp">
|
||||||
|
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">input</DynamicSource>
|
||||||
|
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(Filename).moc</QtMocFileName>
|
||||||
|
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
|
||||||
|
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(Filename).moc</QtMocFileName>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="..\src\optionsdialog.cpp" />
|
<ClCompile Include="..\src\optionsdialog.cpp" />
|
||||||
<ClCompile Include="..\src\pluginmanager.cpp" />
|
<ClCompile Include="..\src\pluginmanager.cpp" />
|
||||||
<ClCompile Include="..\src\processpicker.cpp" />
|
<ClCompile Include="..\src\processpicker.cpp" />
|
||||||
|
|||||||
@@ -89,6 +89,12 @@
|
|||||||
<QtMoc Include="..\src\themes\thememanager.h">
|
<QtMoc Include="..\src\themes\thememanager.h">
|
||||||
<Filter>Header Files\themes</Filter>
|
<Filter>Header Files\themes</Filter>
|
||||||
</QtMoc>
|
</QtMoc>
|
||||||
|
<QtMoc Include="..\src\dock_tab_buttons.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</QtMoc>
|
||||||
|
<QtMoc Include="..\src\startpage.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</QtMoc>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="..\src\addressparser.h">
|
<ClInclude Include="..\src\addressparser.h">
|
||||||
@@ -165,9 +171,6 @@
|
|||||||
<ClCompile Include="..\src\generator.cpp">
|
<ClCompile Include="..\src\generator.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="..\src\main.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="..\src\optionsdialog.cpp">
|
<ClCompile Include="..\src\optionsdialog.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -219,5 +222,8 @@
|
|||||||
<ClCompile Include="..\src\themes\thememanager.cpp">
|
<ClCompile Include="..\src\themes\thememanager.cpp">
|
||||||
<Filter>Source Files\themes</Filter>
|
<Filter>Source Files\themes</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="..\src\main.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -25,6 +25,7 @@ struct ComposeState {
|
|||||||
bool baseEmitted = false; // only first root struct shows base address
|
bool baseEmitted = false; // only first root struct shows base address
|
||||||
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||||
bool treeLines = false; // draw Unicode tree connectors in indentation
|
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
|
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
||||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
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;
|
lm.effectiveNameW = nameW;
|
||||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
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) {
|
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||||
@@ -840,9 +858,26 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
lm.pointerTargetName = ptrTargetName;
|
lm.pointerTargetName = ptrTargetName;
|
||||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
{
|
||||||
prov, absAddr, ptrTypeOverride,
|
QString ptrText = fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||||
typeW, nameW, state.compactColumns), lm);
|
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) {
|
if (!effectiveCollapsed) {
|
||||||
@@ -936,10 +971,11 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||||
bool compactColumns, bool treeLines) {
|
bool compactColumns, bool treeLines, bool braceWrap) {
|
||||||
ComposeState state;
|
ComposeState state;
|
||||||
state.compactColumns = compactColumns;
|
state.compactColumns = compactColumns;
|
||||||
state.treeLines = treeLines;
|
state.treeLines = treeLines;
|
||||||
|
state.braceWrap = braceWrap;
|
||||||
|
|
||||||
// Precompute parent→children map
|
// Precompute parent→children map
|
||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
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)) {
|
for (int childIdx : state.childMap.value(container.id)) {
|
||||||
const Node& child = tree.nodes[childIdx];
|
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());
|
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||||
|
|
||||||
// Name width (skip hex, but include containers)
|
// 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;
|
int rootMaxName = kMinNameW;
|
||||||
for (int childIdx : state.childMap.value(0)) {
|
for (int childIdx : state.childMap.value(0)) {
|
||||||
const Node& child = tree.nodes[childIdx];
|
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());
|
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||||
|
|
||||||
// Name width (skip hex, include containers)
|
// 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);
|
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);
|
const QVector<int>& roots = childIndices(state, 0);
|
||||||
|
|
||||||
for (int idx : roots) {
|
for (int idx : roots) {
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
||||||
bool treeLines) const {
|
bool treeLines, bool braceWrap) const {
|
||||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines);
|
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RcxDocument::save(const QString& path) {
|
bool RcxDocument::save(const QString& path) {
|
||||||
@@ -558,9 +558,9 @@ void RcxController::refresh() {
|
|||||||
|
|
||||||
// Compose against snapshot provider if active, otherwise real provider
|
// Compose against snapshot provider if active, otherwise real provider
|
||||||
if (m_snapshotProv)
|
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
|
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;
|
s_composeDoc = nullptr;
|
||||||
|
|
||||||
@@ -1617,17 +1617,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
return indices;
|
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
|
// Quick-convert shortcuts when all selected nodes share the same kind
|
||||||
NodeKind commonKind = NodeKind::Hex64;
|
NodeKind commonKind = NodeKind::Hex64;
|
||||||
bool allSame = true;
|
bool allSame = true;
|
||||||
@@ -1695,31 +1684,19 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
});
|
});
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// ── Insert ► submenu ──
|
||||||
{
|
{
|
||||||
auto* act = menu.addAction("Track Value Changes");
|
auto* insertMenu = menu.addMenu(icon("diff-added.svg"), "Insert");
|
||||||
act->setCheckable(true);
|
insertMenu->addAction("Insert 4", [this]() {
|
||||||
act->setChecked(m_trackValues);
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
||||||
}
|
});
|
||||||
{
|
insertMenu->addAction("Insert 8", [this]() {
|
||||||
auto* act = menu.addAction("Clear Value History");
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
|
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menu.addSeparator();
|
|
||||||
|
|
||||||
// Check if all selected nodes share the same parent (required for grouping)
|
// 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.addAction("Group into Union", [this, ids]() { groupIntoUnion(ids); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
|
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
|
||||||
for (uint64_t id : ids) {
|
for (uint64_t id : ids) {
|
||||||
int idx = m_doc->tree.indexOfId(id);
|
int idx = m_doc->tree.indexOfId(id);
|
||||||
@@ -1748,6 +1727,33 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
menu.addSeparator();
|
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");
|
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
||||||
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
||||||
QStringList addrs;
|
QStringList addrs;
|
||||||
@@ -1760,34 +1766,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
QApplication::clipboard()->setText(addrs.join('\n'));
|
QApplication::clipboard()->setText(addrs.join('\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emit contextMenuAboutToShow(&menu, line);
|
||||||
menu.exec(globalPos);
|
menu.exec(globalPos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMenu menu;
|
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) ──
|
// ── Node-specific actions (only when clicking on a node) ──
|
||||||
if (hasNode) {
|
if (hasNode) {
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
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
|
// Fall through to always-available actions
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Quick-convert suggestions for Hex nodes
|
// ── Quick-convert suggestions (top-level for fast access) ──
|
||||||
bool addedQuickConvert = false;
|
bool addedQuickConvert = false;
|
||||||
if (node.kind == NodeKind::Hex64) {
|
if (node.kind == NodeKind::Hex64) {
|
||||||
menu.addAction("Change to uint64_t", [this, nodeId]() {
|
menu.addAction("Change to uint64_t", [this, nodeId]() {
|
||||||
@@ -1876,35 +1861,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
});
|
});
|
||||||
addedQuickConvert = true;
|
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)
|
if (addedQuickConvert)
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// ── Edit Value / Rename / Change Type ──
|
||||||
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
|
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
|
||||||
&& !isHexNode(node.kind)
|
&& !isHexNode(node.kind)
|
||||||
&& m_doc->provider->isWritable();
|
&& m_doc->provider->isWritable();
|
||||||
@@ -1923,6 +1883,251 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
});
|
});
|
||||||
|
|
||||||
menu.addSeparator();
|
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");
|
auto* act = menu.addAction("Track Value Changes");
|
||||||
act->setCheckable(true);
|
act->setCheckable(true);
|
||||||
@@ -1936,107 +2141,80 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
m_valueHistory.remove(nodeId);
|
m_valueHistory.remove(nodeId);
|
||||||
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
||||||
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
||||||
m_refreshGen++; // discard in-flight async reads
|
m_refreshGen++;
|
||||||
m_prevPages.clear(); // clean baseline for next read cycle
|
m_prevPages.clear();
|
||||||
m_changedOffsets.clear(); // no phantom change indicators
|
m_changedOffsets.clear();
|
||||||
m_valueTrackCooldown = 5; // suppress tracking for ~1s
|
m_valueTrackCooldown = 5;
|
||||||
refresh();
|
refresh();
|
||||||
for (auto* editor : m_editors)
|
for (auto* editor : m_editors)
|
||||||
editor->dismissHistoryPopup();
|
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();
|
menu.addSeparator();
|
||||||
|
} // else (non-member node actions)
|
||||||
|
}
|
||||||
|
|
||||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
// ── Always-available actions ──
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
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)
|
QString trimmed = input.trimmed();
|
||||||
if (node.parentId != 0 && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
|
int byteCount = 0;
|
||||||
uint64_t parentId = node.parentId;
|
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||||
int pi = m_doc->tree.indexOfId(parentId);
|
byteCount = trimmed.mid(2).toInt(&ok, 16);
|
||||||
if (pi >= 0 && (m_doc->tree.nodes[pi].kind == NodeKind::Struct
|
else
|
||||||
|| m_doc->tree.nodes[pi].kind == NodeKind::Array)) {
|
byteCount = trimmed.toInt(&ok, 10);
|
||||||
menu.addAction("Add Static Field", [this, parentId]() {
|
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;
|
Node sf;
|
||||||
sf.id = m_doc->tree.m_nextId++;
|
sf.id = m_doc->tree.m_nextId++;
|
||||||
sf.kind = NodeKind::Hex64;
|
sf.kind = NodeKind::Hex64;
|
||||||
sf.name = QStringLiteral("static_field");
|
sf.name = QStringLiteral("static_field");
|
||||||
sf.parentId = parentId;
|
sf.parentId = rootId;
|
||||||
sf.offset = 0;
|
sf.offset = 0;
|
||||||
sf.isStatic = true;
|
sf.isStatic = true;
|
||||||
sf.offsetExpr = QStringLiteral("base");
|
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();
|
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");
|
auto* act = menu.addAction("Track Value Changes");
|
||||||
act->setCheckable(true);
|
act->setCheckable(true);
|
||||||
act->setChecked(m_trackValues);
|
act->setChecked(m_trackValues);
|
||||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2214,6 +2283,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emit contextMenuAboutToShow(&menu, line);
|
||||||
menu.exec(globalPos);
|
menu.exec(globalPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2374,6 +2444,7 @@ void RcxController::updateCommandRow() {
|
|||||||
.arg(elide(src, 40), elide(addr, 24));
|
.arg(elide(src, 40), elide(addr, 24));
|
||||||
|
|
||||||
// Build row 2: root class type + name (uses current view root)
|
// Build row 2: root class type + name (uses current view root)
|
||||||
|
QString brace = m_braceWrap ? QString() : QStringLiteral(" {");
|
||||||
QString row2;
|
QString row2;
|
||||||
if (m_viewRootId != 0) {
|
if (m_viewRootId != 0) {
|
||||||
int vi = m_doc->tree.indexOfId(m_viewRootId);
|
int vi = m_doc->tree.indexOfId(m_viewRootId);
|
||||||
@@ -2381,8 +2452,8 @@ void RcxController::updateCommandRow() {
|
|||||||
const auto& n = m_doc->tree.nodes[vi];
|
const auto& n = m_doc->tree.nodes[vi];
|
||||||
QString keyword = n.resolvedClassKeyword();
|
QString keyword = n.resolvedClassKeyword();
|
||||||
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||||
row2 = QStringLiteral("%1 %2 {")
|
row2 = QStringLiteral("%1 %2%3")
|
||||||
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
|
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (row2.isEmpty()) {
|
if (row2.isEmpty()) {
|
||||||
@@ -2392,14 +2463,14 @@ void RcxController::updateCommandRow() {
|
|||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||||
QString keyword = n.resolvedClassKeyword();
|
QString keyword = n.resolvedClassKeyword();
|
||||||
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
|
||||||
row2 = QStringLiteral("%1 %2 {")
|
row2 = QStringLiteral("%1 %2%3")
|
||||||
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
|
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (row2.isEmpty())
|
if (row2.isEmpty())
|
||||||
row2 = QStringLiteral("struct NoName {");
|
row2 = QStringLiteral("struct NoName") + brace;
|
||||||
|
|
||||||
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
||||||
|
|
||||||
@@ -3191,6 +3262,11 @@ void RcxController::setTreeLines(bool v) {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::setBraceWrap(bool v) {
|
||||||
|
m_braceWrap = v;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::setupAutoRefresh() {
|
void RcxController::setupAutoRefresh() {
|
||||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
m_refreshTimer = new QTimer(this);
|
m_refreshTimer = new QTimer(this);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
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 save(const QString& path);
|
||||||
bool load(const QString& path);
|
bool load(const QString& path);
|
||||||
void loadData(const QString& binaryPath);
|
void loadData(const QString& binaryPath);
|
||||||
@@ -130,6 +130,7 @@ public:
|
|||||||
void setRefreshInterval(int ms);
|
void setRefreshInterval(int ms);
|
||||||
void setCompactColumns(bool v);
|
void setCompactColumns(bool v);
|
||||||
void setTreeLines(bool v);
|
void setTreeLines(bool v);
|
||||||
|
void setBraceWrap(bool v);
|
||||||
void resetProvider();
|
void resetProvider();
|
||||||
|
|
||||||
// MCP bridge accessors
|
// MCP bridge accessors
|
||||||
@@ -158,6 +159,7 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void nodeSelected(int nodeIdx);
|
void nodeSelected(int nodeIdx);
|
||||||
void selectionChanged(int count);
|
void selectionChanged(int count);
|
||||||
|
void contextMenuAboutToShow(QMenu* menu, int line);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RcxDocument* m_doc;
|
RcxDocument* m_doc;
|
||||||
@@ -168,6 +170,7 @@ private:
|
|||||||
bool m_suppressRefresh = false;
|
bool m_suppressRefresh = false;
|
||||||
bool m_compactColumns = false;
|
bool m_compactColumns = false;
|
||||||
bool m_treeLines = false;
|
bool m_treeLines = false;
|
||||||
|
bool m_braceWrap = false;
|
||||||
uint64_t m_viewRootId = 0;
|
uint64_t m_viewRootId = 0;
|
||||||
|
|
||||||
// ── Saved sources for quick-switch ──
|
// ── Saved sources for quick-switch ──
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ struct Node {
|
|||||||
QJsonObject bm = v.toObject();
|
QJsonObject bm = v.toObject();
|
||||||
BitfieldMember m;
|
BitfieldMember m;
|
||||||
m.name = bm["name"].toString();
|
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);
|
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
|
||||||
n.bitfieldMembers.append(m);
|
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 kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||||
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
||||||
inline constexpr int kSepWidth = 1;
|
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 kMaxTypeW = 128; // Maximum type column width
|
||||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||||
inline constexpr int kMaxNameW = 128; // Maximum name column width
|
inline constexpr int kMaxNameW = 128; // Maximum name column width
|
||||||
@@ -1031,6 +1031,7 @@ namespace fmt {
|
|||||||
// ── Compose function forward declaration ──
|
// ── Compose function forward declaration ──
|
||||||
|
|
||||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
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
|
} // namespace rcx
|
||||||
|
|||||||
65
src/dock_tab_buttons.h
Normal file
65
src/dock_tab_buttons.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
|
||||||
|
// Dock tab button widget (pin + close)
|
||||||
|
// Placed on the right side of each dock tab via QTabBar::setTabButton.
|
||||||
|
class DockTabButtons : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
QToolButton* pinBtn;
|
||||||
|
QToolButton* closeBtn;
|
||||||
|
bool pinned = false;
|
||||||
|
|
||||||
|
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
|
||||||
|
auto* hl = new QHBoxLayout(this);
|
||||||
|
hl->setContentsMargins(0, 0, 0, 0);
|
||||||
|
hl->setSpacing(0);
|
||||||
|
|
||||||
|
pinBtn = new QToolButton(this);
|
||||||
|
pinBtn->setAutoRaise(true);
|
||||||
|
pinBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
pinBtn->setFixedSize(16, 16);
|
||||||
|
pinBtn->setToolTip("Pin tab");
|
||||||
|
updatePinIcon();
|
||||||
|
hl->addWidget(pinBtn);
|
||||||
|
|
||||||
|
closeBtn = new QToolButton(this);
|
||||||
|
closeBtn->setAutoRaise(true);
|
||||||
|
closeBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
closeBtn->setFixedSize(16, 16);
|
||||||
|
closeBtn->setToolTip("Close tab");
|
||||||
|
closeBtn->setIcon(QIcon(":/vsicons/close.svg"));
|
||||||
|
closeBtn->setIconSize(QSize(12, 12));
|
||||||
|
hl->addWidget(closeBtn);
|
||||||
|
|
||||||
|
connect(pinBtn, &QToolButton::clicked, this, [this]() {
|
||||||
|
pinned = !pinned;
|
||||||
|
updatePinIcon();
|
||||||
|
emit pinToggled(pinned);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTheme(const QColor& hover) {
|
||||||
|
QString style = QStringLiteral(
|
||||||
|
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
|
||||||
|
"QToolButton:hover { background: %1; }").arg(hover.name());
|
||||||
|
pinBtn->setStyleSheet(style);
|
||||||
|
closeBtn->setStyleSheet(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPinned(bool p) { pinned = p; updatePinIcon(); emit pinToggled(pinned); }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pinToggled(bool pinned);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updatePinIcon() {
|
||||||
|
pinBtn->setIcon(QIcon(pinned ? ":/vsicons/pinned.svg" : ":/vsicons/pin.svg"));
|
||||||
|
pinBtn->setIconSize(QSize(12, 12));
|
||||||
|
pinBtn->setToolTip(pinned ? "Unpin tab" : "Pin tab");
|
||||||
|
}
|
||||||
|
};
|
||||||
142
src/editor.cpp
142
src/editor.cpp
@@ -23,6 +23,7 @@
|
|||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
@@ -39,18 +40,30 @@ class ValueHistoryPopup : public QFrame {
|
|||||||
QStringList m_values;
|
QStringList m_values;
|
||||||
QVector<QLabel*> m_labels;
|
QVector<QLabel*> m_labels;
|
||||||
std::function<void(const QString&)> m_onSet;
|
std::function<void(const QString&)> m_onSet;
|
||||||
|
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||||
public:
|
public:
|
||||||
explicit ValueHistoryPopup(QWidget* parent)
|
explicit ValueHistoryPopup(QWidget* parent)
|
||||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||||
{
|
{
|
||||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||||
|
setMouseTracking(true);
|
||||||
setFrameShape(QFrame::NoFrame);
|
setFrameShape(QFrame::NoFrame);
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t nodeId() const { return m_nodeId; }
|
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 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,
|
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
|
||||||
bool showButtons = false) {
|
bool showButtons = false) {
|
||||||
@@ -184,12 +197,14 @@ class DisasmPopup : public QFrame {
|
|||||||
QString m_body;
|
QString m_body;
|
||||||
QLabel* m_titleLabel = nullptr;
|
QLabel* m_titleLabel = nullptr;
|
||||||
QLabel* m_bodyLabel = nullptr;
|
QLabel* m_bodyLabel = nullptr;
|
||||||
|
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||||
public:
|
public:
|
||||||
explicit DisasmPopup(QWidget* parent)
|
explicit DisasmPopup(QWidget* parent)
|
||||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||||
{
|
{
|
||||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||||
|
setMouseTracking(true);
|
||||||
setFrameShape(QFrame::NoFrame);
|
setFrameShape(QFrame::NoFrame);
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
|
|
||||||
@@ -215,8 +230,14 @@ public:
|
|||||||
vbox->addWidget(m_bodyLabel);
|
vbox->addWidget(m_bodyLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
||||||
uint64_t nodeId() const { return m_nodeId; }
|
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,
|
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||||
const QFont& font) {
|
const QFont& font) {
|
||||||
if (nodeId == m_nodeId && body == m_body && isVisible())
|
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||||
@@ -282,12 +303,14 @@ class StructPreviewPopup : public QFrame {
|
|||||||
QString m_body;
|
QString m_body;
|
||||||
QLabel* m_titleLabel = nullptr;
|
QLabel* m_titleLabel = nullptr;
|
||||||
QLabel* m_bodyLabel = nullptr;
|
QLabel* m_bodyLabel = nullptr;
|
||||||
|
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||||
public:
|
public:
|
||||||
explicit StructPreviewPopup(QWidget* parent)
|
explicit StructPreviewPopup(QWidget* parent)
|
||||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||||
{
|
{
|
||||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||||
|
setMouseTracking(true);
|
||||||
setFrameShape(QFrame::NoFrame);
|
setFrameShape(QFrame::NoFrame);
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
|
|
||||||
@@ -314,7 +337,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint64_t nodeId() const { return m_nodeId; }
|
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,
|
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||||
const QFont& font) {
|
const QFont& font) {
|
||||||
if (nodeId == m_nodeId && body == m_body && isVisible())
|
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||||
@@ -938,9 +967,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
int maxLen = 0;
|
int maxLen = 0;
|
||||||
const QStringList lines = result.text.split(QChar('\n'));
|
const QStringList lines = result.text.split(QChar('\n'));
|
||||||
for (const auto& line : lines) {
|
for (const auto& line : lines) {
|
||||||
int len = line.size();
|
int len = (int)line.size();
|
||||||
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
||||||
if (len > maxLen) maxLen = len;
|
maxLen = std::max(len, maxLen);
|
||||||
}
|
}
|
||||||
QFontMetrics fm(editorFont());
|
QFontMetrics fm(editorFont());
|
||||||
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
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
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
|
|
||||||
// Restore hover state
|
// Restore hover state — but clear if the node was deleted
|
||||||
m_hoveredNodeId = savedHoverId;
|
m_hoveredNodeId = savedHoverId;
|
||||||
m_hoveredLine = savedHoverLine;
|
m_hoveredLine = savedHoverLine;
|
||||||
m_hoverInside = savedHoverInside;
|
m_hoverInside = savedHoverInside;
|
||||||
m_applyingDocument = false;
|
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).
|
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||||
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
|
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
|
||||||
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
// 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;
|
m_editState.commentCol = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable Scintilla undo during inline edit
|
// Keep undo collection enabled during inline edit so CellBuffer::DeleteChars
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
|
// 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->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
|
||||||
m_sci->setReadOnly(false);
|
m_sci->setReadOnly(false);
|
||||||
|
|
||||||
@@ -2991,8 +3030,26 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||||
auto it = m_valueHistory->find(lm.nodeId);
|
auto it = m_valueHistory->find(lm.nodeId);
|
||||||
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||||
if (!m_historyPopup)
|
if (!m_historyPopup) {
|
||||||
m_historyPopup = new ValueHistoryPopup(this);
|
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);
|
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
||||||
popup->setOnSet([this](const QString& val) {
|
popup->setOnSet([this](const QString& val) {
|
||||||
if (!m_editState.active) return;
|
if (!m_editState.active) return;
|
||||||
@@ -3152,8 +3209,26 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
QString lineText = getLineText(m_sci, h.line);
|
QString lineText = getLineText(m_sci, h.line);
|
||||||
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||||
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
||||||
if (!m_historyPopup)
|
if (!m_historyPopup) {
|
||||||
m_historyPopup = new ValueHistoryPopup(this);
|
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);
|
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
||||||
popup->populate(lm.nodeId, *it, editorFont(), false);
|
popup->populate(lm.nodeId, *it, editorFont(), false);
|
||||||
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||||
@@ -3237,8 +3312,26 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!body.isEmpty()) {
|
if (!body.isEmpty()) {
|
||||||
if (!m_disasmPopup)
|
if (!m_disasmPopup) {
|
||||||
m_disasmPopup = new DisasmPopup(this);
|
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*>(
|
auto* popup = static_cast<DisasmPopup*>(
|
||||||
m_disasmPopup);
|
m_disasmPopup);
|
||||||
popup->populate(lm.nodeId, title, body,
|
popup->populate(lm.nodeId, title, body,
|
||||||
@@ -3306,8 +3399,26 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!body.isEmpty()) {
|
if (!body.isEmpty()) {
|
||||||
if (!m_structPreviewPopup)
|
if (!m_structPreviewPopup) {
|
||||||
m_structPreviewPopup = new StructPreviewPopup(this);
|
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);
|
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
|
||||||
popup->populate(lm.nodeId,
|
popup->populate(lm.nodeId,
|
||||||
lm.pointerTargetName, body, editorFont());
|
lm.pointerTargetName, body, editorFont());
|
||||||
@@ -3452,14 +3563,8 @@ void RcxEditor::setCommandRowText(const QString& line) {
|
|||||||
long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
||||||
long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR);
|
long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR);
|
||||||
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
|
|
||||||
m_sci->setReadOnly(false);
|
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 start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0);
|
||||||
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
|
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0);
|
||||||
QByteArray utf8 = s.toUtf8();
|
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_SETTARGETEND, end);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
|
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
|
// Adjust saved cursor/anchor for length change in line 0
|
||||||
long delta = (long)utf8.size() - oldLen;
|
long delta = (long)utf8.size() - oldLen;
|
||||||
if (savedPos > end) savedPos += delta;
|
if (savedPos > end) savedPos += delta;
|
||||||
if (savedAnchor > end) savedAnchor += delta;
|
if (savedAnchor > end) savedAnchor += delta;
|
||||||
|
|
||||||
if (wasReadOnly) m_sci->setReadOnly(true);
|
if (wasReadOnly) m_sci->setReadOnly(true);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1);
|
|
||||||
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
|
if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor);
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public:
|
|||||||
void restoreViewState(const ViewState& vs);
|
void restoreViewState(const ViewState& vs);
|
||||||
|
|
||||||
QsciScintilla* scintilla() const { return m_sci; }
|
QsciScintilla* scintilla() const { return m_sci; }
|
||||||
|
QWidget* historyPopup() const { return m_historyPopup; }
|
||||||
|
QWidget* disasmPopup() const { return m_disasmPopup; }
|
||||||
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
||||||
const LineMeta* metaForLine(int line) const;
|
const LineMeta* metaForLine(int line) const;
|
||||||
int currentNodeIndex() const;
|
int currentNodeIndex() const;
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
{
|
|
||||||
"baseAddress": "0",
|
|
||||||
"nextId": "20",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"kind": "Struct",
|
|
||||||
"name": "player",
|
|
||||||
"structTypeName": "PlayerEntity",
|
|
||||||
"classKeyword": "class",
|
|
||||||
"parentId": "0",
|
|
||||||
"offset": 0,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"kind": "Pointer64",
|
|
||||||
"name": "__vptr",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 0,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"kind": "Int32",
|
|
||||||
"name": "health",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 8,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"kind": "Int32",
|
|
||||||
"name": "armor",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 12,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5",
|
|
||||||
"kind": "Float",
|
|
||||||
"name": "pos_x",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 16,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "6",
|
|
||||||
"kind": "Float",
|
|
||||||
"name": "pos_y",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 20,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7",
|
|
||||||
"kind": "Float",
|
|
||||||
"name": "pos_z",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 24,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "8",
|
|
||||||
"kind": "Hex32",
|
|
||||||
"name": "pad_1C",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 28,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "9",
|
|
||||||
"kind": "Pointer64",
|
|
||||||
"name": "name",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 32,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64,
|
|
||||||
"ptrDepth": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "10",
|
|
||||||
"kind": "UInt64",
|
|
||||||
"name": "flags",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 40,
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "11",
|
|
||||||
"kind": "Hex64",
|
|
||||||
"name": "static_field",
|
|
||||||
"parentId": "1",
|
|
||||||
"offset": 0,
|
|
||||||
"isStatic": true,
|
|
||||||
"offsetExpr": "base + pos_x",
|
|
||||||
"collapsed": false,
|
|
||||||
"refId": "0",
|
|
||||||
"elementKind": "UInt8",
|
|
||||||
"arrayLen": 1,
|
|
||||||
"strLen": 64
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
245257
src/examples/WinSDK.rcx
Normal file
245257
src/examples/WinSDK.rcx
Normal file
File diff suppressed because it is too large
Load Diff
42817
src/examples/windows-x86_64.h
Normal file
42817
src/examples/windows-x86_64.h
Normal file
File diff suppressed because it is too large
Load Diff
1312
src/main.cpp
1312
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,8 @@
|
|||||||
#include "titlebar.h"
|
#include "titlebar.h"
|
||||||
#include "pluginmanager.h"
|
#include "pluginmanager.h"
|
||||||
#include "scannerpanel.h"
|
#include "scannerpanel.h"
|
||||||
|
#include "startpage.h"
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMdiArea>
|
|
||||||
#include <QMdiSubWindow>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
@@ -24,6 +23,7 @@ namespace rcx {
|
|||||||
|
|
||||||
class McpBridge;
|
class McpBridge;
|
||||||
class ShimmerLabel;
|
class ShimmerLabel;
|
||||||
|
class DockGripWidget;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -71,22 +71,19 @@ public:
|
|||||||
void clearMcpStatus();
|
void clearMcpStatus();
|
||||||
|
|
||||||
// Project Lifecycle API
|
// Project Lifecycle API
|
||||||
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
QDockWidget* project_new(const QString& classKeyword = QString());
|
||||||
QMdiSubWindow* project_open(const QString& path = {});
|
QDockWidget* project_open(const QString& path = {});
|
||||||
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
|
bool project_save(QDockWidget* dock = nullptr, bool saveAs = false);
|
||||||
void project_close(QMdiSubWindow* sub = nullptr);
|
void project_close(QDockWidget* dock = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||||
|
|
||||||
QMdiArea* m_mdiArea;
|
QWidget* m_centralPlaceholder;
|
||||||
ShimmerLabel* m_statusLabel;
|
ShimmerLabel* m_statusLabel;
|
||||||
QString m_appStatus;
|
QString m_appStatus;
|
||||||
bool m_mcpBusy = false;
|
bool m_mcpBusy = false;
|
||||||
QTimer* m_mcpClearTimer = nullptr;
|
QTimer* m_mcpClearTimer = nullptr;
|
||||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
|
||||||
QPushButton* m_btnReclass = nullptr;
|
|
||||||
QPushButton* m_btnRendered = nullptr;
|
|
||||||
TitleBarWidget* m_titleBar = nullptr;
|
TitleBarWidget* m_titleBar = nullptr;
|
||||||
QMenuBar* m_menuBar = nullptr;
|
QMenuBar* m_menuBar = nullptr;
|
||||||
bool m_menuBarTitleCase = false;
|
bool m_menuBarTitleCase = false;
|
||||||
@@ -94,7 +91,6 @@ private:
|
|||||||
PluginManager m_pluginManager;
|
PluginManager m_pluginManager;
|
||||||
McpBridge* m_mcp = nullptr;
|
McpBridge* m_mcp = nullptr;
|
||||||
QAction* m_mcpAction = nullptr;
|
QAction* m_mcpAction = nullptr;
|
||||||
QAction* m_removeSplitAction = nullptr;
|
|
||||||
QMenu* m_sourceMenu = nullptr;
|
QMenu* m_sourceMenu = nullptr;
|
||||||
QMenu* m_recentFilesMenu = nullptr;
|
QMenu* m_recentFilesMenu = nullptr;
|
||||||
|
|
||||||
@@ -116,7 +112,9 @@ private:
|
|||||||
QVector<SplitPane> panes;
|
QVector<SplitPane> panes;
|
||||||
int activePaneIdx = 0;
|
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
|
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||||
void rebuildAllDocs();
|
void rebuildAllDocs();
|
||||||
|
|
||||||
@@ -133,8 +131,10 @@ private:
|
|||||||
TabState* activeTab();
|
TabState* activeTab();
|
||||||
TabState* tabByIndex(int index);
|
TabState* tabByIndex(int index);
|
||||||
int tabCount() const { return m_tabs.size(); }
|
int tabCount() const { return m_tabs.size(); }
|
||||||
QMdiSubWindow* createTab(RcxDocument* doc);
|
QDockWidget* createTab(RcxDocument* doc);
|
||||||
|
void setupDockTabBars();
|
||||||
void updateWindowTitle();
|
void updateWindowTitle();
|
||||||
|
void closeAllDocDocks();
|
||||||
|
|
||||||
void setViewMode(ViewMode mode);
|
void setViewMode(ViewMode mode);
|
||||||
void updateRenderedView(TabState& tab, SplitPane& pane);
|
void updateRenderedView(TabState& tab, SplitPane& pane);
|
||||||
@@ -144,7 +144,6 @@ private:
|
|||||||
|
|
||||||
SplitPane createSplitPane(TabState& tab);
|
SplitPane createSplitPane(TabState& tab);
|
||||||
void applyTheme(const Theme& theme);
|
void applyTheme(const Theme& theme);
|
||||||
void styleTabCloseButtons();
|
|
||||||
void syncViewButtons(ViewMode mode);
|
void syncViewButtons(ViewMode mode);
|
||||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||||
SplitPane* findActiveSplitPane();
|
SplitPane* findActiveSplitPane();
|
||||||
@@ -158,6 +157,7 @@ private:
|
|||||||
QLineEdit* m_workspaceSearch = nullptr;
|
QLineEdit* m_workspaceSearch = nullptr;
|
||||||
QLabel* m_dockTitleLabel = nullptr;
|
QLabel* m_dockTitleLabel = nullptr;
|
||||||
QToolButton* m_dockCloseBtn = nullptr;
|
QToolButton* m_dockCloseBtn = nullptr;
|
||||||
|
DockGripWidget* m_dockGrip = nullptr;
|
||||||
void createWorkspaceDock();
|
void createWorkspaceDock();
|
||||||
void rebuildWorkspaceModel();
|
void rebuildWorkspaceModel();
|
||||||
void updateBorderColor(const QColor& color);
|
void updateBorderColor(const QColor& color);
|
||||||
@@ -167,8 +167,14 @@ private:
|
|||||||
ScannerPanel* m_scannerPanel = nullptr;
|
ScannerPanel* m_scannerPanel = nullptr;
|
||||||
QLabel* m_scanDockTitle = nullptr;
|
QLabel* m_scanDockTitle = nullptr;
|
||||||
QToolButton* m_scanDockCloseBtn = nullptr;
|
QToolButton* m_scanDockCloseBtn = nullptr;
|
||||||
|
DockGripWidget* m_scanDockGrip = nullptr;
|
||||||
void createScannerDock();
|
void createScannerDock();
|
||||||
|
|
||||||
|
// Start page
|
||||||
|
StartPageWidget* m_startPage = nullptr;
|
||||||
|
Q_INVOKABLE void showStartPage();
|
||||||
|
void dismissStartPage();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void changeEvent(QEvent* event) override;
|
void changeEvent(QEvent* event) override;
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|||||||
@@ -40,9 +40,21 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
m_tree->setHeaderHidden(true);
|
m_tree->setHeaderHidden(true);
|
||||||
m_tree->setRootIsDecorated(true);
|
m_tree->setRootIsDecorated(true);
|
||||||
m_tree->setFixedWidth(200);
|
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"});
|
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
|
||||||
|
envItem->setIcon(0, QIcon(":/vsicons/folder.svg"));
|
||||||
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
|
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
|
||||||
|
generalItem->setIcon(0, QIcon(":/vsicons/settings-gear.svg"));
|
||||||
m_tree->expandAll();
|
m_tree->expandAll();
|
||||||
m_tree->setCurrentItem(generalItem);
|
m_tree->setCurrentItem(generalItem);
|
||||||
leftColumn->addWidget(m_tree, 1);
|
leftColumn->addWidget(m_tree, 1);
|
||||||
@@ -102,7 +114,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
m_fontCombo->setObjectName("fontCombo");
|
m_fontCombo->setObjectName("fontCombo");
|
||||||
visualLayout->addRow("Editor Font:", m_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);
|
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
||||||
visualLayout->addRow(m_titleCaseCheck);
|
visualLayout->addRow(m_titleCaseCheck);
|
||||||
|
|
||||||
@@ -110,25 +122,11 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
m_showIconCheck->setChecked(current.showIcon);
|
m_showIconCheck->setChecked(current.showIcon);
|
||||||
visualLayout->addRow(m_showIconCheck);
|
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);
|
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();
|
generalLayout->addStretch();
|
||||||
|
|
||||||
m_pages->addWidget(generalPage); // index 0
|
m_pages->addWidget(generalPage); // index 0
|
||||||
@@ -136,6 +134,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
|
|
||||||
// -- AI Features page --
|
// -- AI Features page --
|
||||||
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
|
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
|
||||||
|
aiItem->setIcon(0, QIcon(":/vsicons/remote.svg"));
|
||||||
|
|
||||||
auto* aiPage = new QWidget;
|
auto* aiPage = new QWidget;
|
||||||
auto* aiLayout = new QVBoxLayout(aiPage);
|
auto* aiLayout = new QVBoxLayout(aiPage);
|
||||||
@@ -165,6 +164,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
|
|
||||||
// -- Generator page --
|
// -- Generator page --
|
||||||
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
|
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
|
||||||
|
generatorItem->setIcon(0, QIcon(":/vsicons/code.svg"));
|
||||||
|
|
||||||
auto* generatorPage = new QWidget;
|
auto* generatorPage = new QWidget;
|
||||||
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||||
@@ -213,10 +213,10 @@ OptionsResult OptionsDialog::result() const {
|
|||||||
r.fontName = m_fontCombo->currentText();
|
r.fontName = m_fontCombo->currentText();
|
||||||
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
||||||
r.showIcon = m_showIconCheck->isChecked();
|
r.showIcon = m_showIconCheck->isChecked();
|
||||||
r.safeMode = m_safeModeCheck->isChecked();
|
|
||||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||||
r.refreshMs = m_refreshSpin->value();
|
r.refreshMs = m_refreshSpin->value();
|
||||||
r.generatorAsserts = m_assertCheck->isChecked();
|
r.generatorAsserts = m_assertCheck->isChecked();
|
||||||
|
r.braceWrap = m_braceWrapCheck->isChecked();
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ struct OptionsResult {
|
|||||||
QString fontName;
|
QString fontName;
|
||||||
bool menuBarTitleCase = true;
|
bool menuBarTitleCase = true;
|
||||||
bool showIcon = false;
|
bool showIcon = false;
|
||||||
bool safeMode = false;
|
|
||||||
bool autoStartMcp = true;
|
bool autoStartMcp = true;
|
||||||
int refreshMs = 660;
|
int refreshMs = 660;
|
||||||
bool generatorAsserts = false;
|
bool generatorAsserts = false;
|
||||||
|
bool braceWrap = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OptionsDialog : public QDialog {
|
class OptionsDialog : public QDialog {
|
||||||
@@ -39,10 +39,10 @@ private:
|
|||||||
QComboBox* m_fontCombo = nullptr;
|
QComboBox* m_fontCombo = nullptr;
|
||||||
QCheckBox* m_titleCaseCheck = nullptr;
|
QCheckBox* m_titleCaseCheck = nullptr;
|
||||||
QCheckBox* m_showIconCheck = nullptr;
|
QCheckBox* m_showIconCheck = nullptr;
|
||||||
QCheckBox* m_safeModeCheck = nullptr;
|
|
||||||
QCheckBox* m_autoMcpCheck = nullptr;
|
QCheckBox* m_autoMcpCheck = nullptr;
|
||||||
QSpinBox* m_refreshSpin = nullptr;
|
QSpinBox* m_refreshSpin = nullptr;
|
||||||
QCheckBox* m_assertCheck = nullptr;
|
QCheckBox* m_assertCheck = nullptr;
|
||||||
|
QCheckBox* m_braceWrapCheck = nullptr;
|
||||||
|
|
||||||
// searchable keywords per leaf tree item
|
// searchable keywords per leaf tree item
|
||||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ void ProcessPicker::initUi()
|
|||||||
ui->processTable->setColumnWidth(0, 80); // PID column
|
ui->processTable->setColumnWidth(0, 80); // PID column
|
||||||
ui->processTable->setColumnWidth(1, 200); // Name column
|
ui->processTable->setColumnWidth(1, 200); // Name column
|
||||||
ui->processTable->horizontalHeader()->setStretchLastSection(true);
|
ui->processTable->horizontalHeader()->setStretchLastSection(true);
|
||||||
|
ui->processTable->setSortingEnabled(true);
|
||||||
ui->processTable->setWordWrap(false);
|
ui->processTable->setWordWrap(false);
|
||||||
ui->processTable->setTextElideMode(Qt::ElideLeft);
|
ui->processTable->setTextElideMode(Qt::ElideLeft);
|
||||||
ui->processTable->setShowGrid(false);
|
ui->processTable->setShowGrid(false);
|
||||||
@@ -329,6 +330,9 @@ void ProcessPicker::populateTable(const QList<ProcessInfo>& processes)
|
|||||||
pathItem->setToolTip(proc.path); // Show full path on hover
|
pathItem->setToolTip(proc.path); // Show full path on hover
|
||||||
ui->processTable->setItem(i, 2, pathItem);
|
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)
|
void ProcessPicker::filterProcesses(const QString& text)
|
||||||
|
|||||||
@@ -60,5 +60,10 @@
|
|||||||
<file alias="search.svg">vsicons/search.svg</file>
|
<file alias="search.svg">vsicons/search.svg</file>
|
||||||
<file alias="regex.svg">vsicons/regex.svg</file>
|
<file alias="regex.svg">vsicons/regex.svg</file>
|
||||||
<file alias="refresh.svg">vsicons/refresh.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>
|
||||||
|
<file alias="book.svg">vsicons/book.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
|||||||
regEnd = qMin(regEnd, req.endAddress);
|
regEnd = qMin(regEnd, req.endAddress);
|
||||||
}
|
}
|
||||||
uint64_t regSize = regEnd - regStart;
|
uint64_t regSize = regEnd - regStart;
|
||||||
|
if (regSize == 0) continue;
|
||||||
|
|
||||||
if ((uint64_t)patternLen > regSize) {
|
if ((uint64_t)patternLen > regSize) {
|
||||||
scannedBytes += regSize;
|
scannedBytes += regSize;
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
|||||||
QStringLiteral("Copy Address"), this);
|
QStringLiteral("Copy Address"), this);
|
||||||
m_copyBtn->setEnabled(false);
|
m_copyBtn->setEnabled(false);
|
||||||
actionRow->addWidget(m_copyBtn);
|
actionRow->addWidget(m_copyBtn);
|
||||||
|
actionRow->addSpacing(20); // room for resize grip when floating
|
||||||
|
|
||||||
mainLayout->addLayout(actionRow);
|
mainLayout->addLayout(actionRow);
|
||||||
|
|
||||||
|
|||||||
360
src/startpage.h
Normal file
360
src/startpage.h
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "themes/thememanager.h"
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QPainterPath>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// Single-widget start page: everything painted in paintEvent.
|
||||||
|
// Zero CSS, zero Fusion conflicts, zero child-widget styling issues.
|
||||||
|
|
||||||
|
class StartPageWidget : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit StartPageWidget(QWidget* parent = nullptr) : QDialog(parent) {
|
||||||
|
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
|
||||||
|
setMouseTracking(true);
|
||||||
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||||
|
|
||||||
|
m_search = new QLineEdit(this);
|
||||||
|
m_search->setPlaceholderText("Search recent...");
|
||||||
|
m_search->setFixedHeight(30);
|
||||||
|
m_search->setMaximumWidth(330);
|
||||||
|
m_search->addAction(QIcon(":/vsicons/search.svg"), QLineEdit::TrailingPosition);
|
||||||
|
connect(m_search, &QLineEdit::textChanged, this, [this]{ buildGroups(); update(); });
|
||||||
|
|
||||||
|
loadEntries();
|
||||||
|
buildGroups();
|
||||||
|
applyTheme(ThemeManager::instance().current());
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTheme(const Theme& t) {
|
||||||
|
m_t = t;
|
||||||
|
m_search->setStyleSheet(
|
||||||
|
"QLineEdit { background: " + t.background.name() + "; color: " + t.text.name()
|
||||||
|
+ "; border: 1px solid " + t.border.name()
|
||||||
|
+ "; padding: 2px 8px; font-size: 13px; }"
|
||||||
|
"QLineEdit:focus { border: 1px solid " + t.borderFocused.name() + "; }");
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void openProject();
|
||||||
|
void newClass();
|
||||||
|
void importSource();
|
||||||
|
void importXml();
|
||||||
|
void importPdb();
|
||||||
|
void continueClicked();
|
||||||
|
void fileSelected(const QString& path);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent*) override {
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
|
const int LX = 48, TM = 36, RM = 32, GAP = 40, RW = 340;
|
||||||
|
const int rpX = width() - RW - RM;
|
||||||
|
const int lW = qMax(100, rpX - GAP - LX);
|
||||||
|
|
||||||
|
p.fillRect(rect(), m_t.background);
|
||||||
|
|
||||||
|
// ── Title ──
|
||||||
|
int y = TM;
|
||||||
|
QFont titleF = font(); titleF.setPixelSize(30); titleF.setWeight(QFont::Light);
|
||||||
|
p.setFont(titleF); p.setPen(m_t.text);
|
||||||
|
QFontMetrics titleFm(titleF);
|
||||||
|
p.drawText(LX, y + titleFm.ascent(), "Reclass");
|
||||||
|
y += titleFm.height() + 24;
|
||||||
|
|
||||||
|
// ── Headings (left + right at same y) ──
|
||||||
|
QFont headF = font(); headF.setPixelSize(20); headF.setWeight(QFont::DemiBold);
|
||||||
|
p.setFont(headF); QFontMetrics headFm(headF);
|
||||||
|
p.drawText(LX, y + headFm.ascent(), "Open recent");
|
||||||
|
int ry = y;
|
||||||
|
p.drawText(rpX, ry + headFm.ascent(), "Get started");
|
||||||
|
ry += headFm.height() + 14;
|
||||||
|
y += headFm.height() + 14;
|
||||||
|
|
||||||
|
// ── Search bar (only child widget) ──
|
||||||
|
m_search->setGeometry(LX, y, qMin(330, lW), 30);
|
||||||
|
y += 46;
|
||||||
|
m_listTop = y;
|
||||||
|
|
||||||
|
// ── Right panel ──
|
||||||
|
drawCards(p, rpX, ry, RW);
|
||||||
|
|
||||||
|
// ── File list ──
|
||||||
|
drawFileList(p, LX, lW);
|
||||||
|
|
||||||
|
// ── Border ──
|
||||||
|
p.setPen(QPen(m_t.border, 1));
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
p.drawRect(rect().adjusted(0, 0, -1, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseMoveEvent(QMouseEvent* e) override {
|
||||||
|
auto [z, i] = hitTest(e->pos());
|
||||||
|
if (z != m_hz || i != m_hi) {
|
||||||
|
m_hz = z; m_hi = i;
|
||||||
|
setCursor(z != HZ_None ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mousePressEvent(QMouseEvent* e) override {
|
||||||
|
if (e->button() != Qt::LeftButton) return;
|
||||||
|
auto [z, i] = hitTest(e->pos());
|
||||||
|
if (z == HZ_Entry) emit fileSelected(m_filtered[i].path);
|
||||||
|
if (z == HZ_Group) { m_groups[i].expanded = !m_groups[i].expanded; update(); }
|
||||||
|
if (z == HZ_Card && i == 0) emit newClass();
|
||||||
|
if (z == HZ_Card && i == 1) emit openProject();
|
||||||
|
if (z == HZ_Card && i == 2) emit importSource();
|
||||||
|
if (z == HZ_Card && i == 3) emit importXml();
|
||||||
|
if (z == HZ_Card && i == 4) emit importPdb();
|
||||||
|
if (z == HZ_Continue) emit continueClicked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void wheelEvent(QWheelEvent* e) override {
|
||||||
|
m_scrollY = qBound(0, m_scrollY - e->angleDelta().y() / 2, m_maxScroll);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resizeEvent(QResizeEvent* e) override { QWidget::resizeEvent(e); update(); }
|
||||||
|
void leaveEvent(QEvent*) override { m_hz = HZ_None; m_hi = -1; setCursor(Qt::ArrowCursor); update(); }
|
||||||
|
void keyPressEvent(QKeyEvent* e) override { if (e->key() == Qt::Key_Escape) reject(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum HZ { HZ_None, HZ_Entry, HZ_Group, HZ_Card, HZ_Continue };
|
||||||
|
struct Hit { HZ zone; int idx; };
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
QString path, fileName, dirPath;
|
||||||
|
QDateTime lastModified;
|
||||||
|
bool isExample;
|
||||||
|
};
|
||||||
|
struct Group {
|
||||||
|
QString name;
|
||||||
|
bool expanded = true;
|
||||||
|
QVector<int> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
Theme m_t;
|
||||||
|
QLineEdit* m_search;
|
||||||
|
QVector<Entry> m_all, m_filtered;
|
||||||
|
QVector<Group> m_groups;
|
||||||
|
int m_scrollY = 0, m_maxScroll = 0, m_listTop = 0, m_contentH = 0;
|
||||||
|
|
||||||
|
HZ m_hz = HZ_None;
|
||||||
|
int m_hi = -1;
|
||||||
|
|
||||||
|
// Hit rects populated during paint
|
||||||
|
QVector<QPair<int, QRectF>> m_grpRects, m_entRects;
|
||||||
|
QRectF m_cardR[5], m_contR;
|
||||||
|
|
||||||
|
void drawIcon(QPainter& p, const QString& path, int x, int y, int sz) {
|
||||||
|
QIcon(path).paint(&p, x, y, sz, sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Data loading ──
|
||||||
|
|
||||||
|
void loadEntries() {
|
||||||
|
m_all.clear();
|
||||||
|
QSettings s("Reclass", "Reclass");
|
||||||
|
for (const auto& path : s.value("recentFiles").toStringList()) {
|
||||||
|
QFileInfo fi(path);
|
||||||
|
if (!fi.exists()) continue;
|
||||||
|
m_all.append({fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
|
||||||
|
fi.lastModified(), false});
|
||||||
|
}
|
||||||
|
#ifdef __APPLE__
|
||||||
|
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
||||||
|
#else
|
||||||
|
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||||
|
#endif
|
||||||
|
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
|
||||||
|
m_all.append({exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
|
||||||
|
QFileInfo(exDir.filePath(fn)).lastModified(), true});
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildGroups() {
|
||||||
|
QString f = m_search->text().trimmed().toLower();
|
||||||
|
m_filtered.clear();
|
||||||
|
for (const auto& e : m_all)
|
||||||
|
if (f.isEmpty() || e.fileName.toLower().contains(f) || e.dirPath.toLower().contains(f))
|
||||||
|
m_filtered.append(e);
|
||||||
|
|
||||||
|
QDate today = QDate::currentDate();
|
||||||
|
QVector<int> bk[6];
|
||||||
|
for (int i = 0; i < m_filtered.size(); i++) {
|
||||||
|
auto& e = m_filtered[i];
|
||||||
|
if (e.isExample) { bk[5].append(i); continue; }
|
||||||
|
int d = e.lastModified.date().daysTo(today);
|
||||||
|
if (d == 0) bk[0].append(i);
|
||||||
|
else if (d == 1) bk[1].append(i);
|
||||||
|
else if (d < 7) bk[2].append(i);
|
||||||
|
else if (e.lastModified.date().month() == today.month()
|
||||||
|
&& e.lastModified.date().year() == today.year()) bk[3].append(i);
|
||||||
|
else bk[4].append(i);
|
||||||
|
}
|
||||||
|
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
|
||||||
|
m_groups.clear();
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
if (!bk[i].isEmpty()) m_groups.append({names[i], true, bk[i]});
|
||||||
|
m_scrollY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drawing ──
|
||||||
|
|
||||||
|
void drawCards(QPainter& p, int x, int y, int w) {
|
||||||
|
struct C { const char* icon; const char* title; const char* desc; };
|
||||||
|
static const C cards[] = {
|
||||||
|
{":/vsicons/symbol-structure.svg", "New Class", "Start a new binary class definition"},
|
||||||
|
{":/vsicons/folder-opened.svg", "Open project", "Open an existing .rcx project"},
|
||||||
|
{":/vsicons/file-binary.svg", "Import from Source", "Import C/C++ header or source file"},
|
||||||
|
{":/vsicons/code.svg", "Import ReClass XML", "Import from ReClass .xml format"},
|
||||||
|
{":/vsicons/debug.svg", "Import PDB", "Import types from a .pdb symbol file"}
|
||||||
|
};
|
||||||
|
|
||||||
|
const int N = 5, CH = 84, R = 6, panelH = N * CH;
|
||||||
|
|
||||||
|
// Rounded panel background
|
||||||
|
QPainterPath clip;
|
||||||
|
clip.addRoundedRect(QRectF(x, y, w, panelH), R, R);
|
||||||
|
p.save();
|
||||||
|
p.setClipPath(clip);
|
||||||
|
p.fillRect(x, y, w, panelH, m_t.background);
|
||||||
|
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
int cy = y + i * CH;
|
||||||
|
QRectF cr(x, cy, w, CH);
|
||||||
|
m_cardR[i] = cr;
|
||||||
|
bool hov = (m_hz == HZ_Card && m_hi == i);
|
||||||
|
|
||||||
|
if (hov) {
|
||||||
|
p.fillRect(cr, m_t.hover);
|
||||||
|
p.fillRect(QRectF(x, cy, 3, CH), m_t.indHoverSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon (32px, centered vertically)
|
||||||
|
int iconSz = 32;
|
||||||
|
drawIcon(p, cards[i].icon, x + 24, cy + (CH - iconSz) / 2, iconSz);
|
||||||
|
|
||||||
|
// Title + description block, centered vertically
|
||||||
|
int tx = x + 24 + iconSz + 16;
|
||||||
|
QFont tf = font(); tf.setPixelSize(15);
|
||||||
|
QFont df = font(); df.setPixelSize(12);
|
||||||
|
QFontMetrics tfm(tf), dfm(df);
|
||||||
|
int blockH = tfm.height() + 5 + dfm.height();
|
||||||
|
int by = cy + (CH - blockH) / 2;
|
||||||
|
|
||||||
|
p.setFont(tf); p.setPen(m_t.text);
|
||||||
|
p.drawText(tx, by + tfm.ascent(), cards[i].title);
|
||||||
|
p.setFont(df); p.setPen(m_t.textDim);
|
||||||
|
p.drawText(tx, by + tfm.height() + 5 + dfm.ascent(), cards[i].desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
// "Continue →" centered under the panel
|
||||||
|
int cy = y + panelH + 8;
|
||||||
|
QFont lf = font(); lf.setPixelSize(13);
|
||||||
|
if (m_hz == HZ_Continue) lf.setUnderline(true);
|
||||||
|
p.setFont(lf); p.setPen(m_t.indHoverSpan);
|
||||||
|
QFontMetrics lfm(lf);
|
||||||
|
QString ct = QStringLiteral("Continue \u2192");
|
||||||
|
int cw = lfm.horizontalAdvance(ct);
|
||||||
|
m_contR = QRectF(x + (w - cw) / 2, cy, cw, lfm.height());
|
||||||
|
p.drawText(int(m_contR.x()), cy + lfm.ascent(), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawFileList(QPainter& p, int x, int w) {
|
||||||
|
int listH = height() - 24 - m_listTop;
|
||||||
|
p.save();
|
||||||
|
p.setClipRect(x, m_listTop, w, listH);
|
||||||
|
|
||||||
|
int fy = m_listTop - m_scrollY;
|
||||||
|
m_grpRects.clear();
|
||||||
|
m_entRects.clear();
|
||||||
|
|
||||||
|
for (int gi = 0; gi < m_groups.size(); gi++) {
|
||||||
|
auto& g = m_groups[gi];
|
||||||
|
if (gi > 0) fy += 15;
|
||||||
|
|
||||||
|
// Group header
|
||||||
|
m_grpRects.append({gi, QRectF(x, fy, w, 28)});
|
||||||
|
p.setPen(Qt::NoPen); p.setBrush(m_t.text);
|
||||||
|
int triX = x + 8, triY = fy + 11;
|
||||||
|
QPolygonF tri;
|
||||||
|
if (g.expanded) tri << QPointF(triX,triY) << QPointF(triX+6,triY) << QPointF(triX+3,triY+6);
|
||||||
|
else tri << QPointF(triX,triY) << QPointF(triX+6,triY+3) << QPointF(triX,triY+6);
|
||||||
|
p.drawPolygon(tri);
|
||||||
|
|
||||||
|
QFont gf = font(); gf.setPixelSize(13);
|
||||||
|
p.setFont(gf); p.setPen(m_t.text);
|
||||||
|
p.drawText(triX + 14, fy + 14 + QFontMetrics(gf).ascent() / 2 - 1, g.name);
|
||||||
|
fy += 28;
|
||||||
|
|
||||||
|
if (!g.expanded) continue;
|
||||||
|
|
||||||
|
for (int ei : g.entries) {
|
||||||
|
auto& e = m_filtered[ei];
|
||||||
|
QRectF er(x, fy, w, 52);
|
||||||
|
m_entRects.append({ei, er});
|
||||||
|
if (m_hz == HZ_Entry && m_hi == ei) p.fillRect(er, m_t.hover);
|
||||||
|
|
||||||
|
drawIcon(p, e.isExample ? ":/vsicons/book.svg" : ":/vsicons/symbol-structure.svg",
|
||||||
|
x + 24, fy + 17, 18);
|
||||||
|
|
||||||
|
int tx = x + 52, avail = w - 64;
|
||||||
|
QFont nf = font(); nf.setPixelSize(14);
|
||||||
|
p.setFont(nf); p.setPen(m_t.text);
|
||||||
|
QFontMetrics nm(nf);
|
||||||
|
int ny = fy + 8;
|
||||||
|
p.drawText(tx, ny + nm.ascent(),
|
||||||
|
nm.elidedText(e.fileName, Qt::ElideMiddle, avail * 0.65));
|
||||||
|
|
||||||
|
if (!e.isExample) {
|
||||||
|
p.setPen(m_t.textDim);
|
||||||
|
QString dt = e.lastModified.toString("M/d/yyyy h:mm AP");
|
||||||
|
p.drawText(x + w - 12 - nm.horizontalAdvance(dt), ny + nm.ascent(), dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont pf = font(); pf.setPixelSize(12);
|
||||||
|
p.setFont(pf); p.setPen(m_t.textDim);
|
||||||
|
QFontMetrics pm(pf);
|
||||||
|
p.drawText(tx, ny + nm.height() + 4 + pm.ascent(),
|
||||||
|
pm.elidedText(e.dirPath, Qt::ElideMiddle, avail));
|
||||||
|
fy += 52;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_contentH = fy + m_scrollY - m_listTop;
|
||||||
|
m_maxScroll = qMax(0, m_contentH - listH);
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hit testing ──
|
||||||
|
|
||||||
|
Hit hitTest(QPoint pos) const {
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
if (m_cardR[i].contains(pos)) return {HZ_Card, i};
|
||||||
|
if (m_contR.contains(pos)) return {HZ_Continue, 0};
|
||||||
|
if (pos.y() >= m_listTop && pos.y() < height() - 24) {
|
||||||
|
for (const auto& [gi, r] : m_grpRects)
|
||||||
|
if (r.contains(pos)) return {HZ_Group, gi};
|
||||||
|
for (const auto& [ei, r] : m_entRects)
|
||||||
|
if (r.contains(pos)) return {HZ_Entry, ei};
|
||||||
|
}
|
||||||
|
return {HZ_None, -1};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
"button": "#ccccd0",
|
"button": "#ccccd0",
|
||||||
"text": "#1b1b22",
|
"text": "#1b1b22",
|
||||||
"textDim": "#5c5c68",
|
"textDim": "#5c5c68",
|
||||||
"textMuted": "#84848e",
|
"textMuted": "#6a6a78",
|
||||||
"textFaint": "#a8a8b0",
|
"textFaint": "#8a8a94",
|
||||||
"hover": "#d8d8de",
|
"hover": "#d8d8de",
|
||||||
"selected": "#d0d0d8",
|
"selected": "#d0d0d8",
|
||||||
"selection": "#b4c8e8",
|
"selection": "#b4c8e8",
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
|
|
||||||
// ── File info ──
|
// ── File info ──
|
||||||
m_fileInfoLabel = new QLabel;
|
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);
|
QString path = tm.themeFilePath(themeIndex);
|
||||||
m_fileInfoLabel->setText(path.isEmpty()
|
m_fileInfoLabel->setText(path.isEmpty()
|
||||||
? QStringLiteral("Built-in theme (edits save as user copy)")
|
? QStringLiteral("Built-in theme (edits save as user copy)")
|
||||||
@@ -109,7 +110,8 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
|
|
||||||
auto* hexLbl = new QLabel;
|
auto* hexLbl = new QLabel;
|
||||||
hexLbl->setFixedWidth(60);
|
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->addWidget(hexLbl);
|
||||||
|
|
||||||
row->addStretch();
|
row->addStretch();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "titlebar.h"
|
#include "titlebar.h"
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
|
#include <QMenu>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
@@ -76,15 +77,35 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
|||||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
.arg(theme.text.name()));
|
.arg(theme.text.name()));
|
||||||
|
|
||||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
// Menu bar palette — all roles used by MenuBarStyle, so live theme
|
||||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
// switches don't rely on app-palette inheritance (which can stall
|
||||||
|
// once setPalette has been called on a widget).
|
||||||
{
|
{
|
||||||
QPalette mbPal = m_menuBar->palette();
|
QPalette mbPal = m_menuBar->palette();
|
||||||
mbPal.setColor(QPalette::Window, theme.background);
|
mbPal.setColor(QPalette::Window, theme.background);
|
||||||
mbPal.setColor(QPalette::Button, theme.background);
|
mbPal.setColor(QPalette::Button, theme.background);
|
||||||
mbPal.setColor(QPalette::ButtonText, theme.text);
|
mbPal.setColor(QPalette::ButtonText, theme.text);
|
||||||
|
mbPal.setColor(QPalette::Text, theme.text);
|
||||||
|
mbPal.setColor(QPalette::Highlight, theme.selected);
|
||||||
|
mbPal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||||
|
mbPal.setColor(QPalette::AlternateBase, theme.surface);
|
||||||
|
mbPal.setColor(QPalette::Dark, theme.border);
|
||||||
|
mbPal.setColor(QPalette::Mid, theme.hover);
|
||||||
m_menuBar->setPalette(mbPal);
|
m_menuBar->setPalette(mbPal);
|
||||||
m_menuBar->setAutoFillBackground(false);
|
m_menuBar->setAutoFillBackground(false);
|
||||||
|
|
||||||
|
// Propagate to existing QMenu children so dropdown popups update too
|
||||||
|
for (auto* menu : m_menuBar->findChildren<QMenu*>()) {
|
||||||
|
QPalette mp = menu->palette();
|
||||||
|
mp.setColor(QPalette::Window, theme.background);
|
||||||
|
mp.setColor(QPalette::WindowText, theme.text);
|
||||||
|
mp.setColor(QPalette::Text, theme.text);
|
||||||
|
mp.setColor(QPalette::Highlight, theme.selected);
|
||||||
|
mp.setColor(QPalette::Link, theme.indHoverSpan);
|
||||||
|
mp.setColor(QPalette::AlternateBase, theme.surface);
|
||||||
|
mp.setColor(QPalette::Dark, theme.border);
|
||||||
|
menu->setPalette(mp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chrome buttons
|
// Chrome buttons
|
||||||
@@ -95,10 +116,10 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
|||||||
m_btnMin->setStyleSheet(btnStyle);
|
m_btnMin->setStyleSheet(btnStyle);
|
||||||
m_btnMax->setStyleSheet(btnStyle);
|
m_btnMax->setStyleSheet(btnStyle);
|
||||||
|
|
||||||
// Close button: red hover
|
// Close button: themed red hover
|
||||||
m_btnClose->setStyleSheet(QStringLiteral(
|
m_btnClose->setStyleSheet(QStringLiteral(
|
||||||
"QToolButton { background: transparent; border: none; }"
|
"QToolButton { background: transparent; border: none; }"
|
||||||
"QToolButton:hover { background: #c42b1c; }"));
|
"QToolButton:hover { background: %1; }").arg(theme.indHeatHot.name()));
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@@ -107,12 +128,14 @@ void TitleBarWidget::setShowIcon(bool show) {
|
|||||||
if (show) {
|
if (show) {
|
||||||
m_appLabel->setText(QString());
|
m_appLabel->setText(QString());
|
||||||
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(24, 24));
|
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(24, 24));
|
||||||
|
setFixedHeight(34);
|
||||||
} else {
|
} else {
|
||||||
m_appLabel->setPixmap(QPixmap());
|
m_appLabel->setPixmap(QPixmap());
|
||||||
m_appLabel->setText(QStringLiteral("Reclass"));
|
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||||
m_appLabel->setStyleSheet(
|
m_appLabel->setStyleSheet(
|
||||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
.arg(m_theme.text.name()));
|
.arg(m_theme.text.name()));
|
||||||
|
setFixedHeight(32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace rcx {
|
|||||||
struct TabInfo {
|
struct TabInfo {
|
||||||
const NodeTree* tree;
|
const NodeTree* tree;
|
||||||
QString name;
|
QString name;
|
||||||
void* subPtr; // QMdiSubWindow* as void*
|
void* subPtr; // QDockWidget* as void*
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
// 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) {
|
auto nameOf = [](const Node* n) {
|
||||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
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) {
|
// Helper: is a Hex padding node
|
||||||
int ca = a.tree->childrenOf(a.node->id).size();
|
auto isHexPad = [](NodeKind k) {
|
||||||
int cb = b.tree->childrenOf(b.node->id).size();
|
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||||
if (ca != cb) return ca > cb;
|
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||||
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);
|
|
||||||
|
|
||||||
// Helper: type display string for a member node
|
// Helper: type display string for a member node
|
||||||
auto memberTypeName = [](const Node& m) -> QString {
|
auto memberTypeName = [](const Node& m) -> QString {
|
||||||
@@ -69,11 +63,10 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
|||||||
return QString::fromLatin1(kindToString(m.kind));
|
return QString::fromLatin1(kindToString(m.kind));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper: is a Hex padding node
|
// TODO: re-enable sorting once startup perf is acceptable
|
||||||
auto isHexPad = [](NodeKind k) {
|
// auto countVisible = [&](const Entry& e) { ... };
|
||||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
// std::sort(types.begin(), types.end(), cmpChildren);
|
||||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
// std::sort(enums.begin(), enums.end(), cmpName);
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& e : types) {
|
for (const auto& e : types) {
|
||||||
QVector<int> members = e.tree->childrenOf(e.node->id);
|
QVector<int> members = e.tree->childrenOf(e.node->id);
|
||||||
|
|||||||
@@ -2768,6 +2768,125 @@ private slots:
|
|||||||
"Static fields should not have a separator line");
|
"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)
|
QTEST_MAIN(TestEditor)
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ private slots:
|
|||||||
defaults.themeIndex = 0;
|
defaults.themeIndex = 0;
|
||||||
defaults.fontName = "JetBrains Mono";
|
defaults.fontName = "JetBrains Mono";
|
||||||
defaults.menuBarTitleCase = true;
|
defaults.menuBarTitleCase = true;
|
||||||
defaults.safeMode = false;
|
|
||||||
defaults.autoStartMcp = false;
|
defaults.autoStartMcp = false;
|
||||||
|
|
||||||
OptionsDialog dlg(defaults);
|
OptionsDialog dlg(defaults);
|
||||||
@@ -93,7 +92,6 @@ private slots:
|
|||||||
input.themeIndex = 1;
|
input.themeIndex = 1;
|
||||||
input.fontName = "Consolas";
|
input.fontName = "Consolas";
|
||||||
input.menuBarTitleCase = false;
|
input.menuBarTitleCase = false;
|
||||||
input.safeMode = true;
|
|
||||||
input.autoStartMcp = true;
|
input.autoStartMcp = true;
|
||||||
|
|
||||||
OptionsDialog dlg(input);
|
OptionsDialog dlg(input);
|
||||||
@@ -102,7 +100,6 @@ private slots:
|
|||||||
QCOMPARE(r.themeIndex, 1);
|
QCOMPARE(r.themeIndex, 1);
|
||||||
QCOMPARE(r.fontName, QString("Consolas"));
|
QCOMPARE(r.fontName, QString("Consolas"));
|
||||||
QCOMPARE(r.menuBarTitleCase, false);
|
QCOMPARE(r.menuBarTitleCase, false);
|
||||||
QCOMPARE(r.safeMode, true);
|
|
||||||
QCOMPARE(r.autoStartMcp, true);
|
QCOMPARE(r.autoStartMcp, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user