Compare commits

..

24 Commits

Author SHA1 Message Date
Sen66
955db3813a fix: msvc build due to startpage.h 2026-03-06 16:10:54 +01:00
IChooseYou
f4f203e0f0 Merge remote-tracking branch 'origin/fix-msvc-build' 2026-03-06 08:07:57 -07:00
IChooseYou
1d3f1a672a fix: start page card order, icon consistency, and Continue placement 2026-03-06 08:07:27 -07:00
Sen66
da29206bdb fix: msvc build with latest dock header file 2026-03-06 16:03:54 +01:00
IChooseYou
4986893fca feat: VS2022-style start page popup with recent files and get started cards 2026-03-06 07:58:13 -07:00
IChooseYou
17a1fb032e chore: remove Demo.rcx, add WinSDK + windows-x86_64.h examples 2026-03-06 07:56:33 -07:00
IChooseYou
8d92957837 fix: move DockTabButtons to header for MSVC automoc compatibility
automoc doesn't generate main.moc on MSVC, breaking the build.
Move DockTabButtons (which needs Q_OBJECT) to its own header so
automoc handles it as moc_dock_tab_buttons.cpp instead.
2026-03-06 06:14:59 -07:00
IChooseYou
f981fe456d feat: see-through popup dismiss for disasm/value-history/struct-preview
Override mouseMoveEvent in all three popup classes to forward mouse
position back to viewport hover logic. When the row underneath the
popup represents a different node, the popup dismisses automatically,
allowing rapid swiping through FuncPtr rows.
2026-03-05 18:25:40 -07:00
IChooseYou
877ceea4c1 feat: VS-style dock tabs with middle-elision and full context menu
- Remove stylesheet from dock tab bars; handle all painting in
  MenuBarStyle (CE_TabBarTabShape + CE_TabBarTabLabel) so middle-
  elision actually works (QStyleSheetStyle was intercepting labels)
- Accent line on selected tab, dark background, bottom border
- Tab font synced with editor font for correct sizing
- Full right-click context menu: Close, Close All Tabs, Close All
  But This, Close All But Pinned, Copy Full Path, Open Containing
  Folder, Float/Dock, Pin/Unpin Tab, New Horizontal/Vertical
  Document Group
- Add View → Reset Windows to re-tabify all docks
- Remove old View → Split/Remove Split
- Guard deferred timer lambdas with QPointer<QDockWidget>
- Extract setupDockTabBars() for idempotent tab bar configuration
- Register close-all.svg and split-vertical.svg icons
2026-03-05 15:16:01 -07:00
IChooseYou
4160a229c6 feat: workspace double-click opens struct in new tab + flat tab corners
- Double-clicking a root struct in the workspace tree opens it in a new
  tab (dock) sharing the same document, focused on that struct
