diff --git a/CMakeLists.txt b/CMakeLists.txt index 1407566..80c6bc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,14 +116,6 @@ endforeach() include(deploy) -if(TARGET deploy) - add_custom_target(screenshot ALL - COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png - DEPENDS Reclass deploy - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Capturing UI screenshot with class open..." - ) -endif() set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake") file(WRITE ${_combine_script} " diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index dece2ba..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/src/compose.cpp b/src/compose.cpp index 9955ba5..9c57dc5 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -671,7 +671,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR } // Emit CommandRow as line 0 (combined: source + address + root class type + name) - const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE NoName {"); + const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct NoName {"); { LineMeta lm; lm.nodeIdx = -1; @@ -743,20 +743,5 @@ QSet NodeTree::normalizePreferDescendants(const QSet& ids) c return result; } -int NodeTree::computeStructAlignment(uint64_t structId) const { - int idx = indexOfId(structId); - if (idx < 0) return 1; - int maxAlign = 1; - QVector kids = childrenOf(structId); - for (int ci : kids) { - const Node& c = nodes[ci]; - if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) { - maxAlign = qMax(maxAlign, computeStructAlignment(c.id)); - } else { - maxAlign = qMax(maxAlign, alignmentFor(c.kind)); - } - } - return maxAlign; -} } // namespace rcx diff --git a/src/controller.cpp b/src/controller.cpp index f5f5334..5438159 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -203,6 +203,8 @@ void RcxController::connectEditor(RcxEditor* editor) { this, [this, editor](int line, int nodeIdx, int subLine, QPoint globalPos) { showContextMenu(editor, line, nodeIdx, subLine, globalPos); }); + connect(editor, &RcxEditor::keywordConvertRequested, + this, &RcxController::convertRootKeyword); connect(editor, &RcxEditor::nodeClicked, this, [this, editor](int line, uint64_t nodeId, Qt::KeyboardModifiers mods) { handleNodeClick(editor, line, nodeId, mods); @@ -729,6 +731,27 @@ void RcxController::refresh() { applySelectionOverlays(); } +void RcxController::convertRootKeyword(const QString& newKeyword) { + uint64_t targetId = m_viewRootId; + if (targetId == 0) { + for (const auto& n : m_doc->tree.nodes) { + if (n.parentId == 0 && n.kind == NodeKind::Struct) { + targetId = n.id; + break; + } + } + } + if (targetId == 0) return; + int idx = m_doc->tree.indexOfId(targetId); + if (idx < 0) return; + QString oldKw = m_doc->tree.nodes[idx].resolvedClassKeyword(); + if (oldKw == newKeyword) return; + // Only allow class↔struct conversion + if (oldKw == QStringLiteral("enum") || newKeyword == QStringLiteral("enum")) return; + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeClassKeyword{targetId, oldKw, newKeyword})); +} + void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) { if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; auto& node = m_doc->tree.nodes[nodeIdx]; @@ -1438,22 +1461,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, }); } - // Align Members submenu - if (node.kind == NodeKind::Struct) { - int curAlign = m_doc->tree.computeStructAlignment(nodeId); - auto* alignMenu = menu.addMenu(icon("symbol-ruler.svg"), "Align &Members"); - static const int alignValues[] = {1, 2, 4, 8, 16, 32, 64, 128}; - for (int av : alignValues) { - QString label = (av == 1) - ? QStringLiteral("1 (packed)") - : QString::number(av); - auto* act = alignMenu->addAction(label, [this, nodeId, av]() { - performRealignment(nodeId, av); - }); - act->setCheckable(true); - act->setChecked(av == curAlign); - } - } } menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() { @@ -1488,33 +1495,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, // ── Always-available actions ── - // Root struct alignment (always available if a root struct exists) - { - uint64_t rootStructId = 0; - for (const auto& n : m_doc->tree.nodes) { - if (n.parentId == 0 && n.kind == NodeKind::Struct) { - rootStructId = n.id; - break; - } - } - if (rootStructId != 0) { - int curAlign = m_doc->tree.computeStructAlignment(rootStructId); - auto* alignMenu = menu.addMenu(icon("symbol-ruler.svg"), "Align &Members"); - static const int alignValues[] = {1, 2, 4, 8, 16, 32, 64, 128}; - for (int av : alignValues) { - QString label = (av == 1) - ? QStringLiteral("1 (packed)") - : QString::number(av); - auto* act = alignMenu->addAction(label, [this, rootStructId, av]() { - performRealignment(rootStructId, av); - }); - act->setCheckable(true); - act->setChecked(av == curAlign); - } - menu.addSeparator(); - } - } - menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() { uint64_t target = m_viewRootId ? m_viewRootId : 0; m_suppressRefresh = true; @@ -1670,112 +1650,6 @@ void RcxController::applySelectionOverlays() { editor->applySelectionOverlay(m_selIds); } -void RcxController::performRealignment(uint64_t structId, int targetAlign) { - auto& tree = m_doc->tree; - int rootIdx = tree.indexOfId(structId); - if (rootIdx < 0) return; - - // Gather direct children sorted by offset - QVector kids = tree.childrenOf(structId); - std::sort(kids.begin(), kids.end(), [&](int a, int b) { - return tree.nodes[a].offset < tree.nodes[b].offset; - }); - - // Separate into real nodes (non-hex) and hex filler nodes - struct NodeInfo { uint64_t id; int offset; int size; }; - QVector realNodes; - QVector hexIds; - - for (int ci : kids) { - const Node& child = tree.nodes[ci]; - int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) - ? tree.structSpan(child.id) : child.byteSize(); - if (isHexNode(child.kind)) - hexIds.append(child.id); - else - realNodes.append({child.id, child.offset, sz}); - } - - auto roundUp = [](int x, int align) -> int { - return align <= 1 ? x : ((x + align - 1) / align) * align; - }; - - // Compute new offsets for real nodes - struct OffChange { uint64_t id; int oldOff; int newOff; }; - QVector offChanges; - int cursor = 0; - for (auto& rn : realNodes) { - int newOff = roundUp(cursor, targetAlign); - if (newOff != rn.offset) - offChanges.append({rn.id, rn.offset, newOff}); - rn.offset = newOff; // update local copy for gap computation - cursor = newOff + rn.size; - } - - // Compute where padding is needed (gaps between consecutive nodes) - struct PadInsert { int offset; int size; }; - QVector padsNeeded; - - for (int i = 0; i < realNodes.size(); i++) { - int gapStart = (i == 0) ? 0 : realNodes[i - 1].offset + realNodes[i - 1].size; - int gapEnd = realNodes[i].offset; - if (gapEnd > gapStart) - padsNeeded.append({gapStart, gapEnd - gapStart}); - } - - // Check if anything actually changes - if (offChanges.isEmpty() && hexIds.isEmpty() && padsNeeded.isEmpty()) - return; - - // Apply as undoable macro - bool wasSuppressed = m_suppressRefresh; - m_suppressRefresh = true; - m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign)); - - // 1. Remove all existing hex filler nodes (no offset adjustments — we recompute) - for (uint64_t hid : hexIds) { - int idx = tree.indexOfId(hid); - if (idx < 0) continue; - QVector subtree; - subtree.append(tree.nodes[idx]); - m_doc->undoStack.push(new RcxCommand(this, - cmd::Remove{hid, subtree, {}})); - } - - // 2. Reposition real nodes - for (const auto& oc : offChanges) { - m_doc->undoStack.push(new RcxCommand(this, - cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff})); - } - - // 3. Insert hex nodes to fill gaps (largest first for alignment) - for (const auto& pi : padsNeeded) { - int padOffset = pi.offset; - int gap = pi.size; - while (gap > 0) { - NodeKind padKind; - int padSize; - if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; } - else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; } - else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; } - else { padKind = NodeKind::Hex8; padSize = 1; } - - Node pad; - pad.kind = padKind; - pad.parentId = structId; - pad.offset = padOffset; - pad.name = QString("pad_%1").arg(padOffset, 2, 16, QChar('0')); - pad.id = tree.reserveId(); - m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad})); - padOffset += padSize; - gap -= padSize; - } - } - - m_doc->undoStack.endMacro(); - m_suppressRefresh = wasSuppressed; - if (!m_suppressRefresh) refresh(); -} void RcxController::updateCommandRow() { // -- Source label: driven by provider metadata -- @@ -1821,7 +1695,7 @@ void RcxController::updateCommandRow() { const auto& n = m_doc->tree.nodes[vi]; QString keyword = n.resolvedClassKeyword(); QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; - row2 = QStringLiteral("%1\u25BE %2 {") + row2 = QStringLiteral("%1 %2 {") .arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className); } } @@ -1832,14 +1706,14 @@ void RcxController::updateCommandRow() { if (n.parentId == 0 && n.kind == NodeKind::Struct) { QString keyword = n.resolvedClassKeyword(); QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; - row2 = QStringLiteral("%1\u25BE %2 {") + row2 = QStringLiteral("%1 %2 {") .arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className); break; } } } if (row2.isEmpty()) - row2 = QStringLiteral("struct\u25BE NoName {"); + row2 = QStringLiteral("struct NoName {"); QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2; diff --git a/src/controller.h b/src/controller.h index 5fdc674..adbea5b 100644 --- a/src/controller.h +++ b/src/controller.h @@ -85,6 +85,7 @@ public: void removeSplitEditor(RcxEditor* editor); QList editors() const { return m_editors; } + void convertRootKeyword(const QString& newKeyword); void changeNodeKind(int nodeIdx, NodeKind newKind); void renameNode(int nodeIdx, const QString& newName); void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name); @@ -160,7 +161,6 @@ private: void connectEditor(RcxEditor* editor); void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods); void updateCommandRow(); - void performRealignment(uint64_t structId, int targetAlign); void switchToSavedSource(int idx); void pushSavedSourcesToEditors(); void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos); diff --git a/src/core.h b/src/core.h index 0a2896d..791facb 100644 --- a/src/core.h +++ b/src/core.h @@ -380,9 +380,6 @@ struct NodeTree { return qMax(declaredSize, maxEnd); } - // Compute natural alignment of a struct (max alignment of direct children) - int computeStructAlignment(uint64_t structId) const; - // Batch selection normalizers QSet normalizePreferAncestors(const QSet& ids) const; QSet normalizePreferDescendants(const QSet& ids) const; @@ -660,16 +657,17 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) { } // ── CommandRow root-class spans ── -// Combined CommandRow format ends with: " struct▾ ClassName {" +// Combined CommandRow format ends with: " struct ClassName {" inline int commandRowRootStart(const QString& lineText) { int best = -1; int i; - i = lineText.lastIndexOf(QStringLiteral("struct\u25BE")); + // Match "struct " / "class " / "enum " as whole words before the class name + i = lineText.lastIndexOf(QStringLiteral("struct ")); if (i > best) best = i; - i = lineText.lastIndexOf(QStringLiteral("class\u25BE")); + i = lineText.lastIndexOf(QStringLiteral("class ")); if (i > best) best = i; - i = lineText.lastIndexOf(QStringLiteral("enum\u25BE")); + i = lineText.lastIndexOf(QStringLiteral("enum ")); if (i > best) best = i; return best; } @@ -678,8 +676,7 @@ inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) { int start = commandRowRootStart(lineText); if (start < 0) return {}; int end = start; - while (end < lineText.size() && lineText[end] != QChar(' ') - && lineText[end] != QChar(0x25BE)) end++; + while (end < lineText.size() && lineText[end] != QChar(' ')) end++; if (end <= start) return {}; return {start, end, true}; } diff --git a/src/editor.cpp b/src/editor.cpp index 63c7c4e..5a11b26 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -25,6 +25,9 @@ namespace rcx { +// Forward declaration (defined below, after RcxEditor constructor) +static QString getLineText(QsciScintilla* sci, int line); + // ── Value history popup (styled like TypeSelectorPopup) ── class ValueHistoryPopup : public QFrame { @@ -327,6 +330,33 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { } HitInfo hi = hitTest(pos); int line = hi.line; + + // Right-click on command row keyword → show conversion menu + if (line == 0 && hi.col >= 0 && !m_meta.isEmpty() + && m_meta[0].lineKind == LineKind::CommandRow) { + QString lineText = getLineText(m_sci, 0); + ColumnSpan rts = commandRowRootTypeSpan(lineText); + if (rts.valid && hi.col >= rts.start && hi.col < rts.end) { + // Extract current keyword from span text + QString kw = lineText.mid(rts.start, rts.end - rts.start).trimmed(); + QMenu menu; + if (kw == QStringLiteral("class")) + menu.addAction("Convert to Struct"); + else if (kw == QStringLiteral("struct")) + menu.addAction("Convert to Class"); + // enum: no conversion options + if (!menu.isEmpty()) { + QAction* chosen = menu.exec(m_sci->mapToGlobal(pos)); + if (chosen) { + QString newKw = chosen->text().contains("Class") + ? QStringLiteral("class") : QStringLiteral("struct"); + emit keywordConvertRequested(newKw); + } + } + return; + } + } + int nodeIdx = -1; int subLine = 0; if (line >= 0 && line < m_meta.size()) { @@ -341,8 +371,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (!m_editState.active) return; if (id == 1 && (m_editState.target == EditTarget::Type || m_editState.target == EditTarget::ArrayElementType - || m_editState.target == EditTarget::PointerTarget - || m_editState.target == EditTarget::RootClassType)) { + || m_editState.target == EditTarget::PointerTarget)) { auto info = endInlineEdit(); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); } @@ -1469,8 +1498,7 @@ static bool hitTestTarget(QsciScintilla* sci, ColumnSpan as = commandRowAddrSpan(lineText); if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; } - ColumnSpan rts = commandRowRootTypeSpan(lineText); - if (inSpan(rts)) { outTarget = EditTarget::RootClassType; outLine = line; return true; } + // RootClassType is no longer clickable — use right-click to convert ColumnSpan rns = commandRowRootNameSpan(lineText); if (inSpan(rns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; } return false; @@ -2151,23 +2179,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { // and exit early above (never reach here). if (target == EditTarget::Source) QTimer::singleShot(0, this, &RcxEditor::showSourcePicker); - if (target == EditTarget::RootClassType) { - QTimer::singleShot(0, this, [this]() { - if (!m_editState.active || m_editState.target != EditTarget::RootClassType) return; - // Replace text with spaces and show picker - int len = m_editState.original.size(); - QString spaces(len, ' '); - m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, - m_editState.posStart, m_editState.posEnd); - m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL, - (uintptr_t)0, spaces.toUtf8().constData()); - m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n'); - m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW, - (uintptr_t)1, "struct\nclass\nenum"); - m_sci->viewport()->setCursor(Qt::ArrowCursor); - }); - } + // RootClassType is no longer editable via click — use right-click conversion instead // Refresh hover cursor so value history popup appears with Set buttons immediately if (target == EditTarget::Value) QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor); @@ -2444,8 +2456,7 @@ void RcxEditor::paintEditableSpans(int line) { fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); if (resolvedSpanFor(line, EditTarget::BaseAddress, norm)) fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); - if (resolvedSpanFor(line, EditTarget::RootClassType, norm)) - fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); + // RootClassType no longer shown as editable — right-click conversion instead if (resolvedSpanFor(line, EditTarget::RootClassName, norm)) fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); return; diff --git a/src/editor.h b/src/editor.h index 3a5f468..0c7f1fc 100644 --- a/src/editor.h +++ b/src/editor.h @@ -65,6 +65,7 @@ public: signals: void marginClicked(int margin, int line, Qt::KeyboardModifiers mods); void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos); + void keywordConvertRequested(const QString& newKeyword); void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods); void inlineEditCommitted(int nodeIdx, int subLine, EditTarget target, const QString& text); diff --git a/src/main.cpp b/src/main.cpp index 7270286..c700f32 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -399,8 +399,9 @@ inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequ void MainWindow::createMenus() { // File auto* file = m_titleBar->menuBar()->addMenu("&File"); - Qt5Qt6AddAction(file, "&New", QKeySequence::New, QIcon(), this, &MainWindow::newDocument); - Qt5Qt6AddAction(file, "New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newFile); + 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); file->addSeparator(); Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile); @@ -745,11 +746,12 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { } // Build a minimal empty struct for new documents -static void buildEmptyStruct(NodeTree& tree) { +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); @@ -765,47 +767,16 @@ static void buildEmptyStruct(NodeTree& tree) { } } -void MainWindow::newFile() { +void MainWindow::newClass() { + project_new(QStringLiteral("class")); +} + +void MainWindow::newStruct() { project_new(); } -void MainWindow::newDocument() { - auto* tab = activeTab(); - if (!tab) { - project_new(); - return; - } - auto* doc = tab->doc; - auto* ctrl = tab->ctrl; - - // Clear everything - doc->undoStack.clear(); - doc->tree = NodeTree(); - doc->tree.baseAddress = 0x00400000; - doc->filePath.clear(); - doc->typeAliases.clear(); - doc->modified = false; - - buildEmptyStruct(doc->tree); - - QByteArray data(256, '\0'); - doc->provider = std::make_shared(data); - - // Focus on first struct - ctrl->setViewRootId(0); - for (const auto& n : doc->tree.nodes) { - if (n.parentId == 0 && n.kind == NodeKind::Struct) { - ctrl->setViewRootId(n.id); - break; - } - } - ctrl->clearSelection(); - emit doc->documentChanged(); - - auto* sub = m_mdiArea->activeSubWindow(); - if (sub) sub->setWindowTitle(rootName(doc->tree, ctrl->viewRootId())); - updateWindowTitle(); - rebuildWorkspaceModel(); +void MainWindow::newEnum() { + project_new(QStringLiteral("enum")); } static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) { @@ -1560,14 +1531,14 @@ void MainWindow::showTypeAliasesDialog() { // ── Project Lifecycle API ── -QMdiSubWindow* MainWindow::project_new() { +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); + buildEmptyStruct(doc->tree, classKeyword); auto* sub = createTab(doc); rebuildWorkspaceModel(); @@ -1681,22 +1652,52 @@ void MainWindow::createWorkspaceDock() { auto structIdVar = index.data(Qt::UserRole + 1); uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0; - if (structId == 0 || structId == rcx::kGroupSentinel) return; + + // 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; - auto* deleteAction = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete"); - if (menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos)) == deleteAction) { - auto& tab = m_tabs[sub]; - int ni = tab.doc->tree.indexOfId(structId); - if (ni >= 0) { - tab.ctrl->removeNode(ni); - rebuildWorkspaceModel(); - } + 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) { + tab.ctrl->removeNode(ni); + 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(); } }); @@ -1897,27 +1898,11 @@ int main(int argc, char* argv[]) { rcx::MainWindow window; window.setWindowIcon(QIcon(":/icons/class.png")); - bool screenshotMode = app.arguments().contains("--screenshot"); - if (screenshotMode) - window.setWindowOpacity(0.0); window.show(); // Auto-open demo project from saved .rcx file QMetaObject::invokeMethod(&window, "selfTest"); - if (screenshotMode) { - QString out = "screenshot.png"; - int idx = app.arguments().indexOf("--screenshot"); - if (idx + 1 < app.arguments().size()) - out = app.arguments().at(idx + 1); - - QTimer::singleShot(1000, [&window, out]() { - QDir().mkpath(QFileInfo(out).absolutePath()); - window.grab().save(out); - ::_Exit(0); // immediate exit — no need for clean shutdown in screenshot mode - }); - } - return app.exec(); } diff --git a/src/mainwindow.h b/src/mainwindow.h index 761be59..32abe82 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -25,8 +25,9 @@ public: explicit MainWindow(QWidget* parent = nullptr); private slots: - void newFile(); - void newDocument(); + void newClass(); + void newStruct(); + void newEnum(); void selfTest(); void openFile(); void saveFile(); @@ -56,7 +57,7 @@ private slots: public: // Project Lifecycle API - QMdiSubWindow* project_new(); + QMdiSubWindow* project_new(const QString& classKeyword = QString()); QMdiSubWindow* project_open(const QString& path = {}); bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false); void project_close(QMdiSubWindow* sub = nullptr); diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index e28d4b6..d3246fa 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -1920,54 +1920,9 @@ private slots: } } - void testComputeStructAlignment() { - NodeTree tree; - tree.baseAddress = 0; - - Node root; - root.kind = NodeKind::Struct; - root.name = "Root"; - root.parentId = 0; - int ri = tree.addNode(root); - uint64_t rootId = tree.nodes[ri].id; - - // Int32 has alignment 4 - Node f1; - f1.kind = NodeKind::Int32; - f1.name = "a"; - f1.parentId = rootId; - f1.offset = 0; - tree.addNode(f1); - - QCOMPARE(tree.computeStructAlignment(rootId), 4); - - // Add Hex64 (alignment 8) — max should become 8 - Node f2; - f2.kind = NodeKind::Hex64; - f2.name = "b"; - f2.parentId = rootId; - f2.offset = 8; - tree.addNode(f2); - - QCOMPARE(tree.computeStructAlignment(rootId), 8); - } - - void testComputeStructAlignmentEmpty() { - NodeTree tree; - Node root; - root.kind = NodeKind::Struct; - root.name = "Empty"; - root.parentId = 0; - int ri = tree.addNode(root); - uint64_t rootId = tree.nodes[ri].id; - - // Empty struct → alignment 1 - QCOMPARE(tree.computeStructAlignment(rootId), 1); - } - void testCommandRowRootNameSpan() { // Name span should cover the class name in the merged command row - QString text = "source\u25BE \u00B7 0x0 \u00B7 struct\u25BE MyClass {"; + QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {"; ColumnSpan nameSpan = commandRowRootNameSpan(text); QVERIFY(nameSpan.valid); diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 3c0b0c5..2b665d3 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -941,19 +941,13 @@ private slots: // Set CommandRow text with root class (simulates controller.updateCommandRow) m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {")); + QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {")); // RootClassName should be allowed on CommandRow (line 0) bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0); QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow"); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); - - // RootClassType should be allowed on CommandRow (line 0) - ok = m_editor->beginInlineEdit(EditTarget::RootClassType, 0); - QVERIFY2(ok, "RootClassType edit should be allowed on CommandRow"); - QVERIFY(m_editor->isEditing()); - m_editor->cancelInlineEdit(); } // ── Test: CommandRow root class name editable ── @@ -962,7 +956,7 @@ private slots: // Set CommandRow with root class m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {")); + QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {")); // Line 0 is CommandRow const LineMeta* lm = m_editor->metaForLine(0); @@ -1008,7 +1002,7 @@ private slots: // Set command row text (simulates controller.updateCommandRow) QString cmdText = QStringLiteral( - "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"); + "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"); m_editor->setCommandRowText(cmdText); QApplication::processEvents(); @@ -1086,7 +1080,7 @@ private slots: m_editor->applyDocument(m_result); QString cmdText = QStringLiteral( - "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"); + "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"); m_editor->setCommandRowText(cmdText); QApplication::processEvents(); diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index f303bb8..cd92231 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -62,7 +62,7 @@ private slots: // ── Chevron span detection ── void testChevronSpanDetected() { - QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {"); + QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {"); ColumnSpan span = commandRowChevronSpan(text); QVERIFY(span.valid); QCOMPARE(span.start, 0); @@ -79,7 +79,7 @@ private slots: // ── Existing spans unbroken by chevron prefix ── void testSpansWithPrefix() { - QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {"); + QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {"); ColumnSpan src = commandRowSrcSpan(text); QVERIFY(src.valid);