Compare commits

..

16 Commits

Author SHA1 Message Date
IChooseYou
3ab6affa5e fix: vergilius fnptr import, remove tab pin, flatten workspace tree, middle-click close
- Fix vergilius_to_rcx.py to detect function pointer syntax (*Name)(params) and emit FuncPtr64
- Re-fetch 85 structs to recover proper field names (697/716 fixed)
- Remove pin button from dock tabs and all pin-related context menu items
- Fix newClass() creating duplicate tabs
- Set workspace tree font to match tab bar (size 10)
- Flatten workspace tree: remove redundant Project group node (VS Code Explorer style)
- Add middle-click to close dock widget tabs
- Allow type chooser to show cross-doc types for root nodes
2026-03-06 17:39:50 -07:00
IChooseYou
35b3cd9ac1 feat: enum editing UI, protect enums from struct ops, New Class opens two tabs
- New Class creates two Unnamed tabs, selects the first
- New Enum creates 5 placeholder members (Member0-4)
- Right-click enum member: Add Member Above/Below, Remove Member
- Right-click enum header: Add Member, Rename, Delete only
- Enum nodes fully protected from struct operations (no Add Child, Insert, Convert)
2026-03-06 11:00:06 -07:00
IChooseYou
e5938f7e82 fix: enable hover on dock tab bars via WA_Hover attribute 2026-03-06 09:45:23 -07:00
IChooseYou
03c49d19dd fix: type chooser always shows modifiers, tabs show class name, dock buttons restored on re-dock 2026-03-06 09:23:36 -07:00
IChooseYou
b7eebedf50 fix: remove grab_tabs test target (missing source file) 2026-03-06 08:23:09 -07:00
IChooseYou
9ff456a8d6 revert: remove theme xcopy to avoid clobbering custom themes 2026-03-06 08:22:40 -07:00
IChooseYou
580f285edd fix: also copy theme JSON files to output dir for MSVC builds 2026-03-06 08:22:02 -07:00
IChooseYou
d23a6c7656 fix: copy example .rcx files to output dir for MSVC builds 2026-03-06 08:20:33 -07:00
IChooseYou
25d8de95b7 fix: crash in dismissStartPage due to re-entrant close/rejected signal 2026-03-06 08:16:13 -07:00
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
17 changed files with 290902 additions and 3914 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

View File

@@ -72,7 +72,8 @@
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -84,7 +85,8 @@
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<PostBuildEvent>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
@@ -140,10 +142,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 +167,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