- If a tab already views that struct, raises it instead of duplicating
- Child member double-click still navigates within the existing tab
- Doc lifecycle ref-counted: only deleted when last tab referencing it closes
- rebuildAllDocs/rebuildWorkspaceModel deduplicate shared docs
- Removed border-radius from all tab bar stylesheets (flat corners)
2026-03-05 13:49:42 -07:00
Sen66
1e1afc1640 fix: docking of 'project' window 2026-03-05 19:47:18 +01:00
IChooseYou
f0cf6c549a revert: restore .NET CLR hosting description for ReClass.NET plugin 2026-03-05 06:37:56 -07:00
Sen66
683eab16ee fix: better fix to switch to newly created class 2026-03-05 14:25:49 +01:00
Sen66
b53dea8f9f fix crash on application close 2026-03-05 14:25:06 +01:00
Sen66
f06abbab79 fix: on new class, switch to it 2026-03-05 14:23:07 +01:00
Sen66
2477591ed2 fix: assertion due to undo history disabled nullptr 2026-03-05 14:21:07 +01:00
IChooseYou
6c13356d6d docs: trim README plugin descriptions 2026-03-05 06:07:37 -07:00
IChooseYou
3b273a7ab2 fix: don't skip Array in scope width calc — only skip Struct
Array headers like int32_t[10] render in the type column and need
their width accounted for. Only Struct (pointer headers) should be
excluded from inflating sibling column widths.
2026-03-05 06:02:43 -07:00
IChooseYou
3509a0d9dd Merge remote-tracking branch 'origin/floating' 2026-03-05 05:58:18 -07:00
Sen66
43c3f5a842 fix: highlight issue between command row & opening brace 2026-03-05 13:52:40 +01:00
Sen66
0697ce4853 feat: option to have class opening brace on new line 2026-03-05 13:48:26 +01:00
IChooseYou
ed1bfd04cd fix: tighten editor column spacing — skip struct/array in scope width calc
Reduce kMinTypeW from 8 to 7, and exclude Struct/Array children from
per-scope column width measurement so pointer headers don't inflate
sibling hex row padding.
2026-03-05 13:48:26 +01:00
IChooseYou
c275eb33c9 fix: tighten editor column spacing — skip struct/array in scope width calc
Reduce kMinTypeW from 8 to 7, and exclude Struct/Array children from
per-scope column width measurement so pointer headers don't inflate
sibling hex row padding.
2026-03-05 05:46:14 -07:00
Sen66
636176ee8c feat: floating windows like old windbg 2026-03-05 13:23:00 +01:00
24 changed files with 289827 additions and 487 deletions

View File

@@ -109,6 +109,8 @@ add_executable(Reclass
src/scannerpanel.h
src/scannerpanel.cpp
src/mainwindow.h
src/startpage.h
src/dock_tab_buttons.h
src/optionsdialog.h
src/optionsdialog.cpp
src/titlebar.h
@@ -344,6 +346,11 @@ if(BUILD_TESTING)
endif()
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
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp

View File

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

View File

@@ -140,10 +140,12 @@
<ClInclude Include="..\src\addressparser.h" />
<ClInclude Include="..\src\core.h" />
<ClInclude Include="..\src\disasm.h" />
<QtMoc Include="..\src\dock_tab_buttons.h" />
<ClInclude Include="..\src\generator.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\pluginmanager.h" />
<ClInclude Include="..\src\providerregistry.h" />
<QtMoc Include="..\src\startpage.h" />
<ClInclude Include="..\src\workspace_model.h" />
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
<ClInclude Include="..\src\imports\import_pdb.h" />
@@ -163,7 +165,12 @@
<ClCompile Include="..\src\editor.cpp" />
<ClCompile Include="..\src\format.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\pluginmanager.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />

View File

@@ -89,6 +89,12 @@
<QtMoc Include="..\src\themes\thememanager.h">
<Filter>Header Files\themes</Filter>
</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>
<ClInclude Include="..\src\addressparser.h">
@@ -165,9 +171,6 @@
<ClCompile Include="..\src\generator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\optionsdialog.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -219,5 +222,8 @@
<ClCompile Include="..\src\themes\thememanager.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
<ClCompile Include="..\src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>
</Project>

View File

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

View File

@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
}
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
bool treeLines) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines);
bool treeLines, bool braceWrap) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap);
}
bool RcxDocument::save(const QString& path) {
@@ -558,9 +558,9 @@ void RcxController::refresh() {
// Compose against snapshot provider if active, otherwise real provider
if (m_snapshotProv)
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines);
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
else
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines);
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
s_composeDoc = nullptr;
@@ -1766,6 +1766,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QApplication::clipboard()->setText(addrs.join('\n'));
});
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
return;
}
@@ -2282,6 +2283,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
});
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
}
@@ -2442,6 +2444,7 @@ void RcxController::updateCommandRow() {
.arg(elide(src, 40), elide(addr, 24));
// Build row 2: root class type + name (uses current view root)
QString brace = m_braceWrap ? QString() : QStringLiteral(" {");
QString row2;
if (m_viewRootId != 0) {
int vi = m_doc->tree.indexOfId(m_viewRootId);
@@ -2449,8 +2452,8 @@ void RcxController::updateCommandRow() {
const auto& n = m_doc->tree.nodes[vi];
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1 %2 {")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
row2 = QStringLiteral("%1 %2%3")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
}
}
if (row2.isEmpty()) {
@@ -2460,14 +2463,14 @@ void RcxController::updateCommandRow() {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1 %2 {")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
row2 = QStringLiteral("%1 %2%3")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
break;
}
}
}
if (row2.isEmpty())
row2 = QStringLiteral("struct NoName {");
row2 = QStringLiteral("struct NoName") + brace;
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
@@ -3259,6 +3262,11 @@ void RcxController::setTreeLines(bool v) {
refresh();
}
void RcxController::setBraceWrap(bool v) {
m_braceWrap = v;
refresh();
}
void RcxController::setupAutoRefresh() {
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
m_refreshTimer = new QTimer(this);

View File

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

View File

@@ -699,7 +699,7 @@ inline constexpr int kColValue = 96;
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
inline constexpr int kSepWidth = 1;
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
inline constexpr int kMinTypeW = 7; // Minimum type column width (fits "uint8_t")
inline constexpr int kMaxTypeW = 128; // Maximum type column width
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 128; // Maximum name column width
@@ -1031,6 +1031,7 @@ namespace fmt {
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false, bool treeLines = false);
bool compactColumns = false, bool treeLines = false,
bool braceWrap = false);
} // namespace rcx

65
src/dock_tab_buttons.h Normal file
View 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");
}
};

