#include "mainwindow.h" #include "providerregistry.h" #include "generator.h" #include "imports/import_reclass_xml.h" #include "imports/import_source.h" #include "imports/export_reclass_xml.h" #include "imports/import_pdb.h" #include "imports/import_pdb_dialog.h" #include "mcp/mcp_bridge.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "workspace_model.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "themes/thememanager.h" #include "themes/themeeditor.h" #include "optionsdialog.h" #ifdef _WIN32 #include #include #include #include #include static void setDarkTitleBar(QWidget* widget) { // Requires Windows 10 1809+ (build 17763) auto hwnd = reinterpret_cast(widget->winId()); BOOL dark = TRUE; // Attribute 20 = DWMWA_USE_IMMERSIVE_DARK_MODE (build 18985+), 19 for older DWORD attr = 20; if (FAILED(DwmSetWindowAttribute(hwnd, attr, &dark, sizeof(dark)))) { attr = 19; DwmSetWindowAttribute(hwnd, attr, &dark, sizeof(dark)); } } // Guard flag to prevent re-entrant crash inside the handler static volatile LONG s_inCrashHandler = 0; static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) { // Prevent re-entrant crash: if we fault inside the handler, skip the // risky dbghelp work and just terminate with what we already printed. if (InterlockedCompareExchange(&s_inCrashHandler, 1, 0) != 0) { fprintf(stderr, "\n(re-entrant fault inside crash handler — aborting)\n"); fflush(stderr); return EXCEPTION_EXECUTE_HANDLER; } // Phase 1: always-safe output (no allocations, no complex APIs) fprintf(stderr, "\n=== UNHANDLED EXCEPTION ===\n"); fprintf(stderr, "Code : 0x%08lX\n", ep->ExceptionRecord->ExceptionCode); fprintf(stderr, "Addr : %p\n", ep->ExceptionRecord->ExceptionAddress); #ifdef _M_X64 fprintf(stderr, "RIP : 0x%016llx\n", (unsigned long long)ep->ContextRecord->Rip); fprintf(stderr, "RSP : 0x%016llx\n", (unsigned long long)ep->ContextRecord->Rsp); #else fprintf(stderr, "EIP : 0x%08lx\n", (unsigned long)ep->ContextRecord->Eip); #endif fflush(stderr); // Phase 1.5: write a full minidump next to the executable { // Build dump path: /reclass_crash_.dmp wchar_t exePath[MAX_PATH] = {}; GetModuleFileNameW(NULL, exePath, MAX_PATH); // Strip exe filename to get directory wchar_t* lastSlash = wcsrchr(exePath, L'\\'); if (lastSlash) *(lastSlash + 1) = L'\0'; SYSTEMTIME st; GetLocalTime(&st); wchar_t dumpPath[MAX_PATH]; _snwprintf_s(dumpPath, MAX_PATH, L"%sreclass_crash_%04d%02d%02d_%02d%02d%02d.dmp", exePath, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); HANDLE hFile = CreateFileW(dumpPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION mei; mei.ThreadId = GetCurrentThreadId(); mei.ExceptionPointers = ep; mei.ClientPointers = FALSE; // MiniDumpWithFullMemory: captures entire process address space // so we can inspect all heap objects, Qt state, node trees, etc. BOOL ok = MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(), hFile, static_cast(MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules), &mei, NULL, NULL); CloseHandle(hFile); if (ok) { fprintf(stderr, "Dump : %ls\n", dumpPath); } else { fprintf(stderr, "Dump : FAILED (error %lu)\n", GetLastError()); } } else { fprintf(stderr, "Dump : could not create file (error %lu)\n", GetLastError()); } fflush(stderr); } // Phase 2: attempt symbol resolution + stack walk // Copy context so StackWalk64 can mutate it safely CONTEXT ctxCopy = *ep->ContextRecord; HANDLE process = GetCurrentProcess(); HANDLE thread = GetCurrentThread(); SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS); if (!SymInitialize(process, NULL, TRUE)) { fprintf(stderr, "\n(SymInitialize failed — no stack trace available)\n"); fprintf(stderr, "=== END CRASH ===\n"); fflush(stderr); return EXCEPTION_EXECUTE_HANDLER; } STACKFRAME64 frame = {}; DWORD machineType; #ifdef _M_X64 machineType = IMAGE_FILE_MACHINE_AMD64; frame.AddrPC.Offset = ctxCopy.Rip; frame.AddrFrame.Offset = ctxCopy.Rbp; frame.AddrStack.Offset = ctxCopy.Rsp; #else machineType = IMAGE_FILE_MACHINE_I386; frame.AddrPC.Offset = ctxCopy.Eip; frame.AddrFrame.Offset = ctxCopy.Ebp; frame.AddrStack.Offset = ctxCopy.Esp; #endif frame.AddrPC.Mode = AddrModeFlat; frame.AddrFrame.Mode = AddrModeFlat; frame.AddrStack.Mode = AddrModeFlat; fprintf(stderr, "\nStack trace:\n"); for (int i = 0; i < 64; i++) { if (!StackWalk64(machineType, process, thread, &frame, &ctxCopy, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) break; if (frame.AddrPC.Offset == 0) break; char buf[sizeof(SYMBOL_INFO) + 256]; SYMBOL_INFO* sym = reinterpret_cast(buf); sym->SizeOfStruct = sizeof(SYMBOL_INFO); sym->MaxNameLen = 255; DWORD64 disp64 = 0; DWORD disp32 = 0; IMAGEHLP_LINE64 line = {}; line.SizeOfStruct = sizeof(line); bool hasSym = SymFromAddr(process, frame.AddrPC.Offset, &disp64, sym); bool hasLine = SymGetLineFromAddr64(process, frame.AddrPC.Offset, &disp32, &line); if (hasSym && hasLine) { fprintf(stderr, " [%2d] %s+0x%llx (%s:%lu)\n", i, sym->Name, (unsigned long long)disp64, line.FileName, line.LineNumber); } else if (hasSym) { fprintf(stderr, " [%2d] %s+0x%llx\n", i, sym->Name, (unsigned long long)disp64); } else { fprintf(stderr, " [%2d] 0x%llx\n", i, (unsigned long long)frame.AddrPC.Offset); } } SymCleanup(process); fprintf(stderr, "=== END CRASH ===\n"); fflush(stderr); return EXCEPTION_EXECUTE_HANDLER; } #endif class DarkApp : public QApplication { public: using QApplication::QApplication; bool notify(QObject* receiver, QEvent* event) override { if (event->type() == QEvent::WindowActivate && receiver->isWidgetType()) { auto* w = static_cast(receiver); if ((w->windowFlags() & Qt::Window) == Qt::Window && !w->property("DarkTitleBar").toBool()) { w->setProperty("DarkTitleBar", true); #ifdef _WIN32 setDarkTitleBar(w); #endif } } return QApplication::notify(receiver, event); } }; class MenuBarStyle : public QProxyStyle { public: using QProxyStyle::QProxyStyle; QSize sizeFromContents(ContentsType type, const QStyleOption* opt, const QSize& sz, const QWidget* w) const override { QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w); if (type == CT_MenuBarItem) s.setHeight(s.height() + qRound(s.height() * 0.5)); if (type == CT_MenuItem) s = QSize(s.width() + 24, s.height() + 4); return s; } int pixelMetric(PixelMetric metric, const QStyleOption* opt, const QWidget* w) const override { // Kill the 1px frame margin Fusion reserves around QMenu contents if (metric == PM_MenuPanelWidth) return 0; // Thin draggable separator between dock widgets / central widget if (metric == PM_DockWidgetSeparatorExtent) return 1; return QProxyStyle::pixelMetric(metric, opt, w); } void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt, QPainter* p, const QWidget* w) const override { // Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough if (elem == PE_FrameMenu) return; // Kill the status bar item frame and panel border if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar) return; // Transparent menu bar background (no CSS needed) if (elem == PE_PanelMenuBar) return; // Item-view row background — patch Highlight so the row bg matches CE_ItemViewItem if (elem == PE_PanelItemViewRow) { if (auto* vi = qstyleoption_cast(opt)) { QStyleOptionViewItem patched = *vi; patched.palette.setColor(QPalette::Highlight, vi->palette.color(QPalette::Mid)); QProxyStyle::drawPrimitive(elem, &patched, p, w); return; } } QProxyStyle::drawPrimitive(elem, opt, p, w); } void drawControl(ControlElement element, const QStyleOption* opt, QPainter* p, const QWidget* w) const override { // Suppress Fusion's CE_MenuBarEmptyArea — it fills with palette.window() // bypassing PE_PanelMenuBar. TitleBarWidget paints the background. if (element == CE_MenuBarEmptyArea) return; // Menu bar items — fully owned painting (Fusion fills full rect, hiding border) if (element == CE_MenuBarItem) { if (auto* mi = qstyleoption_cast(opt)) { QRect area = mi->rect.adjusted(0, 0, 0, -1); // leave 1px for border bool selected = mi->state & State_Selected; bool sunken = mi->state & State_Sunken; // Only fill background for hover/pressed — non-hovered stays // transparent so the parent's border line shows through. if (sunken) p->fillRect(area, mi->palette.color(QPalette::Mid).darker(130)); else if (selected) p->fillRect(area, mi->palette.color(QPalette::Mid)); QColor fg = (selected || sunken) ? mi->palette.color(QPalette::Link) : mi->palette.color(QPalette::ButtonText); p->setPen(fg); p->drawText(area, Qt::AlignCenter | Qt::TextShowMnemonic, mi->text); return; // never delegate to Fusion } } // Popup menu items — palette patch then delegate to Fusion if (element == CE_MenuItem) { if (auto* mi = qstyleoption_cast(opt)) { if ((mi->state & State_Selected) && mi->menuItemType != QStyleOptionMenuItem::Separator) { QStyleOptionMenuItem patched = *mi; patched.palette.setColor(QPalette::Highlight, mi->palette.color(QPalette::Mid)); // theme.hover patched.palette.setColor(QPalette::HighlightedText, mi->palette.color(QPalette::Link)); // theme.indHoverSpan QProxyStyle::drawControl(element, &patched, p, w); return; } } } // Item views — visible hover + themed selection (Fusion's hover is invisible on dark bg) if (element == CE_ItemViewItem) { if (auto* vi = qstyleoption_cast(opt)) { bool hovered = vi->state & State_MouseOver; bool selected = vi->state & State_Selected; if (hovered && !selected) p->fillRect(vi->rect, vi->palette.color(QPalette::Mid)); QStyleOptionViewItem patched = *vi; patched.palette.setColor(QPalette::Highlight, vi->palette.color(QPalette::Mid)); // theme.hover patched.palette.setColor(QPalette::HighlightedText, vi->palette.color(QPalette::Text)); QProxyStyle::drawControl(element, &patched, p, w); return; } } QProxyStyle::drawControl(element, opt, p, w); } }; static void applyGlobalTheme(const rcx::Theme& theme) { QPalette pal; pal.setColor(QPalette::Window, theme.background); pal.setColor(QPalette::WindowText, theme.text); pal.setColor(QPalette::Base, theme.background); pal.setColor(QPalette::AlternateBase, theme.surface); pal.setColor(QPalette::Text, theme.text); pal.setColor(QPalette::Button, theme.button); pal.setColor(QPalette::ButtonText, theme.text); pal.setColor(QPalette::Highlight, theme.hover); pal.setColor(QPalette::HighlightedText, theme.text); pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt); pal.setColor(QPalette::ToolTipText, theme.text); pal.setColor(QPalette::Mid, theme.hover); pal.setColor(QPalette::Dark, theme.background); pal.setColor(QPalette::Light, theme.textFaint); pal.setColor(QPalette::Link, theme.indHoverSpan); // Disabled group: Fusion reads these for disabled menu items, buttons, etc. pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted); pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted); pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted); pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted); pal.setColor(QPalette::Disabled, QPalette::Light, theme.background); qApp->setPalette(pal); qApp->setStyleSheet(QString()); } class BorderOverlay : public QWidget { public: QColor color; explicit BorderOverlay(QWidget* parent) : QWidget(parent) { setAttribute(Qt::WA_TransparentForMouseEvents); setAttribute(Qt::WA_NoSystemBackground); setFocusPolicy(Qt::NoFocus); } void paintEvent(QPaintEvent*) override { QPainter p(this); p.setPen(color); p.drawRect(0, 0, width() - 1, height() - 1); } }; namespace rcx { #ifdef __APPLE__ void applyMacTitleBarTheme(QWidget* window, const Theme& theme); #endif // MainWindow class declaration is in mainwindow.h MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowTitle("Reclass"); resize(1200, 800); #ifndef __APPLE__ // Frameless window with system menu (Alt+Space) and min/max/close support. setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint); // Custom title bar (replaces native menu bar area in QMainWindow) m_titleBar = new TitleBarWidget(this); m_titleBar->applyTheme(ThemeManager::instance().current()); setMenuWidget(m_titleBar); m_menuBar = m_titleBar->menuBar(); #else setWindowTitle(QStringLiteral("Reclass")); setUnifiedTitleAndToolBarOnMac(true); m_menuBar = menuBar(); m_menuBar->setNativeMenuBar(true); applyMacTitleBarTheme(this, ThemeManager::instance().current()); #endif #ifdef _WIN32 // 1px top margin preserves DWM drop shadow on the frameless window { auto hwnd = reinterpret_cast(winId()); MARGINS margins = {0, 0, 1, 0}; DwmExtendFrameIntoClientArea(hwnd, &margins); } #endif // Border overlay — draws a 1px colored border on top of everything auto* overlay = new BorderOverlay(this); m_borderOverlay = overlay; overlay->color = ThemeManager::instance().current().borderFocused; overlay->setGeometry(rect()); overlay->raise(); overlay->show(); m_mdiArea = new QMdiArea(this); m_mdiArea->setFrameShape(QFrame::NoFrame); m_mdiArea->setViewMode(QMdiArea::TabbedView); m_mdiArea->setTabsClosable(true); m_mdiArea->setTabsMovable(true); { const auto& t = ThemeManager::instance().current(); m_mdiArea->setStyleSheet(QStringLiteral( "QTabBar::tab {" " background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;" "}" "QTabBar::tab:selected { color: %3; background: %4; }" "QTabBar::tab:hover { color: %3; background: %5; }") .arg(t.background.name(), t.textMuted.name(), t.text.name(), t.backgroundAlt.name(), t.hover.name())); } setCentralWidget(m_mdiArea); createWorkspaceDock(); createScannerDock(); createMenus(); createStatusBar(); // Eliminate gap between central widget and status bar if (auto* ml = layout()) { ml->setSpacing(0); ml->setContentsMargins(0, 0, 0, 0); } // Separator line between central widget and status bar is killed in MenuBarStyle::drawControl // Restore menu bar title case setting (after menus are created) { QSettings s("Reclass", "Reclass"); m_menuBarTitleCase = s.value("menuBarTitleCase", false).toBool(); applyMenuBarTitleCase(m_menuBarTitleCase); if (m_titleBar && s.value("showIcon", false).toBool()) m_titleBar->setShowIcon(true); } // MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, &MainWindow::applyTheme); // Load plugins m_pluginManager.LoadPlugins(); // Start MCP bridge m_mcp = new McpBridge(this, this); if (QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool()) m_mcp->start(); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, [this](QMdiSubWindow*) { updateWindowTitle(); rebuildWorkspaceModel(); }); // Track which split pane has focus (for menu-driven view switching) connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* now) { if (!now) return; auto* tab = activeTab(); if (!tab) return; for (int i = 0; i < tab->panes.size(); ++i) { if (tab->panes[i].tabWidget && tab->panes[i].tabWidget->isAncestorOf(now)) { tab->activePaneIdx = i; syncViewButtons(tab->panes[i].viewMode); return; } } }); } QIcon MainWindow::makeIcon(const QString& svgPath) { return QIcon(svgPath); } template < typename...Args > inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequence &shortcut, const QIcon &icon, Args&&...args) { QAction *result = menu->addAction(icon, text); if (!shortcut.isEmpty()) result->setShortcut(shortcut); QObject::connect(result, &QAction::triggered, std::forward(args)...); return result; } void MainWindow::applyMenuBarTitleCase(bool titleCase) { m_menuBarTitleCase = titleCase; if (m_titleBar) { m_titleBar->setMenuBarTitleCase(titleCase); return; } if (!m_menuBar) return; for (QAction* action : m_menuBar->actions()) { QString text = action->text(); QString clean = text; clean.remove('&'); if (titleCase) { action->setText("&" + clean.toUpper()); } else { QString result; bool capitalizeNext = true; for (int i = 0; i < clean.length(); ++i) { QChar ch = clean[i]; if (ch.isLetter()) { result += capitalizeNext ? ch.toUpper() : ch.toLower(); capitalizeNext = false; } else { result += ch; if (ch.isSpace()) capitalizeNext = true; } } action->setText("&" + result); } } } void MainWindow::createMenus() { // File auto* file = m_menuBar->addMenu("&File"); Qt5Qt6AddAction(file, "New &Class", QKeySequence::New, QIcon(), this, &MainWindow::newClass); Qt5Qt6AddAction(file, "New &Struct", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newStruct); Qt5Qt6AddAction(file, "New &Enum", QKeySequence(Qt::CTRL | Qt::Key_E), QIcon(), this, &MainWindow::newEnum); Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile); m_recentFilesMenu = file->addMenu("Recent &Files"); updateRecentFilesMenu(); file->addSeparator(); Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile); Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs); file->addSeparator(); auto* importMenu = file->addMenu("&Import"); Qt5Qt6AddAction(importMenu, "From &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource); Qt5Qt6AddAction(importMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml); Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb); auto* exportMenu = file->addMenu("E&xport"); Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp); Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction); // Examples submenu — scan once at init { #ifdef __APPLE__ QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples")); #else QDir exDir(QCoreApplication::applicationDirPath() + "/examples"); #endif QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name); if (!rcxFiles.isEmpty()) { auto* examples = file->addMenu("E&xamples"); for (const QString& fn : rcxFiles) { QString fullPath = exDir.absoluteFilePath(fn); examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); }); } } } file->addSeparator(); Qt5Qt6AddAction(file, "&Close Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile); file->addSeparator(); Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close); // Edit auto* edit = m_menuBar->addMenu("&Edit"); Qt5Qt6AddAction(edit, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo); Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo); // View auto* view = m_menuBar->addMenu("&View"); Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView); m_removeSplitAction = Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView); m_removeSplitAction->setVisible(false); view->addSeparator(); connect(view, &QMenu::aboutToShow, this, [this]() { auto* tab = activeTab(); m_removeSplitAction->setVisible(tab && tab->panes.size() > 1); }); m_sourceMenu = view->addMenu("&Data Source"); connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu); view->addSeparator(); auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font"); auto* fontGroup = new QActionGroup(this); fontGroup->setExclusive(true); auto* actConsolas = fontMenu->addAction("Consolas"); actConsolas->setCheckable(true); actConsolas->setActionGroup(fontGroup); auto* actJetBrains = fontMenu->addAction("JetBrains Mono"); actJetBrains->setCheckable(true); actJetBrains->setActionGroup(fontGroup); // Load saved preference QSettings settings("Reclass", "Reclass"); QString savedFont = settings.value("font", "JetBrains Mono").toString(); if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true); else actConsolas->setChecked(true); connect(actConsolas, &QAction::triggered, this, [this]() { setEditorFont("Consolas"); }); connect(actJetBrains, &QAction::triggered, this, [this]() { setEditorFont("JetBrains Mono"); }); // Theme submenu auto* themeMenu = view->addMenu("&Theme"); auto* themeGroup = new QActionGroup(this); themeGroup->setExclusive(true); auto& tm = ThemeManager::instance(); auto allThemes = tm.themes(); for (int i = 0; i < allThemes.size(); i++) { auto* act = themeMenu->addAction(allThemes[i].name); act->setCheckable(true); act->setActionGroup(themeGroup); if (i == tm.currentIndex()) act->setChecked(true); connect(act, &QAction::triggered, this, [i]() { ThemeManager::instance().setCurrent(i); }); } themeMenu->addSeparator(); Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme); view->addSeparator(); auto* actCompact = view->addAction("Compact &Columns"); actCompact->setCheckable(true); actCompact->setChecked(settings.value("compactColumns", true).toBool()); connect(actCompact, &QAction::triggered, this, [this](bool checked) { QSettings("Reclass", "Reclass").setValue("compactColumns", checked); for (auto& tab : m_tabs) tab.ctrl->setCompactColumns(checked); }); auto* actRelOfs = view->addAction("R&elative Offsets"); actRelOfs->setCheckable(true); actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool()); connect(actRelOfs, &QAction::triggered, this, [this](bool checked) { QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked); for (auto& tab : m_tabs) for (auto& pane : tab.panes) pane.editor->setRelativeOffsets(checked); }); view->addSeparator(); view->addAction(m_workspaceDock->toggleViewAction()); { auto* scanAct = m_scannerDock->toggleViewAction(); scanAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S)); view->addAction(scanAct); } // Tools auto* tools = m_menuBar->addMenu("&Tools"); Qt5Qt6AddAction(tools, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog); tools->addSeparator(); const auto mcpName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server"; m_mcpAction = Qt5Qt6AddAction(tools, mcpName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp); tools->addSeparator(); Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog); // Plugins auto* plugins = m_menuBar->addMenu("&Plugins"); Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog); // Help auto* help = m_menuBar->addMenu("&Help"); Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about); } // ── Themed resize grip (replaces ugly default QSizeGrip) ── // Positioned as a direct child of MainWindow at the bottom-right corner, // NOT inside the status bar layout (which is font-height dependent). class ResizeGrip : public QWidget { public: static constexpr int kSize = 16; // widget size static constexpr int kPad = 4; // padding from window corner (identical right & bottom) explicit ResizeGrip(QWidget* parent) : QWidget(parent) { setFixedSize(kSize, kSize); setCursor(Qt::SizeFDiagCursor); m_color = rcx::ThemeManager::instance().current().textFaint; } void setGripColor(const QColor& c) { m_color = c; update(); } // Call from parent's resizeEvent to pin to bottom-right corner void reposition() { QWidget* w = parentWidget(); if (w) move(w->width() - kSize - kPad, w->height() - kSize - kPad); } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); p.setPen(Qt::NoPen); p.setBrush(m_color); // 6 dots in a triangle pointing bottom-right (VS2022 style) // Dot grid is centered within the widget: same inset from right and bottom const double r = 1.0, s = 4.0; const double inset = 4.0; double bx = width() - inset; double by = height() - inset; // bottom row: 3 dots p.drawEllipse(QPointF(bx, by), r, r); p.drawEllipse(QPointF(bx - s, by), r, r); p.drawEllipse(QPointF(bx - 2 * s, by), r, r); // middle row: 2 dots p.drawEllipse(QPointF(bx, by - s), r, r); p.drawEllipse(QPointF(bx - s, by - s), r, r); // top row: 1 dot p.drawEllipse(QPointF(bx, by - 2 * s), r, r); } void mousePressEvent(QMouseEvent* e) override { if (e->button() == Qt::LeftButton) { window()->windowHandle()->startSystemResize(Qt::BottomEdge | Qt::RightEdge); e->accept(); } } private: QColor m_color; }; // ── Custom-painted view tab button (no CSS) ── class ViewTabButton : public QPushButton { public: static constexpr int kAccentH = 3; // accent line height in pixels static constexpr int kPadLR = 12; // horizontal padding static constexpr int kPadBot = 4; // extra bottom padding int baselineY = -1; // set by FlatStatusBar for cross-widget text alignment QColor colBg, colBgChecked, colBgHover, colBgPressed; QColor colText, colTextMuted, colAccent, colBorder; explicit ViewTabButton(const QString& text, QWidget* parent = nullptr) : QPushButton(text, parent) { setCheckable(true); setFlat(true); setCursor(Qt::PointingHandCursor); setContentsMargins(0, 0, 0, 0); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored); } QSize sizeHint() const override { QFontMetrics fm(font()); int w = fm.horizontalAdvance(text()) + 2 * kPadLR; int h = qRound((fm.height() + kAccentH + kPadBot) * 1.33); return QSize(w, h); } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); // Background QColor bg = colBg; if (isDown()) bg = colBgPressed; else if (underMouse()) bg = colBgHover; else if (isChecked()) bg = colBgChecked; p.fillRect(rect(), bg); // Top border (continuous with status bar hairline) if (colBorder.isValid()) p.fillRect(0, 0, width(), 1, colBorder); // Accent line at y=0 when checked (paints over border) if (isChecked()) p.fillRect(0, 0, width(), kAccentH, colAccent); // Text — use shared baseline if set, otherwise fall back to VCenter p.setPen(isChecked() || underMouse() || isDown() ? colText : colTextMuted); p.setFont(font()); if (baselineY >= 0) { p.drawText(kPadLR, baselineY, text()); } else { QRect textRect(kPadLR, kAccentH, width() - 2 * kPadLR, height() - kAccentH); p.drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, text()); } } void enterEvent(QEnterEvent*) override { update(); } void leaveEvent(QEvent*) override { update(); } }; // ── Shimmer label — gradient text sweep for MCP activity ── class ShimmerLabel : public QWidget { public: explicit ShimmerLabel(QWidget* parent = nullptr) : QWidget(parent) { m_timer.setInterval(30); connect(&m_timer, &QTimer::timeout, this, [this]() { m_phase += 0.012f; if (m_phase > 1.0f) m_phase -= 1.0f; update(); }); } void setText(const QString& t) { m_text = t; update(); } QString text() const { return m_text; } void setShimmerActive(bool on) { if (m_shimmer == on) return; m_shimmer = on; if (on) { m_phase = 0.0f; m_timer.start(); } else { m_timer.stop(); } update(); } bool shimmerActive() const { return m_shimmer; } void setAlignment(Qt::Alignment a) { m_align = a; update(); } // Colours configurable from theme QColor colBase; // dim text (normal) QColor colBright; // highlight sweep protected: void paintEvent(QPaintEvent*) override { if (m_text.isEmpty()) return; QPainter p(this); p.setRenderHint(QPainter::TextAntialiasing); p.setFont(font()); QRect r = contentsRect(); if (!m_shimmer) { QColor c = colBase.isValid() ? colBase : palette().color(QPalette::WindowText); p.setPen(c); p.drawText(r, m_align, m_text); return; } // Shimmer: sweeping glow band behind text + bright text QColor bright = colBright.isValid() ? colBright : QColor(255, 200, 80); // 1. Sweeping glow band (semi-transparent background highlight) qreal bandW = width() * 0.20; qreal bandCenter = -bandW + (width() + 2 * bandW) * m_phase; QLinearGradient bgGrad(bandCenter - bandW, 0, bandCenter + bandW, 0); QColor glow = bright; glow.setAlpha(35); bgGrad.setColorAt(0.0, Qt::transparent); bgGrad.setColorAt(0.5, glow); bgGrad.setColorAt(1.0, Qt::transparent); p.fillRect(rect(), QBrush(bgGrad)); // 2. Text in bright color p.setPen(bright); p.drawText(r, m_align, m_text); } private: QString m_text; bool m_shimmer = false; float m_phase = 0.0f; Qt::Alignment m_align = Qt::AlignLeft | Qt::AlignVCenter; QTimer m_timer; }; // ── Borderless status bar with manual child layout ── // QStatusBarLayout hardcodes 2px margins that can't be overridden. // We bypass it entirely: children are placed manually in resizeEvent, // and addWidget() is NOT used. Instead, create children as direct // children and call manualLayout() to position them. class FlatStatusBar : public QStatusBar { public: QWidget* tabRow = nullptr; // set by createStatusBar ShimmerLabel* label = nullptr; // set by createStatusBar void setDividerColor(const QColor& c) { m_div = c; update(); } void setTopLineColor(const QColor& c) { m_top = c; update(); } explicit FlatStatusBar(QWidget* parent = nullptr) : QStatusBar(parent) { setSizeGripEnabled(false); } QSize sizeHint() const override { const int tabH = tabRow ? tabRow->sizeHint().height() : 0; const int textH = fontMetrics().height(); const int base = qMax(tabH, textH + 6); const int h = qRound(base * 1.15); return { QStatusBar::sizeHint().width(), h }; } QSize minimumSizeHint() const override { return sizeHint(); } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.fillRect(rect(), palette().window()); // Top hairline separator if (m_top.isValid()) p.fillRect(0, 0, width(), 1, m_top); // Vertical divider between tabRow and label if (m_div.isValid() && m_divX >= 0) p.fillRect(m_divX, 4, 1, height() - 8, m_div); } void resizeEvent(QResizeEvent* e) override { QStatusBar::resizeEvent(e); manualLayout(); } void showEvent(QShowEvent* e) override { QStatusBar::showEvent(e); manualLayout(); } private: QColor m_div, m_top; int m_divX = -1; void manualLayout() { if (!tabRow || !label) return; const int h = height(); const int tw = tabRow->sizeHint().width(); const int gutter = 6; tabRow->setGeometry(0, 0, tw, h); m_divX = tw; label->setGeometry(tw + 1 + gutter, 0, qMax(0, width() - (tw + 1 + gutter)), h); // Shared baseline so tab text and status text align. // Nudge up by half the accent-line height so text centres // in the visible area below the accent bar, not in the full bar. QFontMetrics fm(font()); int by = (h + fm.ascent()) / 2 - (ViewTabButton::kAccentH + 1) / 2; // Push baseline to buttons auto* lay = tabRow->layout(); if (lay) { for (int i = 0; i < lay->count(); i++) static_cast(lay->itemAt(i)->widget())->baselineY = by; } // Align label: set top margin so text baseline matches int labelTop = by - fm.ascent(); label->setContentsMargins(0, labelTop, 0, 0); label->setAlignment(Qt::AlignLeft | Qt::AlignTop); } }; void MainWindow::createStatusBar() { // Replace the default QStatusBar with our borderless, manually-laid-out one. // QStatusBarLayout hardcodes 2px margins; we bypass addWidget entirely. auto* sb = new FlatStatusBar; setStatusBar(sb); m_statusLabel = new ShimmerLabel(sb); m_statusLabel->setText(""); m_statusLabel->setContentsMargins(0, 0, 0, 0); m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); // View toggle buttons (Reclass / C/C++) — custom painted, no CSS m_viewBtnGroup = new QButtonGroup(this); m_viewBtnGroup->setExclusive(true); m_btnReclass = new ViewTabButton("Reclass"); m_btnReclass->setChecked(true); m_btnRendered = new ViewTabButton("C/C++"); m_viewBtnGroup->addButton(m_btnReclass, 0); m_viewBtnGroup->addButton(m_btnRendered, 1); // Wrap buttons in a plain container — FlatStatusBar paints the chrome auto* tabRow = new QWidget(sb); auto* tabLay = new QHBoxLayout(tabRow); tabLay->setContentsMargins(0, 0, 0, 0); tabLay->setSpacing(0); tabLay->addWidget(m_btnReclass); tabLay->addWidget(m_btnRendered); sb->tabRow = tabRow; sb->label = m_statusLabel; sb->setMinimumHeight(qMax(m_btnReclass->sizeHint().height(), sb->fontMetrics().height() + 6)); connect(m_viewBtnGroup, &QButtonGroup::idClicked, this, [this](int id) { setViewMode(id == 1 ? VM_Rendered : VM_Reclass); }); // Grip is a direct child of the main window, NOT in the status bar layout. // Positioned via reposition() in resizeEvent — immune to font/margin changes. auto* grip = new ResizeGrip(this); grip->setObjectName("resizeGrip"); grip->raise(); { const auto& t = ThemeManager::instance().current(); QPalette sbPal = statusBar()->palette(); sbPal.setColor(QPalette::Window, t.background); sbPal.setColor(QPalette::WindowText, t.textDim); statusBar()->setPalette(sbPal); statusBar()->setAutoFillBackground(true); sb->setTopLineColor(t.border); sb->setDividerColor(t.border); auto applyViewTabColors = [&](ViewTabButton* btn) { btn->colBg = t.background; btn->colBgChecked = t.backgroundAlt; btn->colBgHover = t.hover; btn->colBgPressed = t.hover.darker(130); btn->colText = t.text; btn->colTextMuted = t.textMuted; btn->colAccent = t.indHoverSpan; btn->colBorder = t.border; }; applyViewTabColors(static_cast(m_btnReclass)); applyViewTabColors(static_cast(m_btnRendered)); m_statusLabel->colBase = t.textDim; m_statusLabel->colBright = t.indHoverSpan; } } void MainWindow::setAppStatus(const QString& text) { m_appStatus = text; if (!m_mcpBusy) { m_statusLabel->setText(text); m_statusLabel->setShimmerActive(false); } } void MainWindow::setMcpStatus(const QString& text) { // Cancel any pending clear — new activity extends the shimmer if (m_mcpClearTimer) m_mcpClearTimer->stop(); m_mcpBusy = true; m_statusLabel->setText(text); m_statusLabel->setShimmerActive(true); } void MainWindow::clearMcpStatus() { // Delay the clear so the shimmer stays visible for at least 750ms if (!m_mcpClearTimer) { m_mcpClearTimer = new QTimer(this); m_mcpClearTimer->setSingleShot(true); connect(m_mcpClearTimer, &QTimer::timeout, this, [this]() { m_mcpBusy = false; m_statusLabel->setText(m_appStatus); m_statusLabel->setShimmerActive(false); }); } m_mcpClearTimer->start(750); } void MainWindow::styleTabCloseButtons() { auto* tabBar = m_mdiArea->findChild(); if (!tabBar) return; const auto& t = ThemeManager::instance().current(); QString style = QStringLiteral( "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" "QToolButton:hover { color: %2; }") .arg(t.textDim.name(), t.indHoverSpan.name()); auto subs = m_mdiArea->subWindowList(); for (int i = 0; i < tabBar->count() && i < subs.size(); i++) { auto* existing = qobject_cast( tabBar->tabButton(i, QTabBar::RightSide)); if (existing && existing->text() == QStringLiteral("\u2715")) { // Already our button, just restyle existing->setStyleSheet(style); continue; } // Replace with ✕ text button auto* btn = new QToolButton(tabBar); btn->setText(QStringLiteral("\u2715")); btn->setAutoRaise(true); btn->setCursor(Qt::PointingHandCursor); btn->setStyleSheet(style); QMdiSubWindow* sub = subs[i]; connect(btn, &QToolButton::clicked, sub, &QMdiSubWindow::close); tabBar->setTabButton(i, QTabBar::RightSide, btn); } } MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { SplitPane pane; pane.tabWidget = new QTabWidget; pane.tabWidget->setTabPosition(QTabWidget::South); pane.tabWidget->tabBar()->setVisible(false); pane.tabWidget->setDocumentMode(true); // kill QTabWidget frame border // Create editor via controller (parent = tabWidget for ownership) pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget); pane.editor->setRelativeOffsets( QSettings("Reclass", "Reclass").value("relativeOffsets", true).toBool()); pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0 // Create per-pane rendered C++ view with find bar pane.renderedContainer = new QWidget; auto* rvLayout = new QVBoxLayout(pane.renderedContainer); rvLayout->setContentsMargins(0, 0, 0, 0); rvLayout->setSpacing(0); pane.rendered = new QsciScintilla; setupRenderedSci(pane.rendered); rvLayout->addWidget(pane.rendered); // Find bar with prev/next buttons (hidden by default) pane.findContainer = new QWidget; auto* fcLayout = new QHBoxLayout(pane.findContainer); fcLayout->setContentsMargins(4, 0, 0, 0); fcLayout->setSpacing(2); const auto& fbTheme = ThemeManager::instance().current(); auto* ccPrevBtn = new QToolButton; ccPrevBtn->setText(QStringLiteral("\u25C0")); ccPrevBtn->setFixedSize(24, 24); auto* ccNextBtn = new QToolButton; ccNextBtn->setText(QStringLiteral("\u25B6")); ccNextBtn->setFixedSize(24, 24); auto* ccCloseBtn = new QToolButton; ccCloseBtn->setText(QStringLiteral("\u2715")); ccCloseBtn->setFixedSize(24, 24); QString btnCss = QStringLiteral( "QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }" "QToolButton:hover { background: %4; }" "QToolButton:pressed { background: %5; }") .arg(fbTheme.background.name(), fbTheme.text.name(), fbTheme.border.name(), fbTheme.hover.name(), fbTheme.backgroundAlt.name()); ccPrevBtn->setStyleSheet(btnCss); ccNextBtn->setStyleSheet(btnCss); ccCloseBtn->setStyleSheet(btnCss); pane.findBar = new QLineEdit; pane.findBar->setPlaceholderText("Find..."); pane.findBar->setStyleSheet( QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;" " padding: 4px 8px; font-size: 13px; }") .arg(fbTheme.backgroundAlt.name(), fbTheme.text.name(), fbTheme.border.name())); fcLayout->addWidget(ccPrevBtn); fcLayout->addWidget(ccNextBtn); fcLayout->addWidget(ccCloseBtn); fcLayout->addWidget(pane.findBar); pane.findContainer->setVisible(false); rvLayout->addWidget(pane.findContainer); // Ctrl+F to show find bar QsciScintilla* sci = pane.rendered; QLineEdit* fb = pane.findBar; QWidget* fc = pane.findContainer; auto* findAction = new QAction(pane.renderedContainer); findAction->setShortcut(QKeySequence::Find); findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); pane.renderedContainer->addAction(findAction); connect(findAction, &QAction::triggered, fb, [fb, fc]() { fc->setVisible(true); fb->setFocus(); fb->selectAll(); }); // Escape to hide find bar auto* escAction = new QAction(fb); escAction->setShortcut(QKeySequence(Qt::Key_Escape)); escAction->setShortcutContext(Qt::WidgetShortcut); fb->addAction(escAction); connect(escAction, &QAction::triggered, fb, [fc, sci]() { fc->setVisible(false); sci->setFocus(); }); // Search on text change and Enter connect(fb, &QLineEdit::textChanged, sci, [sci](const QString& text) { if (text.isEmpty()) return; sci->findFirst(text, false, false, false, true, true, 0, 0); }); connect(fb, &QLineEdit::returnPressed, sci, [sci, fb]() { QString text = fb->text(); if (text.isEmpty()) return; if (!sci->findNext()) sci->findFirst(text, false, false, false, true, true, 0, 0); }); connect(ccNextBtn, &QToolButton::clicked, sci, [sci, fb]() { if (!sci->findNext()) sci->findFirst(fb->text(), false, false, false, true, true, 0, 0); }); connect(ccPrevBtn, &QToolButton::clicked, sci, [sci, fb]() { QString text = fb->text(); if (text.isEmpty()) return; int line, col; sci->getCursorPosition(&line, &col); sci->findFirst(text, false, false, false, true, false, line, col); }); connect(ccCloseBtn, &QToolButton::clicked, sci, [fc, sci]() { fc->setVisible(false); sci->setFocus(); }); pane.tabWidget->addTab(pane.renderedContainer, "C/C++"); // index 1 pane.tabWidget->setCurrentIndex(0); pane.viewMode = VM_Reclass; // Add to splitter tab.splitter->addWidget(pane.tabWidget); // Connect per-pane page switching (driven by status bar buttons via setViewMode) QTabWidget* tw = pane.tabWidget; connect(tw, &QTabWidget::currentChanged, this, [this, tw](int index) { SplitPane* p = findPaneByTabWidget(tw); if (!p) return; p->viewMode = (index == 1) ? VM_Rendered : VM_Reclass; // Sync status bar buttons if this is the active pane auto* tab = activeTab(); if (tab && &tab->panes[tab->activePaneIdx] == p) syncViewButtons(p->viewMode); if (index == 1) { for (auto& tab : m_tabs) { for (auto& pane : tab.panes) { if (&pane == p) { updateRenderedView(tab, pane); break; } } } } }); return pane; } MainWindow::SplitPane* MainWindow::findPaneByTabWidget(QTabWidget* tw) { for (auto& tab : m_tabs) { for (auto& pane : tab.panes) { if (pane.tabWidget == tw) return &pane; } } return nullptr; } MainWindow::SplitPane* MainWindow::findActiveSplitPane() { auto* tab = activeTab(); if (!tab || tab->panes.isEmpty()) return nullptr; int idx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1); return &tab->panes[idx]; } RcxEditor* MainWindow::activePaneEditor() { auto* pane = findActiveSplitPane(); return pane ? pane->editor : nullptr; } static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) { if (viewRootId != 0) { int idx = tree.indexOfId(viewRootId); if (idx >= 0) { const auto& n = tree.nodes[idx]; if (!n.structTypeName.isEmpty()) return n.structTypeName; if (!n.name.isEmpty()) return n.name; } } for (const auto& n : tree.nodes) { if (n.parentId == 0 && n.kind == NodeKind::Struct) { if (!n.structTypeName.isEmpty()) return n.structTypeName; if (!n.name.isEmpty()) return n.name; } } return QStringLiteral("Untitled"); } QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { auto* splitter = new QSplitter(Qt::Horizontal); splitter->setHandleWidth(1); auto* ctrl = new RcxController(doc, splitter); auto* sub = m_mdiArea->addSubWindow(splitter); sub->setWindowIcon(QIcon()); // suppress app icon in MDI tabs sub->setWindowTitle(doc->filePath.isEmpty() ? rootName(doc->tree) : QFileInfo(doc->filePath).fileName()); sub->setAttribute(Qt::WA_DeleteOnClose); sub->showMaximized(); m_tabs[sub] = { doc, ctrl, splitter, {}, 0 }; auto& tab = m_tabs[sub]; // Create the initial split pane tab.panes.append(createSplitPane(tab)); // Apply global compact columns setting to new tab ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool()); // Give every controller the shared document list for cross-tab type visibility ctrl->setProjectDocuments(&m_allDocs); rebuildAllDocs(); connect(sub, &QObject::destroyed, this, [this, sub]() { auto it = m_tabs.find(sub); if (it != m_tabs.end()) { it->doc->deleteLater(); m_tabs.erase(it); } rebuildAllDocs(); rebuildWorkspaceModel(); }); connect(ctrl, &RcxController::nodeSelected, this, [this, ctrl, sub](int nodeIdx) { if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) { auto& node = ctrl->document()->tree.nodes[nodeIdx]; auto* ap = findActiveSplitPane(); if (ap && ap->viewMode == VM_Rendered) setAppStatus( QString("Rendered: %1 %2") .arg(kindToString(node.kind)) .arg(node.name)); else setAppStatus( QString("%1 %2 offset: 0x%3 size: %4 bytes") .arg(kindToString(node.kind)) .arg(node.name) .arg(node.offset, 4, 16, QChar('0')) .arg(node.byteSize())); } // Update all rendered panes on selection change auto it = m_tabs.find(sub); if (it != m_tabs.end()) updateAllRenderedPanes(*it); }); connect(ctrl, &RcxController::selectionChanged, this, [this](int count) { if (count > 1) setAppStatus(QString("%1 nodes selected").arg(count)); }); // Update rendered panes and workspace on document changes and undo/redo connect(doc, &RcxDocument::documentChanged, this, [this, sub]() { auto it = m_tabs.find(sub); if (it != m_tabs.end()) QTimer::singleShot(0, this, [this, sub]() { auto it2 = m_tabs.find(sub); if (it2 != m_tabs.end()) { updateAllRenderedPanes(*it2); if (it2->doc->filePath.isEmpty()) sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId())); } rebuildWorkspaceModel(); updateWindowTitle(); }); }); connect(&doc->undoStack, &QUndoStack::indexChanged, this, [this, sub](int) { auto it = m_tabs.find(sub); if (it != m_tabs.end()) QTimer::singleShot(0, this, [this, sub]() { auto it2 = m_tabs.find(sub); if (it2 != m_tabs.end()) { updateAllRenderedPanes(*it2); if (it2->doc->filePath.isEmpty()) sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId())); } updateWindowTitle(); rebuildWorkspaceModel(); }); }); // Auto-focus on first root struct (don't show all roots) for (const auto& n : doc->tree.nodes) { if (n.parentId == 0 && n.kind == NodeKind::Struct) { ctrl->setViewRootId(n.id); break; } } ctrl->refresh(); rebuildWorkspaceModel(); styleTabCloseButtons(); return sub; } // Build a minimal empty struct for new documents static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) { Node root; root.kind = NodeKind::Struct; root.name = "instance"; root.structTypeName = "Unnamed"; root.classKeyword = classKeyword; root.parentId = 0; root.offset = 0; int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; for (int i = 0; i < 16; i++) { Node n; n.kind = NodeKind::Hex64; n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0')); n.parentId = rootId; n.offset = i * 8; tree.addNode(n); } // Default project: add an example enum and a class with a union if (classKeyword.isEmpty()) { // ── Example enum: _POOL_TYPE ── { Node e; e.kind = NodeKind::Struct; e.name = QStringLiteral("_POOL_TYPE"); e.structTypeName = QStringLiteral("_POOL_TYPE"); e.classKeyword = QStringLiteral("enum"); e.parentId = 0; e.collapsed = false; e.enumMembers = { {QStringLiteral("NonPagedPool"), 0}, {QStringLiteral("PagedPool"), 1}, {QStringLiteral("NonPagedPoolMustSucceed"), 2}, {QStringLiteral("DontUseThisType"), 3}, {QStringLiteral("NonPagedPoolCacheAligned"), 4}, {QStringLiteral("PagedPoolCacheAligned"), 5}, }; tree.addNode(e); } } } MainWindow::~MainWindow() { /* * When MainWindow is destroyed: * * 1. ~MainWindow() runs (our code — plugin DLLs still loaded) * 2. MainWindow member variables are destroyed (m_pluginManager — unloads plugin DLLs) * 3. QObject::~QObject() runs — destroys child widgets (QMdiSubWindow → RcxController → ~RcxController()) * */ // Disconnect all subwindow destroyed signals before members are torn down, // so the lambdas capturing 'this' never fire on a half-destroyed object. for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) { disconnect(it.key(), &QObject::destroyed, this, nullptr); // Release providers now while plugin DLLs are still loaded; // if deferred to Qt child cleanup the DLL code may already be unloaded. it->doc->provider.reset(); it->ctrl->resetProvider(); } m_tabs.clear(); } void MainWindow::newClass() { project_new(QStringLiteral("class")); } void MainWindow::newStruct() { project_new(); } void MainWindow::newEnum() { project_new(QStringLiteral("enum")); } static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) { tree.nodes.clear(); tree.invalidateIdCache(); tree.m_nextId = 1; tree.baseAddress = static_cast(editorAddr); // ── Root struct: RcxEditor ── Node root; root.kind = NodeKind::Struct; root.name = QStringLiteral("editor"); root.structTypeName = QStringLiteral("RcxEditor"); root.classKeyword = QStringLiteral("class"); int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; // ── VTable struct definition (separate root) ── Node vtStruct; vtStruct.kind = NodeKind::Struct; vtStruct.name = QStringLiteral("VTable"); vtStruct.structTypeName = QStringLiteral("QWidgetVTable"); int vti = tree.addNode(vtStruct); uint64_t vtId = tree.nodes[vti].id; // VTable entries — these are real virtual function pointers from QObject/QWidget static const char* vfNames[] = { "deleting_dtor", "metaObject", "qt_metacast", "qt_metacall", "event", "eventFilter", "timerEvent", "childEvent", "customEvent", "connectNotify", "disconnectNotify", "devType", "setVisible", "sizeHint", "minimumSizeHint", "heightForWidth", }; for (int i = 0; i < 16; i++) { Node fn; fn.kind = NodeKind::FuncPtr64; fn.name = QString::fromLatin1(vfNames[i]); fn.parentId = vtId; fn.offset = i * 8; tree.addNode(fn); } // ── RcxEditor fields ── // offset 0: vtable pointer → QWidgetVTable { Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("__vptr"); n.parentId = rootId; n.offset = 0; n.refId = vtId; tree.addNode(n); } // offset 8: QObjectData* d_ptr (QObject internals) { Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("d_ptr"); n.parentId = rootId; n.offset = 8; tree.addNode(n); } // The rest of the object: raw memory visible as Hex64 fields // QWidget base is large (~200+ bytes), then RcxEditor members follow. // Lay out enough to cover the interesting editor state. for (int off = 16; off < 512; off += 8) { Node n; n.kind = NodeKind::Hex64; n.name = QStringLiteral("field_%1").arg(off, 3, 16, QLatin1Char('0')); n.parentId = rootId; n.offset = off; tree.addNode(n); } // ── Example enum: _POOL_TYPE ── { Node e; e.kind = NodeKind::Struct; e.name = QStringLiteral("_POOL_TYPE"); e.structTypeName = QStringLiteral("_POOL_TYPE"); e.classKeyword = QStringLiteral("enum"); e.parentId = 0; e.collapsed = false; e.enumMembers = { {QStringLiteral("NonPagedPool"), 0}, {QStringLiteral("PagedPool"), 1}, {QStringLiteral("NonPagedPoolMustSucceed"), 2}, {QStringLiteral("DontUseThisType"), 3}, {QStringLiteral("NonPagedPoolCacheAligned"), 4}, {QStringLiteral("PagedPoolCacheAligned"), 5}, }; tree.addNode(e); } } void MainWindow::selfTest() { #ifdef Q_OS_WIN // Tab 2: Editor demo with live process memory (created first) project_new(); auto* ctrl = activeController(); if (!ctrl || ctrl->editors().isEmpty()) return; auto* editor = ctrl->editors().first(); auto* doc = ctrl->document(); // Build a tree describing RcxEditor, based at the real object address buildEditorDemo(doc->tree, reinterpret_cast(editor)); // Attach process memory to self — provider base will be set to the editor address DWORD pid = GetCurrentProcessId(); QString target = QString("%1:Reclass.exe").arg(pid); ctrl->attachViaPlugin(QStringLiteral("processmemory"), target); // Tab 1: Empty class for user work (created second, becomes active) auto* userTab = project_new(QStringLiteral("class")); m_mdiArea->setActiveSubWindow(userTab); #else project_new(); auto* userTab = project_new(QStringLiteral("class")); m_mdiArea->setActiveSubWindow(userTab); #endif } void MainWindow::openFile() { project_open(); } void MainWindow::saveFile() { project_save(nullptr, false); } void MainWindow::saveFileAs() { project_save(nullptr, true); } void MainWindow::closeFile() { project_close(); } void MainWindow::addNode() { auto* ctrl = activeController(); if (!ctrl) return; uint64_t parentId = ctrl->viewRootId(); // default to current view root auto* primary = activePaneEditor(); if (primary && primary->isEditing()) return; if (primary) { int ni = primary->currentNodeIndex(); if (ni >= 0) { auto& node = ctrl->document()->tree.nodes[ni]; if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) parentId = node.id; else parentId = node.parentId; } } ctrl->insertNode(parentId, -1, NodeKind::Hex64, "newField"); } void MainWindow::removeNode() { auto* ctrl = activeController(); if (!ctrl) return; auto* primary = activePaneEditor(); if (primary && primary->isEditing()) return; QSet ids = ctrl->selectedIds(); QVector indices; for (uint64_t id : ids) { int idx = ctrl->document()->tree.indexOfId( id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask | kMemberBit | kMemberSubMask)); if (idx >= 0) indices.append(idx); } if (indices.size() > 1) ctrl->batchRemoveNodes(indices); else if (indices.size() == 1) ctrl->removeNode(indices.first()); } void MainWindow::changeNodeType() { auto* ctrl = activeController(); if (!ctrl) return; auto* primary = activePaneEditor(); if (!primary) return; primary->beginInlineEdit(EditTarget::Type); } void MainWindow::renameNodeAction() { auto* ctrl = activeController(); if (!ctrl) return; auto* primary = activePaneEditor(); if (!primary) return; primary->beginInlineEdit(EditTarget::Name); } void MainWindow::duplicateNodeAction() { auto* ctrl = activeController(); if (!ctrl) return; auto* primary = activePaneEditor(); if (!primary || primary->isEditing()) return; int ni = primary->currentNodeIndex(); if (ni >= 0) ctrl->duplicateNode(ni); } void MainWindow::splitView() { auto* tab = activeTab(); if (!tab) return; tab->panes.append(createSplitPane(*tab)); } void MainWindow::unsplitView() { auto* tab = activeTab(); if (!tab || tab->panes.size() <= 1) return; auto pane = tab->panes.takeLast(); tab->ctrl->removeSplitEditor(pane.editor); pane.tabWidget->deleteLater(); tab->activePaneIdx = qBound(0, tab->activePaneIdx, tab->panes.size() - 1); } void MainWindow::undo() { auto* tab = activeTab(); if (tab) tab->doc->undoStack.undo(); } void MainWindow::redo() { auto* tab = activeTab(); if (tab) tab->doc->undoStack.redo(); } void MainWindow::about() { QDialog dlg(this); dlg.setWindowTitle("About Reclass"); dlg.setFixedSize(260, 120); auto* lay = new QVBoxLayout(&dlg); lay->setContentsMargins(20, 16, 20, 16); lay->setSpacing(12); auto* buildLabel = new QLabel( QStringLiteral("" "Build " __DATE__ " " __TIME__ "") .arg(ThemeManager::instance().current().textDim.name())); buildLabel->setAlignment(Qt::AlignCenter); lay->addWidget(buildLabel); auto* ghBtn = new QPushButton("GitHub"); ghBtn->setCursor(Qt::PointingHandCursor); { const auto& t = ThemeManager::instance().current(); ghBtn->setStyleSheet(QStringLiteral( "QPushButton {" " background: %1; color: %2; border: 1px solid %3;" " border-radius: 4px; padding: 5px 16px; font-size: 12px;" "}" "QPushButton:hover { background: %4; border-color: %5; }") .arg(t.indCmdPill.name(), t.text.name(), t.border.name(), t.button.name(), t.textFaint.name())); } connect(ghBtn, &QPushButton::clicked, this, []() { QDesktopServices::openUrl(QUrl("https://github.com/IChooseYou/Reclass")); }); lay->addWidget(ghBtn, 0, Qt::AlignCenter); { QPalette dlgPal = dlg.palette(); dlgPal.setColor(QPalette::Window, ThemeManager::instance().current().background); dlg.setPalette(dlgPal); dlg.setAutoFillBackground(true); } dlg.exec(); } void MainWindow::toggleMcp() { if (m_mcp->isRunning()) { m_mcp->stop(); m_mcpAction->setText("Start &MCP Server"); setAppStatus("MCP server stopped"); } else { m_mcp->start(); m_mcpAction->setText("Stop &MCP Server"); setAppStatus("MCP server listening on pipe: ReclassMcpBridge"); } } void MainWindow::applyTheme(const Theme& theme) { applyGlobalTheme(theme); #ifdef __APPLE__ applyMacTitleBarTheme(this, theme); #endif // Dock separator is 1px via PM_DockWidgetSeparatorExtent in MenuBarStyle // Custom title bar if (m_titleBar) m_titleBar->applyTheme(theme); // Update border overlay color updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border); // MDI area tabs — text color + height handled by MenuBarStyle QProxyStyle m_mdiArea->setStyleSheet(QStringLiteral( "QTabBar::tab {" " background: %1; padding: 0px 16px; border: none;" "}" "QTabBar::tab:selected { background: %2; }" "QTabBar::tab:hover { background: %3; }") .arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.name())); // Dim MDI tab text via palette (Fusion reads WindowText, not CSS color:) if (auto* tabBar = m_mdiArea->findChild()) { QPalette tp = tabBar->palette(); tp.setColor(QPalette::WindowText, theme.textDim); tabBar->setPalette(tp); } // Re-style ✕ close buttons on MDI tabs styleTabCloseButtons(); // Status bar { QPalette sbPal = statusBar()->palette(); sbPal.setColor(QPalette::Window, theme.background); sbPal.setColor(QPalette::WindowText, theme.textDim); statusBar()->setPalette(sbPal); } // View toggle buttons + status bar chrome { auto applyColors = [&](ViewTabButton* btn) { btn->colBg = theme.background; btn->colBgChecked = theme.backgroundAlt; btn->colBgHover = theme.hover; btn->colBgPressed = theme.hover.darker(130); btn->colText = theme.text; btn->colTextMuted = theme.textMuted; btn->colAccent = theme.indHoverSpan; btn->colBorder = theme.border; btn->update(); }; applyColors(static_cast(m_btnReclass)); applyColors(static_cast(m_btnRendered)); { auto* fsb = static_cast(statusBar()); fsb->setTopLineColor(theme.border); fsb->setDividerColor(theme.border); } } // Resize grip (direct child of main window, not in status bar) if (auto* w = findChild("resizeGrip")) static_cast(w)->setGripColor(theme.textFaint); // Workspace tree: colors from theme (selection + text) if (m_workspaceTree) { QPalette tp = m_workspaceTree->palette(); tp.setColor(QPalette::Text, theme.textDim); tp.setColor(QPalette::Highlight, theme.hover); tp.setColor(QPalette::HighlightedText, theme.text); m_workspaceTree->setPalette(tp); } if (m_workspaceSearch) { m_workspaceSearch->setStyleSheet(QStringLiteral( "QLineEdit { background: %1; color: %2; border: none;" " border-bottom: 1px solid %3; padding: 4px 6px; }") .arg(theme.background.name(), theme.textDim.name(), theme.border.name())); } // Dock titlebar: restyle via palette + close button if (m_dockTitleLabel) { QPalette lp = m_dockTitleLabel->palette(); lp.setColor(QPalette::WindowText, theme.textDim); m_dockTitleLabel->setPalette(lp); } if (auto* titleBar = m_workspaceDock ? m_workspaceDock->titleBarWidget() : nullptr) { QPalette tbPal = titleBar->palette(); tbPal.setColor(QPalette::Window, theme.backgroundAlt); titleBar->setPalette(tbPal); } if (m_dockCloseBtn) m_dockCloseBtn->setStyleSheet(QStringLiteral( "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" "QToolButton:hover { color: %2; }") .arg(theme.textDim.name(), theme.indHoverSpan.name())); // Scanner dock if (m_scannerPanel) m_scannerPanel->applyTheme(theme); if (m_scanDockTitle) { QPalette lp = m_scanDockTitle->palette(); lp.setColor(QPalette::WindowText, theme.textDim); m_scanDockTitle->setPalette(lp); } if (auto* titleBar = m_scannerDock ? m_scannerDock->titleBarWidget() : nullptr) { QPalette tbPal = titleBar->palette(); tbPal.setColor(QPalette::Window, theme.backgroundAlt); titleBar->setPalette(tbPal); } if (m_scanDockCloseBtn) m_scanDockCloseBtn->setStyleSheet(QStringLiteral( "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" "QToolButton:hover { color: %2; }") .arg(theme.textDim.name(), theme.indHoverSpan.name())); // Rendered C/C++ views: update lexer colors, paper, margins for (auto& tab : m_tabs) { for (auto& pane : tab.panes) { auto* sci = pane.rendered; if (!sci) continue; if (auto* lexer = qobject_cast(sci->lexer())) { lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword); lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::KeywordSet2); lexer->setColor(theme.syntaxNumber, QsciLexerCPP::Number); lexer->setColor(theme.syntaxString, QsciLexerCPP::DoubleQuotedString); lexer->setColor(theme.syntaxString, QsciLexerCPP::SingleQuotedString); lexer->setColor(theme.syntaxComment, QsciLexerCPP::Comment); lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentLine); lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentDoc); lexer->setColor(theme.text, QsciLexerCPP::Default); lexer->setColor(theme.text, QsciLexerCPP::Identifier); lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor); lexer->setColor(theme.text, QsciLexerCPP::Operator); for (int i = 0; i <= 127; i++) lexer->setPaper(theme.background, i); } sci->setPaper(theme.background); sci->setColor(theme.text); sci->setCaretForegroundColor(theme.text); sci->setCaretLineBackgroundColor(theme.hover); sci->setSelectionBackgroundColor(theme.selection); sci->setSelectionForegroundColor(theme.text); sci->setMarginsBackgroundColor(theme.backgroundAlt); sci->setMarginsForegroundColor(theme.textDim); } } } void MainWindow::editTheme() { auto& tm = ThemeManager::instance(); int idx = tm.currentIndex(); ThemeEditor dlg(idx, this); if (dlg.exec() == QDialog::Accepted) { tm.updateTheme(dlg.selectedIndex(), dlg.result()); } else { tm.revertPreview(); } } // TODO: when adding more and more options, this func becomes very clunky. Fix void MainWindow::showOptionsDialog() { auto& tm = ThemeManager::instance(); OptionsResult current; current.themeIndex = tm.currentIndex(); current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString(); current.menuBarTitleCase = m_menuBarTitleCase; current.showIcon = m_titleBar ? QSettings("Reclass", "Reclass").value("showIcon", false).toBool() : false; current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool(); current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool(); current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt(); current.generatorAsserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); OptionsDialog dlg(current, this); if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK auto r = dlg.result(); if (r.themeIndex != current.themeIndex) tm.setCurrent(r.themeIndex); if (r.fontName != current.fontName) setEditorFont(r.fontName); if (r.menuBarTitleCase != current.menuBarTitleCase) { applyMenuBarTitleCase(r.menuBarTitleCase); QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase); } if (r.showIcon != current.showIcon) { if (m_titleBar) m_titleBar->setShowIcon(r.showIcon); QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon); } if (r.safeMode != current.safeMode) QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode); if (r.autoStartMcp != current.autoStartMcp) QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp); if (r.refreshMs != current.refreshMs) { QSettings("Reclass", "Reclass").setValue("refreshMs", r.refreshMs); for (auto& tab : m_tabs) tab.ctrl->setRefreshInterval(r.refreshMs); } if (r.generatorAsserts != current.generatorAsserts) QSettings("Reclass", "Reclass").setValue("generatorAsserts", r.generatorAsserts); } void MainWindow::setEditorFont(const QString& fontName) { QSettings settings("Reclass", "Reclass"); settings.setValue("font", fontName); QFont f(fontName, 12); f.setFixedPitch(true); for (auto& state : m_tabs) { state.ctrl->setEditorFont(fontName); for (auto& pane : state.panes) { // Update rendered view font if (pane.rendered) { pane.rendered->setFont(f); if (auto* lex = pane.rendered->lexer()) { lex->setFont(f); for (int i = 0; i <= 127; i++) lex->setFont(f, i); } pane.rendered->setMarginsFont(f); } } } // Sync workspace tree font if (m_workspaceTree) m_workspaceTree->setFont(f); // Sync dock titlebar font if (m_dockTitleLabel) m_dockTitleLabel->setFont(f); // Sync scanner panel font if (m_scannerPanel) m_scannerPanel->setEditorFont(f); if (m_scanDockTitle) m_scanDockTitle->setFont(f); } RcxController* MainWindow::activeController() const { auto* sub = m_mdiArea->activeSubWindow(); if (sub && m_tabs.contains(sub)) return m_tabs[sub].ctrl; return nullptr; } MainWindow::TabState* MainWindow::activeTab() { auto* sub = m_mdiArea->activeSubWindow(); if (sub && m_tabs.contains(sub)) return &m_tabs[sub]; return nullptr; } MainWindow::TabState* MainWindow::tabByIndex(int index) { auto subs = m_mdiArea->subWindowList(); if (index < 0 || index >= subs.size()) return nullptr; auto* sub = subs[index]; if (m_tabs.contains(sub)) return &m_tabs[sub]; return nullptr; } void MainWindow::updateWindowTitle() { #ifdef __APPLE__ setWindowTitle(QStringLiteral("Reclass")); #else QString title; auto* sub = m_mdiArea->activeSubWindow(); if (sub && m_tabs.contains(sub)) { auto& tab = m_tabs[sub]; QString name = tab.doc->filePath.isEmpty() ? rootName(tab.doc->tree, tab.ctrl->viewRootId()) : QFileInfo(tab.doc->filePath).fileName(); if (tab.doc->modified) name += " *"; title = name + " - Reclass"; } else { title = "Reclass"; } setWindowTitle(title); #endif } // ── Rendered view setup ── void MainWindow::setupRenderedSci(QsciScintilla* sci) { QSettings settings("Reclass", "Reclass"); QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont f(fontName, 12); f.setFixedPitch(true); sci->setFont(f); sci->setReadOnly(false); sci->setWrapMode(QsciScintilla::WrapNone); sci->setTabWidth(4); sci->setIndentationsUseTabs(false); sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2); sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2); // Line number margin sci->setMarginType(0, QsciScintilla::NumberMargin); sci->setMarginWidth(0, "00000"); const auto& theme = ThemeManager::instance().current(); sci->setMarginsBackgroundColor(theme.backgroundAlt); sci->setMarginsForegroundColor(theme.textDim); sci->setMarginsFont(f); // Hide other margins sci->setMarginWidth(1, 0); sci->setMarginWidth(2, 0); // C++ lexer for syntax highlighting — must be set BEFORE colors below, // because setLexer() resets caret line, selection, and paper colors. auto* lexer = new QsciLexerCPP(sci); lexer->setFont(f); lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword); lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::KeywordSet2); lexer->setColor(theme.syntaxNumber, QsciLexerCPP::Number); lexer->setColor(theme.syntaxString, QsciLexerCPP::DoubleQuotedString); lexer->setColor(theme.syntaxString, QsciLexerCPP::SingleQuotedString); lexer->setColor(theme.syntaxComment, QsciLexerCPP::Comment); lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentLine); lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentDoc); lexer->setColor(theme.text, QsciLexerCPP::Default); lexer->setColor(theme.text, QsciLexerCPP::Identifier); lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor); lexer->setColor(theme.text, QsciLexerCPP::Operator); for (int i = 0; i <= 127; i++) { lexer->setPaper(theme.background, i); lexer->setFont(f, i); } sci->setLexer(lexer); sci->setBraceMatching(QsciScintilla::NoBraceMatch); // Colors applied AFTER setLexer() — the lexer resets these on attach sci->setPaper(theme.background); sci->setColor(theme.text); sci->setCaretForegroundColor(theme.text); sci->setCaretLineVisible(true); sci->setCaretLineBackgroundColor(theme.hover); sci->setSelectionBackgroundColor(theme.selection); sci->setSelectionForegroundColor(theme.text); } // ── View mode / generator switching ── void MainWindow::setViewMode(ViewMode mode) { auto* pane = findActiveSplitPane(); if (!pane) return; pane->viewMode = mode; int idx = (mode == VM_Rendered) ? 1 : 0; pane->tabWidget->setCurrentIndex(idx); syncViewButtons(mode); } void MainWindow::syncViewButtons(ViewMode mode) { QSignalBlocker block(m_viewBtnGroup); if (mode == VM_Rendered) m_btnRendered->setChecked(true); else m_btnReclass->setChecked(true); } // ── Find the root-level struct ancestor for a node ── uint64_t MainWindow::findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const { QSet visited; uint64_t cur = nodeId; uint64_t lastStruct = 0; while (cur != 0 && !visited.contains(cur)) { visited.insert(cur); int idx = tree.indexOfId(cur); if (idx < 0) break; const Node& n = tree.nodes[idx]; if (n.kind == NodeKind::Struct) lastStruct = n.id; if (n.parentId == 0) return (n.kind == NodeKind::Struct) ? n.id : lastStruct; cur = n.parentId; } return lastStruct; } // ── Update the rendered view for a single pane ── void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) { if (pane.viewMode != VM_Rendered) return; if (!pane.rendered) return; // Determine which struct to render based on selection uint64_t rootId = 0; QSet selIds = tab.ctrl->selectedIds(); if (selIds.size() >= 1) { uint64_t selId = *selIds.begin(); selId &= ~(kFooterIdBit | kArrayElemBit | kArrayElemMask | kMemberBit | kMemberSubMask); rootId = findRootStructForNode(tab.doc->tree, selId); } // Fall back to the controller's current view root (set by double-click / navigation) if (rootId == 0) rootId = findRootStructForNode(tab.doc->tree, tab.ctrl->viewRootId()); // Last resort: first root-level struct in the project if (rootId == 0) { for (const auto& n : tab.doc->tree.nodes) { if (n.parentId == 0 && n.kind == rcx::NodeKind::Struct) { rootId = n.id; break; } } } // Generate text const QHash* aliases = tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases; bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); QString text; if (rootId != 0) text = renderCpp(tab.doc->tree, rootId, aliases, asserts); else text = renderCppAll(tab.doc->tree, aliases, asserts); // Scroll restoration: save if same root, reset if different int restoreLine = 0; if (rootId != 0 && rootId == pane.lastRenderedRootId) { restoreLine = (int)pane.rendered->SendScintilla( QsciScintillaBase::SCI_GETFIRSTVISIBLELINE); } pane.lastRenderedRootId = rootId; // Set text pane.rendered->setText(text); // Update margin width for line count int lineCount = pane.rendered->lines(); QString marginStr = QString(QString::number(lineCount).size() + 2, '0'); pane.rendered->setMarginWidth(0, marginStr); // Restore scroll if (restoreLine > 0) { pane.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, (unsigned long)restoreLine); } } void MainWindow::updateAllRenderedPanes(TabState& tab) { for (auto& pane : tab.panes) { if (pane.viewMode == VM_Rendered) updateRenderedView(tab, pane); } } // ── Export C++ header to file ── void MainWindow::exportCpp() { auto* tab = activeTab(); if (!tab) return; QString path = QFileDialog::getSaveFileName(this, "Export C++ Header", {}, "C++ Header (*.h);;All Files (*)"); if (path.isEmpty()) return; const QHash* aliases = tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases; bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); QString text = renderCppAll(tab->doc->tree, aliases, asserts); QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, "Export Failed", "Could not write to: " + path); return; } file.write(text.toUtf8()); setAppStatus("Exported to " + QFileInfo(path).fileName()); } // ── Export ReClass XML ── void MainWindow::exportReclassXmlAction() { auto* tab = activeTab(); if (!tab) return; QString path = QFileDialog::getSaveFileName(this, "Export ReClass XML", {}, "ReClass XML (*.reclass);;All Files (*)"); if (path.isEmpty()) return; QString error; if (!rcx::exportReclassXml(tab->doc->tree, path, &error)) { QMessageBox::warning(this, "Export Failed", error.isEmpty() ? QStringLiteral("Could not export") : error); return; } int classCount = 0; for (const auto& n : tab->doc->tree.nodes) if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++; setAppStatus(QStringLiteral("Exported %1 classes to %2") .arg(classCount).arg(QFileInfo(path).fileName())); } // ── Import ReClass XML ── void MainWindow::importReclassXml() { QString filePath = QFileDialog::getOpenFileName(this, "Import ReClass XML", {}, "ReClass XML (*.reclass *.MemeCls *.xml);;All Files (*)"); if (filePath.isEmpty()) return; QString error; NodeTree tree = rcx::importReclassXml(filePath, &error); if (tree.nodes.isEmpty()) { QMessageBox::warning(this, "Import Failed", error.isEmpty() ? QStringLiteral("No data found in file") : error); return; } // Count root structs for status message int classCount = 0; for (const auto& n : tree.nodes) if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++; auto* doc = new RcxDocument(this); doc->tree = std::move(tree); m_mdiArea->closeAllSubWindows(); createTab(doc); rebuildWorkspaceModel(); setAppStatus(QStringLiteral("Imported %1 classes from %2") .arg(classCount).arg(QFileInfo(filePath).fileName())); } // ── Import from Source ── void MainWindow::importFromSource() { QDialog dlg(this); dlg.setWindowTitle("Import from Source"); dlg.resize(700, 600); auto* layout = new QVBoxLayout(&dlg); auto* sci = new QsciScintilla(&dlg); setupRenderedSci(sci); sci->setReadOnly(false); sci->setMarginWidth(0, "00000"); layout->addWidget(sci); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); buttons->button(QDialogButtonBox::Ok)->setText("Import"); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() != QDialog::Accepted) return; QString source = sci->text(); if (source.trimmed().isEmpty()) return; QString error; NodeTree tree = rcx::importFromSource(source, &error); if (tree.nodes.isEmpty()) { QMessageBox::warning(this, "Import Failed", error.isEmpty() ? QStringLiteral("No struct definitions found") : error); return; } int classCount = 0; for (const auto& n : tree.nodes) if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++; auto* doc = new RcxDocument(this); doc->tree = std::move(tree); m_mdiArea->closeAllSubWindows(); createTab(doc); rebuildWorkspaceModel(); m_workspaceDock->show(); setAppStatus(QStringLiteral("Imported %1 classes from source").arg(classCount)); } // ── Import PDB ── void MainWindow::importPdb() { rcx::PdbImportDialog dlg(this); if (dlg.exec() != QDialog::Accepted) return; QString pdbPath = dlg.pdbPath(); QVector indices = dlg.selectedTypeIndices(); if (indices.isEmpty()) return; QProgressDialog progress("Importing types...", "Cancel", 0, indices.size(), this); progress.setWindowModality(Qt::WindowModal); progress.setMinimumDuration(200); bool cancelled = false; QString error; NodeTree tree = rcx::importPdbSelected(pdbPath, indices, &error, [&](int current, int total) -> bool { progress.setMaximum(total); progress.setValue(current); QApplication::processEvents(); if (progress.wasCanceled()) { cancelled = true; return false; } return true; }); progress.close(); if (tree.nodes.isEmpty()) { if (!cancelled) QMessageBox::warning(this, "Import Failed", error.isEmpty() ? QStringLiteral("No types imported") : error); return; } int classCount = 0; for (const auto& n : tree.nodes) if (n.parentId == 0 && n.kind == rcx::NodeKind::Struct) classCount++; auto* doc = new rcx::RcxDocument(this); doc->tree = std::move(tree); m_mdiArea->closeAllSubWindows(); createTab(doc); rebuildWorkspaceModel(); m_workspaceDock->show(); setAppStatus(QStringLiteral("Imported %1 classes from %2") .arg(classCount).arg(QFileInfo(pdbPath).fileName())); } // ── Type Aliases Dialog ── void MainWindow::showTypeAliasesDialog() { auto* tab = activeTab(); if (!tab) return; QDialog dlg(this); dlg.setWindowTitle("Type Aliases"); dlg.resize(400, 380); auto* layout = new QVBoxLayout(&dlg); // Preset buttons (stdint + Windows only, no redundant Reset) auto* presetRow = new QHBoxLayout; auto* btnStdint = new QPushButton("stdint (C99)", &dlg); auto* btnWindows = new QPushButton("Windows (basetsd.h)", &dlg); presetRow->addWidget(btnStdint); presetRow->addWidget(btnWindows); presetRow->addStretch(); layout->addLayout(presetRow); auto* table = new QTableWidget(&dlg); table->setColumnCount(2); table->horizontalHeader()->setVisible(false); table->horizontalHeader()->setStretchLastSection(true); table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); table->setSelectionMode(QAbstractItemView::SingleSelection); table->verticalHeader()->setVisible(false); // Skip types that nobody aliases (Vec, Mat, Struct, Array) auto shouldSkip = [](NodeKind k) { return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4 || k == NodeKind::Mat4x4 || k == NodeKind::Struct || k == NodeKind::Array; }; // Build filtered row→meta index mapping QVector rowMap; int totalMeta = static_cast(std::size(kKindMeta)); for (int i = 0; i < totalMeta; i++) if (!shouldSkip(kKindMeta[i].kind)) rowMap.append(i); table->setRowCount(rowMap.size()); for (int row = 0; row < rowMap.size(); row++) { const auto& meta = kKindMeta[rowMap[row]]; auto* kindItem = new QTableWidgetItem(QString::fromLatin1(meta.name)); kindItem->setFlags(kindItem->flags() & ~Qt::ItemIsEditable); table->setItem(row, 0, kindItem); QString alias = tab->doc->typeAliases.value(meta.kind); table->setItem(row, 1, new QTableWidgetItem(alias)); } // stdint preset: actual typeName values from kKindMeta static QHash kStdintPreset; if (kStdintPreset.isEmpty()) { for (const auto& m : kKindMeta) kStdintPreset[m.kind] = QString::fromLatin1(m.typeName); } // Windows (basetsd.h) preset mapping static const QHash kWindowsPreset = { {NodeKind::Int8, QStringLiteral("CHAR")}, {NodeKind::Int16, QStringLiteral("SHORT")}, {NodeKind::Int32, QStringLiteral("LONG")}, {NodeKind::Int64, QStringLiteral("LONGLONG")}, {NodeKind::UInt8, QStringLiteral("UCHAR")}, {NodeKind::UInt16, QStringLiteral("USHORT")}, {NodeKind::UInt32, QStringLiteral("ULONG")}, {NodeKind::UInt64, QStringLiteral("ULONGLONG")}, {NodeKind::Float, QStringLiteral("FLOAT")}, {NodeKind::Double, QStringLiteral("DOUBLE")}, {NodeKind::Bool, QStringLiteral("BOOLEAN")}, {NodeKind::Pointer32, QStringLiteral("ULONG")}, {NodeKind::Pointer64, QStringLiteral("ULONG_PTR")}, {NodeKind::FuncPtr32, QStringLiteral("ULONG")}, {NodeKind::FuncPtr64, QStringLiteral("ULONG_PTR")}, {NodeKind::Hex8, QStringLiteral("BYTE")}, {NodeKind::Hex16, QStringLiteral("WORD")}, {NodeKind::Hex32, QStringLiteral("DWORD")}, {NodeKind::Hex64, QStringLiteral("DWORD64")}, {NodeKind::UTF8, QStringLiteral("CHAR[]")}, {NodeKind::UTF16, QStringLiteral("WCHAR[]")}, }; auto applyPreset = [&](const QHash& preset) { for (int row = 0; row < rowMap.size(); row++) table->item(row, 1)->setText(preset.value(kKindMeta[rowMap[row]].kind)); }; connect(btnStdint, &QPushButton::clicked, [&]() { applyPreset(kStdintPreset); }); connect(btnWindows, &QPushButton::clicked, [&]() { applyPreset(kWindowsPreset); }); layout->addWidget(table); auto* buttons = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() != QDialog::Accepted) return; // Collect new aliases QHash newAliases; for (int row = 0; row < rowMap.size(); row++) { QString val = table->item(row, 1)->text().trimmed(); if (!val.isEmpty()) newAliases[kKindMeta[rowMap[row]].kind] = val; } tab->doc->typeAliases = newAliases; tab->doc->modified = true; tab->ctrl->refresh(); updateWindowTitle(); } // ── Project Lifecycle API ── QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) { auto* doc = new RcxDocument(this); QByteArray data(256, '\0'); doc->loadData(data); doc->tree.baseAddress = 0x00400000; buildEmptyStruct(doc->tree, classKeyword); // Inherit source from current tab (if any) auto* currentCtrl = activeController(); if (currentCtrl && currentCtrl->document()->provider && currentCtrl->document()->provider->isValid()) { doc->provider = currentCtrl->document()->provider; } auto* sub = createTab(doc); // Copy saved sources to new tab's controller if (currentCtrl && !currentCtrl->savedSources().isEmpty()) { auto& newTab = m_tabs[sub]; newTab.ctrl->copySavedSources(currentCtrl->savedSources(), currentCtrl->activeSourceIndex()); } rebuildWorkspaceModel(); return sub; } QMdiSubWindow* MainWindow::project_open(const QString& path) { QString filePath = 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)" ";;All (*)"); if (filePath.isEmpty()) return nullptr; } // Detect if this is an XML-based ReClass file by checking first bytes bool isXml = false; { QFile probe(filePath); if (probe.open(QIODevice::ReadOnly)) { QByteArray head = probe.read(64); isXml = head.trimmed().startsWith("tree = std::move(tree); m_mdiArea->closeAllSubWindows(); auto* sub = createTab(doc); rebuildWorkspaceModel(); m_workspaceDock->show(); int classCount = 0; for (const auto& n : doc->tree.nodes) if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++; setAppStatus(QStringLiteral("Imported %1 classes from %2") .arg(classCount).arg(QFileInfo(filePath).fileName())); addRecentFile(filePath); return sub; } auto* doc = new RcxDocument(this); if (!doc->load(filePath)) { QMessageBox::warning(this, "Error", "Failed to load: " + filePath); delete doc; return nullptr; } // Close all existing tabs so the project replaces the current state m_mdiArea->closeAllSubWindows(); auto* sub = createTab(doc); rebuildWorkspaceModel(); m_workspaceDock->show(); addRecentFile(filePath); return sub; } bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) { if (!sub) sub = m_mdiArea->activeSubWindow(); if (!sub || !m_tabs.contains(sub)) return false; auto& tab = m_tabs[sub]; if (saveAs || tab.doc->filePath.isEmpty()) { QString path = QFileDialog::getSaveFileName(this, "Save Definition", {}, "Reclass (*.rcx);;JSON (*.json)"); if (path.isEmpty()) return false; tab.doc->save(path); addRecentFile(path); } else { tab.doc->save(tab.doc->filePath); addRecentFile(tab.doc->filePath); } updateWindowTitle(); return true; } void MainWindow::project_close(QMdiSubWindow* sub) { if (!sub) sub = m_mdiArea->activeSubWindow(); if (!sub) return; sub->close(); rebuildWorkspaceModel(); } // ── Workspace Dock ── void MainWindow::createWorkspaceDock() { m_workspaceDock = new QDockWidget("Project", this); m_workspaceDock->setObjectName("WorkspaceDock"); m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); m_workspaceDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable); // Custom titlebar: label + ✕ close button (matches MDI tab style) { const auto& t = ThemeManager::instance().current(); auto* titleBar = new QWidget(m_workspaceDock); titleBar->setFixedHeight(24); titleBar->setAutoFillBackground(true); { QPalette tbPal = titleBar->palette(); tbPal.setColor(QPalette::Window, t.backgroundAlt); titleBar->setPalette(tbPal); } auto* layout = new QHBoxLayout(titleBar); layout->setContentsMargins(6, 2, 2, 2); layout->setSpacing(0); m_dockTitleLabel = new QLabel("Project", titleBar); { QPalette lp = m_dockTitleLabel->palette(); lp.setColor(QPalette::WindowText, t.textDim); m_dockTitleLabel->setPalette(lp); } layout->addWidget(m_dockTitleLabel); layout->addStretch(); m_dockCloseBtn = new QToolButton(titleBar); m_dockCloseBtn->setText(QStringLiteral("\u2715")); m_dockCloseBtn->setAutoRaise(true); m_dockCloseBtn->setCursor(Qt::PointingHandCursor); m_dockCloseBtn->setStyleSheet(QStringLiteral( "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" "QToolButton:hover { color: %2; }") .arg(t.textDim.name(), t.indHoverSpan.name())); connect(m_dockCloseBtn, &QToolButton::clicked, m_workspaceDock, &QDockWidget::close); layout->addWidget(m_dockCloseBtn); m_workspaceDock->setTitleBarWidget(titleBar); } // Container widget: search box + tree view auto* dockContainer = new QWidget(m_workspaceDock); auto* dockLayout = new QVBoxLayout(dockContainer); dockLayout->setContentsMargins(0, 0, 0, 0); dockLayout->setSpacing(0); m_workspaceSearch = new QLineEdit(dockContainer); m_workspaceSearch->setPlaceholderText(QStringLiteral("Search...")); m_workspaceSearch->setClearButtonEnabled(true); { const auto& t = ThemeManager::instance().current(); m_workspaceSearch->setStyleSheet(QStringLiteral( "QLineEdit { background: %1; color: %2; border: none;" " border-bottom: 1px solid %3; padding: 4px 6px; }") .arg(t.background.name(), t.textDim.name(), t.border.name())); } dockLayout->addWidget(m_workspaceSearch); m_workspaceTree = new QTreeView(dockContainer); m_workspaceModel = new QStandardItemModel(this); m_workspaceModel->setHorizontalHeaderLabels({"Name"}); m_workspaceProxy = new QSortFilterProxyModel(this); m_workspaceProxy->setSourceModel(m_workspaceModel); m_workspaceProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); m_workspaceProxy->setRecursiveFilteringEnabled(true); m_workspaceTree->setModel(m_workspaceProxy); m_workspaceTree->setHeaderHidden(true); m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers); m_workspaceTree->setExpandsOnDoubleClick(false); m_workspaceTree->setMouseTracking(true); 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); }); // Override palette: selection + hover use theme colors (not default blue) { const auto& t = ThemeManager::instance().current(); QPalette tp = m_workspaceTree->palette(); tp.setColor(QPalette::Text, t.textDim); tp.setColor(QPalette::Highlight, t.hover); tp.setColor(QPalette::HighlightedText, t.text); m_workspaceTree->setPalette(tp); } dockLayout->addWidget(m_workspaceTree); 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) { QMenu menu; auto* actClass = menu.addAction("New Class"); auto* actStruct = menu.addAction("New Struct"); auto* actEnum = menu.addAction("New Enum"); QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos)); if (chosen == actClass) newClass(); else if (chosen == actStruct) newStruct(); else if (chosen == actEnum) newEnum(); return; } if (structId == 0) return; auto subVar = index.data(Qt::UserRole); if (!subVar.isValid()) return; auto* sub = static_cast(subVar.value()); if (!sub || !m_tabs.contains(sub)) return; auto& tab = m_tabs[sub]; int ni = tab.doc->tree.indexOfId(structId); if (ni < 0) return; QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword(); QMenu menu; QAction* actConvert = nullptr; // class↔struct conversion only (no enum conversion) if (kw == QStringLiteral("class")) actConvert = menu.addAction("Convert to Struct"); else if (kw == QStringLiteral("struct")) actConvert = menu.addAction("Convert to Class"); auto* actDelete = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete"); QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos)); if (chosen == actDelete) { QString typeName = tab.doc->tree.nodes[ni].structTypeName.isEmpty() ? tab.doc->tree.nodes[ni].name : tab.doc->tree.nodes[ni].structTypeName; if (typeName.isEmpty()) typeName = QStringLiteral("(unnamed)"); // Collect detailed reference info QStringList refDetails; for (const auto& n : tab.doc->tree.nodes) { if (n.refId == structId) { QString ownerName; uint64_t pid = n.parentId; while (pid != 0) { int pi = tab.doc->tree.indexOfId(pid); if (pi < 0) break; if (tab.doc->tree.nodes[pi].parentId == 0) { ownerName = tab.doc->tree.nodes[pi].structTypeName.isEmpty() ? tab.doc->tree.nodes[pi].name : tab.doc->tree.nodes[pi].structTypeName; break; } pid = tab.doc->tree.nodes[pi].parentId; } QString fieldDesc = ownerName.isEmpty() ? n.name : QStringLiteral("%1::%2").arg(ownerName, n.name); refDetails << QStringLiteral(" \u2022 %1 (%2)") .arg(fieldDesc, kindToString(n.kind)); } } QString msg; if (refDetails.isEmpty()) { msg = QString("Delete '%1'?").arg(typeName); } else { msg = QString("Delete '%1'?\n\n" "The following %2 field(s) reference this type " "and will become untyped (void):\n\n%3") .arg(typeName) .arg(refDetails.size()) .arg(refDetails.join('\n')); } auto answer = QMessageBox::question(this, "Delete Type", msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer != QMessageBox::Yes) return; tab.ctrl->deleteRootStruct(structId); rebuildWorkspaceModel(); } else if (chosen && chosen == actConvert) { QString newKw = kw == QStringLiteral("class") ? QStringLiteral("struct") : QStringLiteral("class"); QString oldKw = tab.doc->tree.nodes[ni].resolvedClassKeyword(); tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl, rcx::cmd::ChangeClassKeyword{structId, oldKw, newKw})); rebuildWorkspaceModel(); } }); // Ctrl+F focuses the workspace search field { auto* findAction = new QAction(dockContainer); findAction->setShortcut(QKeySequence::Find); findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); dockContainer->addAction(findAction); connect(findAction, &QAction::triggered, this, [this]() { m_workspaceSearch->setFocus(); m_workspaceSearch->selectAll(); }); } m_workspaceDock->setWidget(dockContainer); addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock); m_workspaceDock->hide(); connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) { 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* sub = static_cast(subVar.value()); if (!sub || !m_tabs.contains(sub)) return; m_mdiArea->setActiveSubWindow(sub); auto& tree = m_tabs[sub].doc->tree; int ni = tree.indexOfId(structId); if (ni < 0) return; auto& tab = m_tabs[sub]; // Child member item: navigate to parent struct, then scroll to this member uint64_t parentId = tree.nodes[ni].parentId; if (parentId != 0) { int pi = tree.indexOfId(parentId); if (pi >= 0) tree.nodes[pi].collapsed = false; tab.ctrl->setViewRootId(parentId); tab.ctrl->scrollToNodeId(structId); } else { // Root type/enum: navigate directly tree.nodes[ni].collapsed = false; tab.ctrl->setViewRootId(structId); tab.ctrl->scrollToNodeId(structId); } // If active pane is in C/C++ mode, refresh after navigation settles QTimer::singleShot(0, this, [this, sub]() { if (!m_tabs.contains(sub)) return; auto& t = m_tabs[sub]; if (t.activePaneIdx >= 0 && t.activePaneIdx < t.panes.size()) { auto& p = t.panes[t.activePaneIdx]; if (p.viewMode == VM_Rendered) updateRenderedView(t, p); } }); }); } // ── Scanner Dock ── void MainWindow::createScannerDock() { m_scannerDock = new QDockWidget("Scanner", this); m_scannerDock->setObjectName("ScannerDock"); m_scannerDock->setAllowedAreas( Qt::BottomDockWidgetArea | Qt::RightDockWidgetArea | Qt::LeftDockWidgetArea); m_scannerDock->setFeatures( QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); // Custom titlebar: label + close button (matches workspace dock) { const auto& t = ThemeManager::instance().current(); auto* titleBar = new QWidget(m_scannerDock); titleBar->setFixedHeight(24); titleBar->setAutoFillBackground(true); { QPalette tbPal = titleBar->palette(); tbPal.setColor(QPalette::Window, t.backgroundAlt); titleBar->setPalette(tbPal); } auto* layout = new QHBoxLayout(titleBar); layout->setContentsMargins(6, 2, 2, 2); layout->setSpacing(0); m_scanDockTitle = new QLabel("Scanner", titleBar); { QPalette lp = m_scanDockTitle->palette(); lp.setColor(QPalette::WindowText, t.textDim); m_scanDockTitle->setPalette(lp); } layout->addWidget(m_scanDockTitle); layout->addStretch(); m_scanDockCloseBtn = new QToolButton(titleBar); m_scanDockCloseBtn->setText(QStringLiteral("\u2715")); m_scanDockCloseBtn->setAutoRaise(true); m_scanDockCloseBtn->setCursor(Qt::PointingHandCursor); m_scanDockCloseBtn->setStyleSheet(QStringLiteral( "QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }" "QToolButton:hover { color: %2; }") .arg(t.textDim.name(), t.indHoverSpan.name())); connect(m_scanDockCloseBtn, &QToolButton::clicked, m_scannerDock, &QDockWidget::close); layout->addWidget(m_scanDockCloseBtn); m_scannerDock->setTitleBarWidget(titleBar); } m_scannerPanel = new ScannerPanel(m_scannerDock); m_scannerPanel->applyTheme(ThemeManager::instance().current()); { QSettings settings("Reclass", "Reclass"); QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont f(fontName, 12); f.setFixedPitch(true); m_scannerPanel->setEditorFont(f); m_scanDockTitle->setFont(f); } m_scannerDock->setWidget(m_scannerPanel); addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock); m_scannerDock->hide(); // Wire provider getter: lazily captures the active tab's provider at scan time m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr { auto* ctrl = activeController(); return ctrl ? ctrl->document()->provider : nullptr; }); // Wire "Go to Address" to rebase the active tab connect(m_scannerPanel, &ScannerPanel::goToAddress, this, [this](uint64_t addr) { auto* ctrl = activeController(); if (!ctrl) return; ctrl->document()->tree.baseAddress = addr; ctrl->document()->tree.baseAddressFormula.clear(); ctrl->resetChangeTracking(); ctrl->refresh(); }); } void MainWindow::rebuildAllDocs() { m_allDocs.clear(); for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) m_allDocs.append(it.value().doc); } void MainWindow::rebuildWorkspaceModel() { QVector tabs; for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) { TabState& tab = it.value(); QString name = tab.doc->filePath.isEmpty() ? rootName(tab.doc->tree, tab.ctrl->viewRootId()) : QFileInfo(tab.doc->filePath).fileName(); tabs.append({ &tab.doc->tree, name, static_cast(it.key()) }); } rcx::buildProjectExplorer(m_workspaceModel, tabs); m_workspaceTree->expandToDepth(0); } void MainWindow::addRecentFile(const QString& path) { if (path.isEmpty()) return; QString absPath = QFileInfo(path).absoluteFilePath(); QSettings s("Reclass", "Reclass"); QStringList recent = s.value("recentFiles").toStringList(); recent.removeAll(absPath); recent.prepend(absPath); while (recent.size() > 10) recent.removeLast(); s.setValue("recentFiles", recent); updateRecentFilesMenu(); } void MainWindow::updateRecentFilesMenu() { if (!m_recentFilesMenu) return; m_recentFilesMenu->clear(); QSettings s("Reclass", "Reclass"); QStringList recent = s.value("recentFiles").toStringList(); int added = 0; for (const QString& path : recent) { if (!QFile::exists(path)) continue; QString label = QStringLiteral("&%1 %2").arg(added + 1).arg(QFileInfo(path).fileName()); m_recentFilesMenu->addAction(label, this, [this, path]() { project_open(path); })->setToolTip(path); if (++added >= 10) break; } if (added == 0) { auto* empty = m_recentFilesMenu->addAction(QStringLiteral("(empty)")); empty->setEnabled(false); } } void MainWindow::populateSourceMenu() { m_sourceMenu->clear(); auto* ctrl = activeController(); // Icon map for known provider identifiers static const QHash s_providerIcons = { {QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")}, {QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")}, {QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")}, {QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")}, }; auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) { auto* act = m_sourceMenu->addAction(icon, text); act->setIconVisibleInMenu(true); connect(act, &QAction::triggered, this, std::forward(slot)); return act; }; addSourceAction(QStringLiteral("File"), makeIcon(QStringLiteral(":/vsicons/file-binary.svg")), [this]() { if (auto* c = activeController()) c->selectSource(QStringLiteral("File")); }); const auto& providers = ProviderRegistry::instance().providers(); for (const auto& prov : providers) { QString name = prov.name; auto it = s_providerIcons.constFind(prov.identifier); QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it : QStringLiteral(":/vsicons/extensions.svg")); QString label = prov.dllFileName.isEmpty() ? name : QStringLiteral("%1 (%2)").arg(name, prov.dllFileName); addSourceAction(label, icon, [this, name]() { if (auto* c = activeController()) c->selectSource(name); }); } if (ctrl && !ctrl->savedSources().isEmpty()) { m_sourceMenu->addSeparator(); for (int i = 0; i < ctrl->savedSources().size(); i++) { const auto& e = ctrl->savedSources()[i]; auto* act = m_sourceMenu->addAction( QStringLiteral("%1 '%2'").arg(e.kind, e.displayName)); act->setCheckable(true); act->setChecked(i == ctrl->activeSourceIndex()); connect(act, &QAction::triggered, this, [this, i]() { if (auto* c = activeController()) c->switchSource(i); }); } m_sourceMenu->addSeparator(); auto* clearAct = addSourceAction(QStringLiteral("Clear All"), makeIcon(QStringLiteral(":/vsicons/clear-all.svg")), [this]() { if (auto* c = activeController()) c->clearSources(); }); Q_UNUSED(clearAct); } } void MainWindow::showPluginsDialog() { QDialog dialog(this); dialog.setWindowTitle("Plugins"); dialog.resize(600, 400); auto* layout = new QVBoxLayout(&dialog); auto* list = new QListWidget(); layout->addWidget(list); auto refreshList = [&]() { list->clear(); // Populate plugin list for (IPlugin* plugin : m_pluginManager.plugins()) { QString typeStr; switch (plugin->Type()) { case IPlugin::ProviderPlugin: typeStr = "Provider"; break; default: typeStr = "Unknown"; break; } QString text = QString("%1 v%2\n %3\n Type: %4\n Author: %5") .arg(QString::fromStdString(plugin->Name())) .arg(QString::fromStdString(plugin->Version())) .arg(QString::fromStdString(plugin->Description())) .arg(typeStr) .arg(QString::fromStdString(plugin->Author())); auto* item = new QListWidgetItem(plugin->Icon(), text); item->setData(Qt::UserRole, QString::fromStdString(plugin->Name())); list->addItem(item); } if (m_pluginManager.plugins().isEmpty()) { list->addItem("No plugins loaded"); } }; refreshList(); // Button row auto* btnLayout = new QHBoxLayout(); auto* btnLoad = new QPushButton("Load Plugin..."); connect(btnLoad, &QPushButton::clicked, [&, refreshList]() { QString path = QFileDialog::getOpenFileName(&dialog, "Load Plugin", QCoreApplication::applicationDirPath() + "/Plugins", "Plugins (*.dll *.so *.dylib);;All Files (*)"); if (!path.isEmpty()) { if (m_pluginManager.LoadPluginFromPath(path)) { refreshList(); setAppStatus("Plugin loaded successfully"); } else { QMessageBox::warning(&dialog, "Failed to Load Plugin", "Could not load the selected plugin.\nCheck the console for details."); } } }); auto* btnUnload = new QPushButton("Unload Selected"); connect(btnUnload, &QPushButton::clicked, [&, list, refreshList]() { auto* item = list->currentItem(); if (!item) { QMessageBox::information(&dialog, "No Selection", "Please select a plugin to unload."); return; } QString pluginName = item->data(Qt::UserRole).toString(); if (pluginName.isEmpty()) return; auto reply = QMessageBox::question(&dialog, "Unload Plugin", QString("Are you sure you want to unload '%1'?").arg(pluginName), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { if (m_pluginManager.UnloadPlugin(pluginName)) { refreshList(); setAppStatus("Plugin unloaded"); } else { QMessageBox::warning(&dialog, "Failed to Unload", "Could not unload the selected plugin."); } } }); auto* btnClose = new QPushButton("Close"); connect(btnClose, &QPushButton::clicked, &dialog, &QDialog::accept); btnLayout->addWidget(btnLoad); btnLayout->addWidget(btnUnload); btnLayout->addStretch(); btnLayout->addWidget(btnClose); layout->addLayout(btnLayout); dialog.exec(); } void MainWindow::changeEvent(QEvent* event) { QMainWindow::changeEvent(event); if (event->type() == QEvent::ActivationChange) { const auto& t = ThemeManager::instance().current(); updateBorderColor(isActiveWindow() ? t.borderFocused : t.border); } if (event->type() == QEvent::WindowStateChange && m_titleBar) m_titleBar->updateMaximizeIcon(); } void MainWindow::resizeEvent(QResizeEvent* event) { QMainWindow::resizeEvent(event); if (m_borderOverlay) { m_borderOverlay->setGeometry(rect()); m_borderOverlay->raise(); } if (auto* w = findChild("resizeGrip")) { auto* grip = static_cast(w); grip->reposition(); grip->raise(); } } void MainWindow::updateBorderColor(const QColor& color) { static_cast(m_borderOverlay)->color = color; m_borderOverlay->update(); } } // namespace rcx // ── Entry point ── int main(int argc, char* argv[]) { #ifdef _WIN32 SetUnhandledExceptionFilter(crashHandler); #endif #ifdef Q_OS_MACOS QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); #endif DarkApp app(argc, argv); app.setApplicationName("Reclass"); app.setOrganizationName("Reclass"); app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing // Load embedded fonts int fontId = QFontDatabase::addApplicationFont(":/fonts/JetBrainsMono.ttf"); if (fontId == -1) qWarning("Failed to load embedded JetBrains Mono font"); // Apply saved font preference before creating any editors { QSettings settings("Reclass", "Reclass"); QString savedFont = settings.value("font", "JetBrains Mono").toString(); rcx::RcxEditor::setGlobalFontName(savedFont); } // Global theme applyGlobalTheme(rcx::ThemeManager::instance().current()); rcx::MainWindow window; window.setWindowIcon(QIcon(":/icons/class.png")); window.show(); // Auto-open demo project from saved .rcx file QMetaObject::invokeMethod(&window, "selfTest"); return app.exec(); } // MainWindow Q_OBJECT is now in mainwindow.h; AUTOMOC handles moc generation.