@@ -231,17 +231,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
TypePopupMode mode = TypePopupMode::FieldType;
if (target == EditTarget::ArrayElementType)
mode = TypePopupMode::ArrayElement;
else if (target == EditTarget::PointerTarget) {
// Primitive pointers (ptrDepth>0) should open FieldType with
// the base type selected and *//** preselected — not PointerTarget.
bool isPrimPtr = false;
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
const auto& n = m_doc->tree.nodes[nodeIdx];
isPrimPtr = n.ptrDepth > 0 && n.refId == 0;
}
mode = isPrimPtr ? TypePopupMode::FieldType
: TypePopupMode::PointerTarget;
}
// PointerTarget is handled as FieldType — modifiers * / ** will be pre-selected
showTypePopup(editor, mode, nodeIdx, globalPos);
});
@@ -1787,7 +1777,41 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
&& !node.bitfieldMembers.isEmpty()
&& subLine >= 0 && subLine < node.bitfieldMembers.size();
bool isEnumNode = node.resolvedClassKeyword() == QStringLiteral("enum");
if (isEnumMember || isBitfieldMember) {
if (isEnumMember) {
menu.addAction(icon("diff-added.svg"), "Add Member Above", [this, nodeId, subLine]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
auto members = m_doc->tree.nodes[ni].enumMembers;
int64_t val = (subLine > 0) ? members[subLine - 1].second + 1 : 0;
auto oldMembers = members;
members.insert(subLine, {QStringLiteral("NewMember"), val});
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
});
menu.addAction(icon("diff-added.svg"), "Add Member Below", [this, nodeId, subLine]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
auto members = m_doc->tree.nodes[ni].enumMembers;
int64_t val = members[subLine].second + 1;
auto oldMembers = members;
members.insert(subLine + 1, {QStringLiteral("NewMember"), val});
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
});
menu.addAction(icon("trash.svg"), "Remove Member", [this, nodeId, subLine]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
auto members = m_doc->tree.nodes[ni].enumMembers;
auto oldMembers = members;
members.remove(subLine);
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
});
menu.addSeparator();
}
if (isBitfieldMember) {
const auto& bm = node.bitfieldMembers[subLine];
if (bm.bitWidth == 1) {
@@ -1802,6 +1826,28 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addSeparator();
}
// Fall through to always-available actions
} else if (isEnumNode) {
// Enum header line — enum-specific actions only (no struct ops)
menu.addAction(icon("diff-added.svg"), "Add Member", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
auto members = m_doc->tree.nodes[ni].enumMembers;
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
auto oldMembers = members;
members.append({QStringLiteral("NewMember"), nextVal});
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
});
menu.addAction(icon("edit.svg"), "&Rename...", [this, editor, line]() {
editor->beginInlineEdit(EditTarget::Name, line);
});
menu.addSeparator();
menu.addAction(icon("trash.svg"), "&Delete", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) removeNode(ni);
});
menu.addSeparator();
// Fall through to always-available actions
} else {
// ── Quick-convert suggestions (top-level for fast access) ──
@@ -2775,7 +2821,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
}
// Add types from other open documents
if (mode != TypePopupMode::Root && m_projectDocs) {
if (m_projectDocs) {
QSet<QString> localNames;
for (const auto& e : entries)
if (e.entryKind == TypeEntry::Composite)

36
src/dock_tab_buttons.h Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include <QWidget>
#include <QToolButton>
#include <QHBoxLayout>
#include <QIcon>
// Dock tab button widget (close button)
// Placed on the right side of each dock tab via QTabBar::setTabButton.
class DockTabButtons : public QWidget {
Q_OBJECT
public:
QToolButton* closeBtn;
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
auto* hl = new QHBoxLayout(this);
hl->setContentsMargins(0, 0, 0, 0);
hl->setSpacing(0);
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);
}
void applyTheme(const QColor& hover) {
QString style = QStringLiteral(
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
"QToolButton:hover { background: %1; }").arg(hover.name());
closeBtn->setStyleSheet(style);
}
};

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
}
]
}

File diff suppressed because it is too large Load Diff

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

View File