View File

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

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

42817
src/examples/windows-x86_64.h Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,8 @@
#include "titlebar.h"
#include "pluginmanager.h"
#include "scannerpanel.h"
#include "startpage.h"
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QLabel>
#include <QSplitter>
#include <QTabWidget>
@@ -72,22 +71,19 @@ public:
void clearMcpStatus();
// Project Lifecycle API
QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);
QDockWidget* project_new(const QString& classKeyword = QString());
QDockWidget* project_open(const QString& path = {});
bool project_save(QDockWidget* dock = nullptr, bool saveAs = false);
void project_close(QDockWidget* dock = nullptr);
private:
enum ViewMode { VM_Reclass, VM_Rendered };
QMdiArea* m_mdiArea;
QWidget* m_centralPlaceholder;
ShimmerLabel* m_statusLabel;
QString m_appStatus;
bool m_mcpBusy = false;
QTimer* m_mcpClearTimer = nullptr;
QButtonGroup* m_viewBtnGroup = nullptr;
QPushButton* m_btnReclass = nullptr;
QPushButton* m_btnRendered = nullptr;
TitleBarWidget* m_titleBar = nullptr;
QMenuBar* m_menuBar = nullptr;
bool m_menuBarTitleCase = false;
@@ -95,7 +91,6 @@ private:
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
QAction* m_removeSplitAction = nullptr;
QMenu* m_sourceMenu = nullptr;
QMenu* m_recentFilesMenu = nullptr;
@@ -117,7 +112,9 @@ private:
QVector<SplitPane> panes;
int activePaneIdx = 0;
};
QMap<QMdiSubWindow*, TabState> m_tabs;
QMap<QDockWidget*, TabState> m_tabs;
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
void rebuildAllDocs();
@@ -134,8 +131,10 @@ private:
TabState* activeTab();
TabState* tabByIndex(int index);
int tabCount() const { return m_tabs.size(); }
QMdiSubWindow* createTab(RcxDocument* doc);
QDockWidget* createTab(RcxDocument* doc);
void setupDockTabBars();
void updateWindowTitle();
void closeAllDocDocks();
void setViewMode(ViewMode mode);
void updateRenderedView(TabState& tab, SplitPane& pane);
@@ -145,7 +144,6 @@ private:
SplitPane createSplitPane(TabState& tab);
void applyTheme(const Theme& theme);
void styleTabCloseButtons();
void syncViewButtons(ViewMode mode);
SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane();
@@ -172,6 +170,11 @@ private:
DockGripWidget* m_scanDockGrip = nullptr;
void createScannerDock();
// Start page
StartPageWidget* m_startPage = nullptr;
Q_INVOKABLE void showStartPage();
void dismissStartPage();
protected:
void changeEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;

