mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -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} "
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -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<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
|
||||
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
|
||||
|
||||
@@ -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<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() {
|
||||
// -- 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;
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ public:
|
||||
void removeSplitEditor(RcxEditor* editor);
|
||||
QList<RcxEditor*> 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);
|
||||
|
||||
15
src/core.h
15
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<uint64_t> normalizePreferAncestors(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 ──
|
||||
// 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};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
121
src/main.cpp
121
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<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();
|
||||
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<QMdiSubWindow*>(subVar.value<void*>());
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user