feat: simplify cmd bar keyword, add File menu class/struct/enum, remove Align Members

- Command bar shows static keyword (struct/class/enum) without dropdown or colon
- Right-click keyword in cmd bar for class↔struct conversion (enum blocked)
- File menu: New Class (Ctrl+N), New Struct (Ctrl+T), New Enum (Ctrl+E)
- Project explorer right-click: New Class/Struct/Enum on Project node
- Explorer right-click: Convert to Class/Struct on class/struct items
- Remove Align Members submenu, performRealignment, computeStructAlignment
- Remove screenshot code and screenshot.png

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IChooseYou
2026-02-18 09:38:54 -07:00
parent 5b6e0473cb
commit 1cccd320b0
13 changed files with 133 additions and 338 deletions

View File

@@ -116,14 +116,6 @@ endforeach()
include(deploy) 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") set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
file(WRITE ${_combine_script} " file(WRITE ${_combine_script} "

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -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) // 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; LineMeta lm;
lm.nodeIdx = -1; lm.nodeIdx = -1;
@@ -743,20 +743,5 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
return result; return result;
} }
int NodeTree::computeStructAlignment(uint64_t structId) const {
int idx = indexOfId(structId);
if (idx < 0) return 1;
int maxAlign = 1;
QVector<int> 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 } // namespace rcx

View File

@@ -203,6 +203,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
this, [this, editor](int line, int nodeIdx, int subLine, QPoint globalPos) { this, [this, editor](int line, int nodeIdx, int subLine, QPoint globalPos) {
showContextMenu(editor, line, nodeIdx, subLine, globalPos); showContextMenu(editor, line, nodeIdx, subLine, globalPos);
}); });
connect(editor, &RcxEditor::keywordConvertRequested,
this, &RcxController::convertRootKeyword);
connect(editor, &RcxEditor::nodeClicked, connect(editor, &RcxEditor::nodeClicked,
this, [this, editor](int line, uint64_t nodeId, Qt::KeyboardModifiers mods) { this, [this, editor](int line, uint64_t nodeId, Qt::KeyboardModifiers mods) {
handleNodeClick(editor, line, nodeId, mods); handleNodeClick(editor, line, nodeId, mods);
@@ -729,6 +731,27 @@ void RcxController::refresh() {
applySelectionOverlays(); 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) { void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx]; 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]() { 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 ── // ── 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]() { menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0; uint64_t target = m_viewRootId ? m_viewRootId : 0;
m_suppressRefresh = true; m_suppressRefresh = true;
@@ -1670,112 +1650,6 @@ void RcxController::applySelectionOverlays() {
editor->applySelectionOverlay(m_selIds); 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<int> 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<NodeInfo> realNodes;
QVector<uint64_t> 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<OffChange> 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<PadInsert> 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<Node> 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() { void RcxController::updateCommandRow() {
// -- Source label: driven by provider metadata -- // -- Source label: driven by provider metadata --
@@ -1821,7 +1695,7 @@ void RcxController::updateCommandRow() {
const auto& n = m_doc->tree.nodes[vi]; const auto& n = m_doc->tree.nodes[vi];
QString keyword = n.resolvedClassKeyword(); QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; 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); .arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
} }
} }
@@ -1832,14 +1706,14 @@ void RcxController::updateCommandRow() {
if (n.parentId == 0 && n.kind == NodeKind::Struct) { if (n.parentId == 0 && n.kind == NodeKind::Struct) {
QString keyword = n.resolvedClassKeyword(); QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; 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); .arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
break; break;
} }
} }
} }
if (row2.isEmpty()) if (row2.isEmpty())
row2 = QStringLiteral("struct\u25BE NoName {"); row2 = QStringLiteral("struct NoName {");
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2; QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2;

View File