View File

@@ -122,6 +122,10 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_showIconCheck->setChecked(current.showIcon);
visualLayout->addRow(m_showIconCheck);
m_braceWrapCheck = new QCheckBox("Opening brace on new line");
m_braceWrapCheck->setChecked(current.braceWrap);
visualLayout->addRow(m_braceWrapCheck);
generalLayout->addWidget(visualGroup);
generalLayout->addStretch();
@@ -212,6 +216,7 @@ OptionsResult OptionsDialog::result() const {
r.autoStartMcp = m_autoMcpCheck->isChecked();
r.refreshMs = m_refreshSpin->value();
r.generatorAsserts = m_assertCheck->isChecked();
r.braceWrap = m_braceWrapCheck->isChecked();
return r;
}

View File

@@ -18,6 +18,7 @@ struct OptionsResult {
bool autoStartMcp = true;
int refreshMs = 660;
bool generatorAsserts = false;
bool braceWrap = false;
};
class OptionsDialog : public QDialog {
@@ -41,6 +42,7 @@ private:
QCheckBox* m_autoMcpCheck = nullptr;
QSpinBox* m_refreshSpin = nullptr;
QCheckBox* m_assertCheck = nullptr;
QCheckBox* m_braceWrapCheck = nullptr;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;

View File

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

View File

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

360
src/startpage.h Normal file
View 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

View File

@@ -1,5 +1,6 @@
#include "titlebar.h"
#include "themes/thememanager.h"
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QStyle>
@@ -76,15 +77,35 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
.arg(theme.text.name()));
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
// Set Window + Button to background so Fusion never paints a foreign color.
// Menu bar palette — all roles used by MenuBarStyle, so live theme
// switches don't rely on app-palette inheritance (which can stall
// once setPalette has been called on a widget).
{
QPalette mbPal = m_menuBar->palette();
mbPal.setColor(QPalette::Window, theme.background);
mbPal.setColor(QPalette::Button, theme.background);
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->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

View File

@@ -10,7 +10,7 @@ namespace rcx {
struct TabInfo {
const NodeTree* tree;
QString name;
void* subPtr; // QMdiSubWindow* as void*
void* subPtr; // QDockWidget* as void*
};
// Sentinel value stored in UserRole+1 to mark the Project group node.
@@ -63,24 +63,10 @@ inline void buildProjectExplorer(QStandardItemModel* model,
return QString::fromLatin1(kindToString(m.kind));
};
// Sort structs by visible children count descending (most fields first)
auto countVisible = [&](const Entry& e) {
int n = 0;
for (int idx : e.tree->childrenOf(e.node->id))
if (!isHexPad(e.tree->nodes[idx].kind)) ++n;
return n;
};
auto cmpChildren = [&](const Entry& a, const Entry& b) {
int ca = countVisible(a);
int cb = countVisible(b);
if (ca != cb) return ca > cb;
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
};
std::sort(types.begin(), types.end(), cmpChildren);
auto cmpName = [&](const Entry& a, const Entry& b) {
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
};
std::sort(enums.begin(), enums.end(), cmpName);
// TODO: re-enable sorting once startup perf is acceptable
// auto countVisible = [&](const Entry& e) { ... };
// std::sort(types.begin(), types.end(), cmpChildren);
// std::sort(enums.begin(), enums.end(), cmpName);
for (const auto& e : types) {
QVector<int> members = e.tree->childrenOf(e.node->id);

View File

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