diff --git a/src/editor.h b/src/editor.h index a5b17e2..32f257d 100644 --- a/src/editor.h +++ b/src/editor.h @@ -61,6 +61,8 @@ public: m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree; } + void setRelativeOffsets(bool rel) { m_relativeOffsets = rel; reformatMargins(); } + // Saved sources for quick-switch in source picker void setSavedSources(const QVector& sources) { m_savedSourceDisplay = sources; } diff --git a/src/main.cpp b/src/main.cpp index 8783a9a..4e754fe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -244,8 +244,6 @@ public: s.setHeight(s.height() + qRound(s.height() * 0.5)); if (type == CT_MenuItem) s = QSize(s.width() + 24, s.height() + 4); - if (type == CT_TabBarTab) - s = QSize(s.width(), 24); return s; } int pixelMetric(PixelMetric metric, const QStyleOption* opt, @@ -314,21 +312,6 @@ public: } } } - // MDI tab bar — override text color: dimmed for selected/hover - if (element == CE_TabBarTabLabel) { - if (auto* tab = qstyleoption_cast(opt)) { - const auto& t = rcx::ThemeManager::instance().current(); - QStyleOptionTab patched = *tab; - bool selected = tab->state & State_Selected; - bool hovered = tab->state & State_MouseOver; - if (selected || hovered) - patched.palette.setColor(QPalette::WindowText, t.textDim); - else - patched.palette.setColor(QPalette::WindowText, t.textMuted); - QProxyStyle::drawControl(element, &patched, p, w); - return; - } - } // Tree view items — use theme.hover for selection instead of blue if (element == CE_ItemViewItem) { if (auto* vi = qstyleoption_cast(opt)) { @@ -520,22 +503,19 @@ void MainWindow::createMenus() { 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(); - m_sourceMenu = file->addMenu("Current Tab So&urce"); - connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu); - file->addSeparator(); - Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile); - file->addSeparator(); - Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp); - Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction); - Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource); - Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml); - Qt5Qt6AddAction(file, "Import &PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb); + 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 { QDir exDir(QCoreApplication::applicationDirPath() + "/examples"); QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name); if (!rcxFiles.isEmpty()) { - auto* examples = file->addMenu("&Examples"); + 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); }); @@ -543,10 +523,7 @@ void MainWindow::createMenus() { } } file->addSeparator(); - const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server"; - m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp); - file->addSeparator(); - Qt5Qt6AddAction(file, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog); + 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); @@ -554,13 +531,14 @@ void MainWindow::createMenus() { auto* edit = m_titleBar->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); - edit->addSeparator(); - Qt5Qt6AddAction(edit, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog); // View auto* view = m_titleBar->menuBar()->addMenu("&View"); Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView); - Qt5Qt6AddAction(view, "&Unsplit", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView); + Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView); + view->addSeparator(); + 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); @@ -607,9 +585,28 @@ void MainWindow::createMenus() { 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()); + // Tools + auto* tools = m_titleBar->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_titleBar->menuBar()->addMenu("&Plugins"); Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog); @@ -1035,6 +1032,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { // 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 @@ -1672,6 +1671,13 @@ void MainWindow::applyTheme(const Theme& theme) { "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(); @@ -1715,6 +1721,12 @@ void MainWindow::applyTheme(const Theme& theme) { 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) { @@ -2444,15 +2456,47 @@ void MainWindow::createWorkspaceDock() { m_workspaceDock->setTitleBarWidget(titleBar); } - m_workspaceTree = new QTreeView(m_workspaceDock); + // 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_workspaceTree->setModel(m_workspaceModel); + + 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(); @@ -2463,6 +2507,8 @@ void MainWindow::createWorkspaceDock() { 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); @@ -2565,7 +2611,7 @@ void MainWindow::createWorkspaceDock() { } }); - m_workspaceDock->setWidget(m_workspaceTree); + m_workspaceDock->setWidget(dockContainer); addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock); m_workspaceDock->hide(); @@ -2586,12 +2632,23 @@ void MainWindow::createWorkspaceDock() { m_mdiArea->setActiveSubWindow(sub); - // Type/Enum node: navigate to it auto& tree = m_tabs[sub].doc->tree; int ni = tree.indexOfId(structId); - if (ni >= 0) tree.nodes[ni].collapsed = false; - m_tabs[sub].ctrl->setViewRootId(structId); - m_tabs[sub].ctrl->scrollToNodeId(structId); + if (ni < 0) return; + + // 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; + m_tabs[sub].ctrl->setViewRootId(parentId); + m_tabs[sub].ctrl->scrollToNodeId(structId); + } else { + // Root type/enum: navigate directly + tree.nodes[ni].collapsed = false; + m_tabs[sub].ctrl->setViewRootId(structId); + m_tabs[sub].ctrl->scrollToNodeId(structId); + } }); } @@ -2611,7 +2668,7 @@ void MainWindow::rebuildWorkspaceModel() { tabs.append({ &tab.doc->tree, name, static_cast(it.key()) }); } rcx::buildProjectExplorer(m_workspaceModel, tabs); - m_workspaceTree->expandToDepth(1); + m_workspaceTree->expandToDepth(0); } void MainWindow::populateSourceMenu() { diff --git a/src/mainwindow.h b/src/mainwindow.h index 1cd40ed..52f9aeb 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -137,11 +139,13 @@ private: RcxEditor* activePaneEditor(); // Workspace dock - QDockWidget* m_workspaceDock = nullptr; - QTreeView* m_workspaceTree = nullptr; - QStandardItemModel* m_workspaceModel = nullptr; - QLabel* m_dockTitleLabel = nullptr; - QToolButton* m_dockCloseBtn = nullptr; + QDockWidget* m_workspaceDock = nullptr; + QTreeView* m_workspaceTree = nullptr; + QStandardItemModel* m_workspaceModel = nullptr; + QSortFilterProxyModel* m_workspaceProxy = nullptr; + QLineEdit* m_workspaceSearch = nullptr; + QLabel* m_dockTitleLabel = nullptr; + QToolButton* m_dockCloseBtn = nullptr; void createWorkspaceDock(); void rebuildWorkspaceModel(); void updateBorderColor(const QColor& color); diff --git a/src/themes/defaults/reclass_dark.json b/src/themes/defaults/reclass_dark.json index b8792c2..4ea8030 100644 --- a/src/themes/defaults/reclass_dark.json +++ b/src/themes/defaults/reclass_dark.json @@ -10,8 +10,8 @@ "textDim": "#858585", "textMuted": "#585858", "textFaint": "#505050", - "hover": "#1e1e1e", - "selected": "#1e1e1e", + "hover": "#2a2a2a", + "selected": "#2a2d2e", "selection": "#2b2b2b", "syntaxKeyword": "#569cd6", "syntaxNumber": "#b5cea8", diff --git a/src/workspace_model.h b/src/workspace_model.h index 9838299..7164d50 100644 --- a/src/workspace_model.h +++ b/src/workspace_model.h @@ -29,46 +29,88 @@ inline void buildProjectExplorer(QStandardItemModel* model, projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1); // Collect all top-level structs/enums across all tabs - QVector> types, enums; + struct Entry { const Node* node; void* subPtr; const NodeTree* tree; }; + QVector types, enums; for (const auto& tab : tabs) { QVector topLevel = tab.tree->childrenOf(0); for (int idx : topLevel) { const Node& n = tab.tree->nodes[idx]; if (n.kind != NodeKind::Struct) continue; if (n.resolvedClassKeyword() == QStringLiteral("enum")) - enums.append({&n, tab.subPtr}); + enums.append({&n, tab.subPtr, tab.tree}); else - types.append({&n, tab.subPtr}); + types.append({&n, tab.subPtr, tab.tree}); } } auto nameOf = [](const Node* n) { return n->structTypeName.isEmpty() ? n->name : n->structTypeName; }; - auto cmpName = [&](const std::pair& a, - const std::pair& b) { - return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0; + auto cmpName = [&](const Entry& a, const Entry& b) { + return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0; }; std::sort(types.begin(), types.end(), cmpName); std::sort(enums.begin(), enums.end(), cmpName); - for (const auto& [n, subPtr] : types) { - QString display = QStringLiteral("%1 (%2)") - .arg(nameOf(n), n->resolvedClassKeyword()); + // Helper: type display string for a member node + auto memberTypeName = [](const Node& m) -> QString { + if (m.kind == NodeKind::Struct) { + QString stn = m.structTypeName.isEmpty() ? m.resolvedClassKeyword() + : m.structTypeName; + return stn; + } + return QString::fromLatin1(kindToString(m.kind)); + }; + + // Helper: is a Hex padding node + auto isHexPad = [](NodeKind k) { + return k == NodeKind::Hex8 || k == NodeKind::Hex16 + || k == NodeKind::Hex32 || k == NodeKind::Hex64; + }; + + for (const auto& e : types) { + QVector members = e.tree->childrenOf(e.node->id); + + // Count non-hex members for display + int visibleCount = 0; + for (int mi : members) + if (!isHexPad(e.tree->nodes[mi].kind)) ++visibleCount; + + QString display = QStringLiteral("%1 (%2) \u2014 %3") + .arg(nameOf(e.node), e.node->resolvedClassKeyword(), + QString::number(visibleCount)); auto* item = new QStandardItem( QIcon(":/vsicons/symbol-structure.svg"), display); - item->setData(QVariant::fromValue(subPtr), Qt::UserRole); - item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1); + item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole); + item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1); + + // Add child rows sorted by offset (skip Hex padding) + std::sort(members.begin(), members.end(), [&](int a, int b) { + return e.tree->nodes[a].offset < e.tree->nodes[b].offset; + }); + for (int mi : members) { + const Node& m = e.tree->nodes[mi]; + if (isHexPad(m.kind)) continue; + QString childDisplay = QStringLiteral("%1 %2") + .arg(memberTypeName(m), m.name); + auto* childItem = new QStandardItem(childDisplay); + childItem->setData(QVariant::fromValue(e.subPtr), Qt::UserRole); + childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1); + item->appendRow(childItem); + } + projectItem->appendRow(item); } - for (const auto& [n, subPtr] : enums) { - QString display = QStringLiteral("%1 (%2)") - .arg(nameOf(n), n->resolvedClassKeyword()); + for (const auto& e : enums) { + int count = e.node->enumMembers.size(); + QString display = QStringLiteral("%1 (%2) \u2014 %3") + .arg(nameOf(e.node), e.node->resolvedClassKeyword(), + QString::number(count)); auto* item = new QStandardItem( QIcon(":/vsicons/symbol-enum.svg"), display); - item->setData(QVariant::fromValue(subPtr), Qt::UserRole); - item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1); + item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole); + item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1); projectItem->appendRow(item); }