@@ -85,6 +85,7 @@ public:
void removeSplitEditor(RcxEditor* editor); void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; } QList<RcxEditor*> editors() const { return m_editors; }
void convertRootKeyword(const QString& newKeyword);
void changeNodeKind(int nodeIdx, NodeKind newKind); void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName); void renameNode(int nodeIdx, const QString& newName);
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name); void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
@@ -160,7 +161,6 @@ private:
void connectEditor(RcxEditor* editor); void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods); void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow(); void updateCommandRow();
void performRealignment(uint64_t structId, int targetAlign);
void switchToSavedSource(int idx); void switchToSavedSource(int idx);
void pushSavedSourcesToEditors(); void pushSavedSourcesToEditors();
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos); void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);

View File

@@ -380,9 +380,6 @@ struct NodeTree {
return qMax(declaredSize, maxEnd); return qMax(declaredSize, maxEnd);
} }
// Compute natural alignment of a struct (max alignment of direct children)
int computeStructAlignment(uint64_t structId) const;
// Batch selection normalizers // Batch selection normalizers
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const; QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const; QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
@@ -660,16 +657,17 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
} }
// ── CommandRow root-class spans ── // ── CommandRow root-class spans ──
// Combined CommandRow format ends with: " struct ClassName {" // Combined CommandRow format ends with: " struct ClassName {"
inline int commandRowRootStart(const QString& lineText) { inline int commandRowRootStart(const QString& lineText) {
int best = -1; int best = -1;
int i; 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; if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("class\u25BE")); i = lineText.lastIndexOf(QStringLiteral("class "));
if (i > best) best = i; if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE")); i = lineText.lastIndexOf(QStringLiteral("enum "));
if (i > best) best = i; if (i > best) best = i;
return best; return best;
} }
@@ -678,8 +676,7 @@ inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
int start = commandRowRootStart(lineText); int start = commandRowRootStart(lineText);
if (start < 0) return {}; if (start < 0) return {};
int end = start; int end = start;
while (end < lineText.size() && lineText[end] != QChar(' ') while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
&& lineText[end] != QChar(0x25BE)) end++;
if (end <= start) return {}; if (end <= start) return {};
return {start, end, true}; return {start, end, true};
} }

View File