@@ -480,64 +480,7 @@ public:
}
};
// ── 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");
}
};
#include "dock_tab_buttons.h"
static void applyGlobalTheme(const rcx::Theme& theme) {
QPalette pal;
@@ -631,6 +574,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
overlay->raise();
overlay->show();
// Central placeholder — will be replaced by start page after construction
m_centralPlaceholder = new QWidget(this);
m_centralPlaceholder->setFixedSize(0, 0);
m_centralPlaceholder->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
@@ -669,6 +613,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
this, &MainWindow::applyTheme);
// Apply theme once at startup (the signal only fires on change, not initial load)
applyTheme(ThemeManager::instance().current());
// Load plugins
m_pluginManager.LoadPlugins();
@@ -1494,8 +1441,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
splitter->setHandleWidth(1);
auto* ctrl = new RcxController(doc, splitter);
QString title = doc->filePath.isEmpty()
? rootName(doc->tree) : QFileInfo(doc->filePath).fileName();
QString title = rootName(doc->tree);
auto* dock = new QDockWidget(title, this);
dock->setObjectName(QStringLiteral("DocDock_%1").arg(quintptr(dock), 0, 16));
dock->setFeatures(QDockWidget::DockWidgetClosable |
@@ -1585,7 +1531,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
dockGrip->hide();
// Swap title bar when floating/docking, show/hide border + grip
connect(dock, &QDockWidget::topLevelChanged, this, [dock, emptyTitleBar, floatTitleBar, dockBorder, dockGrip](bool floating) {
connect(dock, &QDockWidget::topLevelChanged, this, [this, dock, emptyTitleBar, floatTitleBar, dockBorder, dockGrip](bool floating) {
dock->setTitleBarWidget(floating ? floatTitleBar : emptyTitleBar);
if (floating) {
dockBorder->setGeometry(0, 0, dock->width(), dock->height());
@@ -1597,6 +1543,8 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
} else {
dockBorder->hide();
dockGrip->hide();
// Re-docking creates a new tab bar — reinstall pin/close buttons
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
}
});
dock->installEventFilter(new DockBorderFilter(dockBorder, dockGrip, dock));
@@ -1720,8 +1668,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto it2 = m_tabs.find(dockGuard);
if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2);
if (it2->doc->filePath.isEmpty())
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
}
rebuildWorkspaceModel();
updateWindowTitle();
@@ -1737,8 +1684,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto it2 = m_tabs.find(dockGuard);
if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2);
if (it2->doc->filePath.isEmpty())
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
}
updateWindowTitle();
rebuildWorkspaceModel();
@@ -1787,6 +1733,7 @@ void MainWindow::setupDockTabBars() {
// No stylesheet — painting handled by MenuBarStyle
tabBar->setStyleSheet(QString());
tabBar->setAttribute(Qt::WA_Hover, true);
tabBar->setElideMode(Qt::ElideNone);
tabBar->setExpanding(false);
// Set editor font so tab width sizing matches our label painting
@@ -1827,8 +1774,9 @@ void MainWindow::setupDockTabBars() {
tabBar->setTabButton(i, QTabBar::RightSide, btns);
}
// Context menu (install only once)
// Middle-click close + context menu (install only once)
if (tabBar->contextMenuPolicy() == Qt::CustomContextMenu) continue;
tabBar->installEventFilter(this);
tabBar->setContextMenuPolicy(Qt::CustomContextMenu);
connect(tabBar, &QTabBar::customContextMenuRequested,
this, [this, tabBar](const QPoint& pos) {
@@ -1843,9 +1791,6 @@ void MainWindow::setupDockTabBars() {
if (!target) return;
auto tabIt = m_tabs.find(target);
auto* btns = qobject_cast<DockTabButtons*>(
tabBar->tabButton(idx, QTabBar::RightSide));
bool isPinned = btns && btns->pinned;
QMenu menu;
@@ -1869,28 +1814,6 @@ void MainWindow::setupDockTabBars() {
});
}
// Close All But Pinned (only if any tab is pinned)
bool anyPinned = false;
for (int i = 0; i < tabBar->count(); ++i) {
auto* b = qobject_cast<DockTabButtons*>(
tabBar->tabButton(i, QTabBar::RightSide));
if (b && b->pinned) { anyPinned = true; break; }
}
if (anyPinned) {
menu.addAction("Close All But Pinned", [this, tabBar]() {
QVector<QDockWidget*> toClose;
for (int i = 0; i < tabBar->count(); ++i) {
auto* b = qobject_cast<DockTabButtons*>(
tabBar->tabButton(i, QTabBar::RightSide));
if (b && b->pinned) continue;
QString title = tabBar->tabText(i);
for (auto* d : m_docDocks)
if (d->windowTitle() == title) { toClose.append(d); break; }
}
for (auto* d : toClose) d->close();
});
}
menu.addSeparator();
// Copy Full Path / Open Containing Folder (only if saved)
@@ -1912,14 +1835,6 @@ void MainWindow::setupDockTabBars() {
menu.addSeparator();
// Pin / Unpin
if (btns) {
QIcon pinIcon = makeIcon(isPinned ? ":/vsicons/pinned.svg"
: ":/vsicons/pin.svg");
menu.addAction(pinIcon, isPinned ? "Unpin Tab" : "Pin Tab",
[btns, isPinned]() { btns->setPinned(!isPinned); });
}
menu.addSeparator();
// New Document Groups (only if >1 tab)
@@ -1971,8 +1886,47 @@ void MainWindow::setupDockTabBars() {
}
}
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::MiddleButton) {
if (auto* tabBar = qobject_cast<QTabBar*>(obj)) {
int idx = tabBar->tabAt(me->pos());
if (idx >= 0) {
QString title = tabBar->tabText(idx);
for (auto* d : m_docDocks) {
if (d->windowTitle() == title) { d->close(); break; }
}
return true;
}
}
}
}
return QMainWindow::eventFilter(obj, event);
}
// Build a minimal empty struct for new documents
static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
// ── Enum: bare node with empty enumMembers, no hex children ──
if (classKeyword == QStringLiteral("enum")) {
Node root;
root.kind = NodeKind::Struct;
root.name = "Unnamed";
root.structTypeName = "Unnamed";
root.classKeyword = classKeyword;
root.parentId = 0;
root.offset = 0;
root.enumMembers = {
{QStringLiteral("Member0"), 0},
{QStringLiteral("Member1"), 1},
{QStringLiteral("Member2"), 2},
{QStringLiteral("Member3"), 3},
{QStringLiteral("Member4"), 4},
};
tree.addNode(root);
return;
}
Node root;
root.kind = NodeKind::Struct;
root.name = "instance";
@@ -2349,15 +2303,20 @@ void MainWindow::applyTheme(const Theme& theme) {
if (m_titleBar)
m_titleBar->applyTheme(theme);
// Start page
if (m_startPage)
m_startPage->applyTheme(theme);
// Update border overlay color
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
// Style doc dock tab bars and remove dock borders
// Style doc dock tab bars and remove dock borders.
// QWidget default colors are required because having ANY stylesheet on QMainWindow
// switches children from palette-based to CSS-based rendering.
setStyleSheet(QStringLiteral(
"QMainWindow::separator { width: 1px; height: 1px; background: transparent; }"
"QDockWidget { border: none; }"
"QDockWidget > QWidget { border: none; }")
.arg(theme.border.name()));
"QDockWidget > QWidget { border: none; }"));
for (auto* tabBar : findChildren<QTabBar*>()) {
// Only style tab bars owned directly by this QMainWindow (dock tabs),
@@ -2365,6 +2324,7 @@ void MainWindow::applyTheme(const Theme& theme) {
if (tabBar->parent() == this) {
// No stylesheet — painting handled by MenuBarStyle (CE_TabBarTabShape/Label)
tabBar->setStyleSheet(QString());
tabBar->setAttribute(Qt::WA_Hover, true);
tabBar->setElideMode(Qt::ElideNone);
tabBar->setExpanding(false);
// Set editor font so tab width sizing matches our label painting
@@ -2630,9 +2590,12 @@ void MainWindow::setEditorFont(const QString& fontName) {
}
}
}
// Sync workspace tree font
if (m_workspaceTree)
m_workspaceTree->setFont(f);
// Sync workspace tree font (match tab bar size)
if (m_workspaceTree) {
QFont wf(fontName, 10);
wf.setFixedPitch(true);
m_workspaceTree->setFont(wf);
}
// Sync dock titlebar font
if (m_dockTitleLabel)
m_dockTitleLabel->setFont(f);
@@ -2687,9 +2650,7 @@ void MainWindow::updateWindowTitle() {
auto* activeDock = m_activeDocDock;
if (activeDock && m_tabs.contains(activeDock)) {
auto& tab = m_tabs[activeDock];
QString name = tab.doc->filePath.isEmpty()
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
: QFileInfo(tab.doc->filePath).fileName();
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
if (tab.doc->modified) name += " *";
title = name + " - Reclass";
} else {
@@ -3222,10 +3183,7 @@ QDockWidget* MainWindow::project_open(const QString& path) {
if (filePath.isEmpty()) {
filePath = QFileDialog::getOpenFileName(this,
"Open Definition", {},
"All Supported (*.rcx *.json *.reclass *.MemeCls *.xml)"
";;Reclass (*.rcx)"
";;JSON (*.json)"
";;ReClass XML (*.reclass *.MemeCls *.xml)"
"Reclass (*.rcx)"
";;All (*)");
if (filePath.isEmpty()) return nullptr;
}
@@ -3236,8 +3194,7 @@ QDockWidget* MainWindow::project_open(const QString& path) {
QFile probe(filePath);
if (probe.open(QIODevice::ReadOnly)) {
QByteArray head = probe.read(64);
isXml = head.trimmed().startsWith("<?xml") || head.trimmed().startsWith("<ReClass")
|| head.trimmed().startsWith("<MemeCls");
isXml = head.trimmed().startsWith("<?xml") || head.trimmed().startsWith("<ReClass");
}
}
@@ -3408,13 +3365,19 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_workspaceTree->setExpandsOnDoubleClick(false);
m_workspaceTree->setMouseTracking(true);
{
QSettings s("Reclass", "Reclass");
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
f.setFixedPitch(true);
m_workspaceTree->setFont(f);
}
connect(m_workspaceSearch, &QLineEdit::textChanged, this, [this](const QString& text) {
m_workspaceProxy->setFilterFixedString(text);
if (!text.isEmpty())
m_workspaceTree->expandAll();
else
m_workspaceTree->expandToDepth(0);
m_workspaceTree->collapseAll();
});
// Override palette: selection + hover use theme colors (not default blue)
@@ -3432,13 +3395,9 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
QModelIndex index = m_workspaceTree->indexAt(pos);
if (!index.isValid()) return;
auto structIdVar = index.data(Qt::UserRole + 1);
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
// Right-click on "Project" group → New Class / New Struct / New Enum
if (structId == rcx::kGroupSentinel) {
// Right-click on empty area → New Class / New Struct / New Enum
if (!index.isValid()) {
QMenu menu;
auto* actClass = menu.addAction("New Class");
auto* actStruct = menu.addAction("New Struct");
@@ -3450,6 +3409,8 @@ void MainWindow::createWorkspaceDock() {
return;
}
auto structIdVar = index.data(Qt::UserRole + 1);
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
if (structId == 0) return;
auto subVar = index.data(Qt::UserRole);
@@ -3554,12 +3515,6 @@ void MainWindow::createWorkspaceDock() {
auto structIdVar = index.data(Qt::UserRole + 1);
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
if (structId == rcx::kGroupSentinel) {
// "Project" folder: toggle expand/collapse
m_workspaceTree->setExpanded(index, !m_workspaceTree->isExpanded(index));
return;
}
auto subVar = index.data(Qt::UserRole);
if (!subVar.isValid()) return;
auto* ownerDock = static_cast<QDockWidget*>(subVar.value<void*>());
@@ -3765,13 +3720,10 @@ void MainWindow::rebuildWorkspaceModel() {
TabState& tab = it.value();
if (seenDocs.contains(tab.doc)) continue; // skip duplicate doc views
seenDocs.insert(tab.doc);
QString name = tab.doc->filePath.isEmpty()
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
: QFileInfo(tab.doc->filePath).fileName();
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
}
rcx::buildProjectExplorer(m_workspaceModel, tabs);
m_workspaceTree->expandToDepth(0);
}
void MainWindow::addRecentFile(const QString& path) {
@@ -4007,6 +3959,67 @@ void MainWindow::updateBorderColor(const QColor& color) {
m_borderOverlay->update();
}
void MainWindow::showStartPage() {
if (m_startPage) return;
m_startPage = new StartPageWidget(this);
m_startPage->applyTheme(ThemeManager::instance().current());
// Size the popup to ~90% of the main window
QSize sz(qBound(900, int(width() * 0.9), width() - 20),
qBound(560, int(height() * 0.85), height() - 20));
m_startPage->setFixedSize(sz);
// Wire start page signals — each closes the dialog then performs action
connect(m_startPage, &StartPageWidget::openProject, this, [this]() {
dismissStartPage();
openFile();
if (m_tabs.isEmpty()) showStartPage();
});
connect(m_startPage, &StartPageWidget::newClass, this, [this]() {
dismissStartPage();
newClass();
});
connect(m_startPage, &StartPageWidget::importSource, this, [this]() {
dismissStartPage();
importFromSource();
if (m_tabs.isEmpty()) showStartPage();
});
connect(m_startPage, &StartPageWidget::importXml, this, [this]() {
dismissStartPage();
importReclassXml();
if (m_tabs.isEmpty()) showStartPage();
});
connect(m_startPage, &StartPageWidget::importPdb, this, [this]() {
dismissStartPage();
importPdb();
if (m_tabs.isEmpty()) showStartPage();
});
connect(m_startPage, &StartPageWidget::continueClicked, this, [this]() {
dismissStartPage();
selfTest();
});
connect(m_startPage, &StartPageWidget::fileSelected, this, [this](const QString& path) {
dismissStartPage();
project_open(path);
});
connect(m_startPage, &QDialog::rejected, this, [this]() {
dismissStartPage();
});
// Center over main window and show as application-modal
m_startPage->move(geometry().center() - m_startPage->rect().center());
m_startPage->open();
}
void MainWindow::dismissStartPage() {
if (!m_startPage) return;
auto* sp = m_startPage;
m_startPage = nullptr; // null first — close() may re-enter via rejected signal
sp->close();
sp->deleteLater();
}
} // namespace rcx
// ── Entry point ──
@@ -4043,11 +4056,9 @@ int main(int argc, char* argv[]) {
window.show();
// Auto-open demo project from saved .rcx file
QMetaObject::invokeMethod(&window, "selfTest");
// Show VS2022-style start page instead of jumping straight to demo
QMetaObject::invokeMethod(&window, "showStartPage", Qt::QueuedConnection);
return app.exec();
}
// DockTabButtons has Q_OBJECT in main.cpp — need the moc include
#include "main.moc"

View File

@@ -3,6 +3,7 @@
#include "titlebar.h"
#include "pluginmanager.h"
#include "scannerpanel.h"
#include "startpage.h"
#include <QMainWindow>
#include <QLabel>
#include <QSplitter>
@@ -169,9 +170,15 @@ 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;
bool eventFilter(QObject* obj, QEvent* event) override;
};
} // namespace rcx

View File

@@ -64,5 +64,6 @@
<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>

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

@@ -415,9 +415,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
return btn;
};
m_chipPrim = makeChip(QStringLiteral("P"));
m_chipTypes = makeChip(QStringLiteral("T"));
m_chipEnums = makeChip(QStringLiteral("E"));
m_chipPrim = makeChip(QStringLiteral("Built-in"));
m_chipTypes = makeChip(QStringLiteral("Types"));
m_chipEnums = makeChip(QStringLiteral("Enum"));
m_chipPrim->setAccessibleName(QStringLiteral("Show primitives"));
m_chipTypes->setAccessibleName(QStringLiteral("Show composites"));
m_chipEnums->setAccessibleName(QStringLiteral("Show enums"));
@@ -1080,9 +1080,9 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
auto updateChipLabel = [](QToolButton* btn, const QString& abbrev, int count) {
btn->setText(QStringLiteral("%1 (%2)").arg(abbrev).arg(count));
};
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("P"), primCount);
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("T"), typeCount);
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("E"), enumCount);
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("Built-in"), primCount);
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("Types"), typeCount);
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("Enum"), enumCount);
if (m_statusLabel)
m_statusLabel->setText(QStringLiteral("%1 results").arg(resultCount));