@@ -25,6 +25,9 @@
namespace rcx { namespace rcx {
// Forward declaration (defined below, after RcxEditor constructor)
static QString getLineText(QsciScintilla* sci, int line);
// ── Value history popup (styled like TypeSelectorPopup) ── // ── Value history popup (styled like TypeSelectorPopup) ──
class ValueHistoryPopup : public QFrame { class ValueHistoryPopup : public QFrame {
@@ -327,6 +330,33 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
} }
HitInfo hi = hitTest(pos); HitInfo hi = hitTest(pos);
int line = hi.line; 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 nodeIdx = -1;
int subLine = 0; int subLine = 0;
if (line >= 0 && line < m_meta.size()) { if (line >= 0 && line < m_meta.size()) {
@@ -341,8 +371,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (!m_editState.active) return; if (!m_editState.active) return;
if (id == 1 && (m_editState.target == EditTarget::Type if (id == 1 && (m_editState.target == EditTarget::Type
|| m_editState.target == EditTarget::ArrayElementType || m_editState.target == EditTarget::ArrayElementType
|| m_editState.target == EditTarget::PointerTarget || m_editState.target == EditTarget::PointerTarget)) {
|| m_editState.target == EditTarget::RootClassType)) {
auto info = endInlineEdit(); auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
} }
@@ -1469,8 +1498,7 @@ static bool hitTestTarget(QsciScintilla* sci,
ColumnSpan as = commandRowAddrSpan(lineText); ColumnSpan as = commandRowAddrSpan(lineText);
if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; } if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; }
ColumnSpan rts = commandRowRootTypeSpan(lineText); // RootClassType is no longer clickable — use right-click to convert
if (inSpan(rts)) { outTarget = EditTarget::RootClassType; outLine = line; return true; }
ColumnSpan rns = commandRowRootNameSpan(lineText); ColumnSpan rns = commandRowRootNameSpan(lineText);
if (inSpan(rns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; } if (inSpan(rns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; }
return false; return false;
@@ -2151,23 +2179,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
// and exit early above (never reach here). // and exit early above (never reach here).
if (target == EditTarget::Source) if (target == EditTarget::Source)
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker); QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
if (target == EditTarget::RootClassType) { // RootClassType is no longer editable via click — use right-click conversion instead
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);
});
}
// Refresh hover cursor so value history popup appears with Set buttons immediately // Refresh hover cursor so value history popup appears with Set buttons immediately
if (target == EditTarget::Value) if (target == EditTarget::Value)
QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor); QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor);
@@ -2444,8 +2456,7 @@ void RcxEditor::paintEditableSpans(int line) {
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
if (resolvedSpanFor(line, EditTarget::BaseAddress, norm)) if (resolvedSpanFor(line, EditTarget::BaseAddress, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
if (resolvedSpanFor(line, EditTarget::RootClassType, norm)) // RootClassType no longer shown as editable — right-click conversion instead
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
if (resolvedSpanFor(line, EditTarget::RootClassName, norm)) if (resolvedSpanFor(line, EditTarget::RootClassName, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
return; return;

View File

@@ -65,6 +65,7 @@ public:
signals: signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods); void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos); 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 nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine, void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text); EditTarget target, const QString& text);

View File

@@ -399,8 +399,9 @@ inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequ
void MainWindow::createMenus() { void MainWindow::createMenus() {
// File // File
auto* file = m_titleBar->menuBar()->addMenu("&File"); auto* file = m_titleBar->menuBar()->addMenu("&File");
Qt5Qt6AddAction(file, "&New", QKeySequence::New, QIcon(), this, &MainWindow::newDocument); Qt5Qt6AddAction(file, "New &Class", QKeySequence::New, QIcon(), this, &MainWindow::newClass);
Qt5Qt6AddAction(file, "New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newFile); 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); Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile);
file->addSeparator(); file->addSeparator();
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile); 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 // Build a minimal empty struct for new documents
static void buildEmptyStruct(NodeTree& tree) { static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;
root.name = "instance"; root.name = "instance";
root.structTypeName = "Unnamed"; root.structTypeName = "Unnamed";
root.classKeyword = classKeyword;
root.parentId = 0; root.parentId = 0;
root.offset = 0; root.offset = 0;
int ri = tree.addNode(root); 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(); project_new();
} }
void MainWindow::newDocument() { void MainWindow::newEnum() {
auto* tab = activeTab(); project_new(QStringLiteral("enum"));
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<BufferProvider>(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();
} }
static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) { static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
@@ -1560,14 +1531,14 @@ void MainWindow::showTypeAliasesDialog() {
// ── Project Lifecycle API ── // ── Project Lifecycle API ──
QMdiSubWindow* MainWindow::project_new() { QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
auto* doc = new RcxDocument(this); auto* doc = new RcxDocument(this);
QByteArray data(256, '\0'); QByteArray data(256, '\0');
doc->loadData(data); doc->loadData(data);
doc->tree.baseAddress = 0x00400000; doc->tree.baseAddress = 0x00400000;
buildEmptyStruct(doc->tree); buildEmptyStruct(doc->tree, classKeyword);
auto* sub = createTab(doc); auto* sub = createTab(doc);
rebuildWorkspaceModel(); rebuildWorkspaceModel();
@@ -1681,22 +1652,52 @@ void MainWindow::createWorkspaceDock() {
auto structIdVar = index.data(Qt::UserRole + 1); auto structIdVar = index.data(Qt::UserRole + 1);
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0; 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); auto subVar = index.data(Qt::UserRole);
if (!subVar.isValid()) return; if (!subVar.isValid()) return;
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>()); auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
if (!sub || !m_tabs.contains(sub)) return; if (!sub || !m_tabs.contains(sub)) return;
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]; auto& tab = m_tabs[sub];
int ni = tab.doc->tree.indexOfId(structId); int ni = tab.doc->tree.indexOfId(structId);
if (ni >= 0) { 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) {
tab.ctrl->removeNode(ni); tab.ctrl->removeNode(ni);
rebuildWorkspaceModel(); 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; rcx::MainWindow window;
window.setWindowIcon(QIcon(":/icons/class.png")); window.setWindowIcon(QIcon(":/icons/class.png"));
bool screenshotMode = app.arguments().contains("--screenshot");
if (screenshotMode)
window.setWindowOpacity(0.0);
window.show(); window.show();
// Auto-open demo project from saved .rcx file // Auto-open demo project from saved .rcx file
QMetaObject::invokeMethod(&window, "selfTest"); 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(); return app.exec();
} }

View File

@@ -25,8 +25,9 @@ public:
explicit MainWindow(QWidget* parent = nullptr); explicit MainWindow(QWidget* parent = nullptr);
private slots: private slots:
void newFile(); void newClass();
void newDocument(); void newStruct();
void newEnum();
void selfTest(); void selfTest();
void openFile(); void openFile();
void saveFile(); void saveFile();
@@ -56,7 +57,7 @@ private slots:
public: public:
// Project Lifecycle API // Project Lifecycle API
QMdiSubWindow* project_new(); QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {}); QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false); bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr); void project_close(QMdiSubWindow* sub = nullptr);