View File

@@ -21,13 +21,6 @@ inline void buildProjectExplorer(QStandardItemModel* model,
model->clear();
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
// Single "Project" root with folder icon
void* firstSub = tabs.isEmpty() ? nullptr : tabs[0].subPtr;
auto* projectItem = new QStandardItem(QIcon(":/vsicons/folder.svg"),
QStringLiteral("Project"));
projectItem->setData(QVariant::fromValue(firstSub), Qt::UserRole);
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
// Collect all top-level structs/enums across all tabs
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
QVector<Entry> types, enums;
@@ -63,24 +56,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);
@@ -113,7 +92,7 @@ inline void buildProjectExplorer(QStandardItemModel* model,
item->appendRow(childItem);
}
projectItem->appendRow(item);
model->appendRow(item);
}
for (const auto& e : enums) {
@@ -125,10 +104,8 @@ inline void buildProjectExplorer(QStandardItemModel* model,
QIcon(":/vsicons/symbol-enum.svg"), display);
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
projectItem->appendRow(item);
model->appendRow(item);
}
model->appendRow(projectItem);
}
} // namespace rcx

View File

@@ -341,6 +341,19 @@ def parse_field_line(line, offset, parent_id, ids, struct_registry):
line = re.sub(r'\bvolatile\b', '', line).strip()
line = re.sub(r'\s+', ' ', line)
# Check for function pointer: RETURN_TYPE (*NAME)(PARAMS)
fnptr_m = re.search(r'\(\*\s*(\w+)\s*\)', line)
if fnptr_m:
field_name = fnptr_m.group(1)
node_id = ids.alloc()
return {
'id': str(node_id),
'kind': 'FuncPtr64',
'name': field_name,
'offset': offset,
'parentId': str(parent_id),
}
# Check for struct/union keyword prefix
keyword = None
m = re.match(r'^(struct|union|enum)\s+(.+)', line)