View File

@@ -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() { void testCommandRowRootNameSpan() {
// Name span should cover the class name in the merged command row // 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); ColumnSpan nameSpan = commandRowRootNameSpan(text);
QVERIFY(nameSpan.valid); QVERIFY(nameSpan.valid);

View File

@@ -941,19 +941,13 @@ private slots:
// Set CommandRow text with root class (simulates controller.updateCommandRow) // Set CommandRow text with root class (simulates controller.updateCommandRow)
m_editor->setCommandRowText( 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) // RootClassName should be allowed on CommandRow (line 0)
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0); bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow"); QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow");
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit(); 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 ── // ── Test: CommandRow root class name editable ──
@@ -962,7 +956,7 @@ private slots:
// Set CommandRow with root class // Set CommandRow with root class
m_editor->setCommandRowText( m_editor->setCommandRowText(
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {")); QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
// Line 0 is CommandRow // Line 0 is CommandRow
const LineMeta* lm = m_editor->metaForLine(0); const LineMeta* lm = m_editor->metaForLine(0);
@@ -1008,7 +1002,7 @@ private slots:
// Set command row text (simulates controller.updateCommandRow) // Set command row text (simulates controller.updateCommandRow)
QString cmdText = QStringLiteral( QString cmdText = QStringLiteral(
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"); "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
m_editor->setCommandRowText(cmdText); m_editor->setCommandRowText(cmdText);
QApplication::processEvents(); QApplication::processEvents();
@@ -1086,7 +1080,7 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
QString cmdText = QStringLiteral( QString cmdText = QStringLiteral(
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"); "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
m_editor->setCommandRowText(cmdText); m_editor->setCommandRowText(cmdText);
QApplication::processEvents(); QApplication::processEvents();

View File

@@ -62,7 +62,7 @@ private slots:
// ── Chevron span detection ── // ── Chevron span detection ──
void testChevronSpanDetected() { 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); ColumnSpan span = commandRowChevronSpan(text);
QVERIFY(span.valid); QVERIFY(span.valid);
QCOMPARE(span.start, 0); QCOMPARE(span.start, 0);
@@ -79,7 +79,7 @@ private slots:
// ── Existing spans unbroken by chevron prefix ── // ── Existing spans unbroken by chevron prefix ──
void testSpansWithPrefix() { 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); ColumnSpan src = commandRowSrcSpan(text);
QVERIFY(src.valid); QVERIFY(src.valid);