feat: safe workspace tree deletion with reference cleanup and confirmation

- Add deleteRootStruct() that clears orphaned refId references before removal
- Show confirmation dialog listing all fields that reference the deleted type
- Auto-switch view to next root struct when the viewed one is deleted
- Entire operation is a single undo macro (Ctrl+Z restores everything)
This commit is contained in:
IChooseYou
2026-02-19 10:06:13 -07:00
parent 7678da033d
commit d989e2a947
3 changed files with 90 additions and 1 deletions

View File

@@ -776,6 +776,48 @@ void RcxController::removeNode(int nodeIdx) {
cmd::Remove{nodeId, subtree, adjs}));
}
void RcxController::deleteRootStruct(uint64_t structId) {
int ni = m_doc->tree.indexOfId(structId);
if (ni < 0) return;
const Node& node = m_doc->tree.nodes[ni];
if (node.parentId != 0 || node.kind != NodeKind::Struct) return;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Delete root struct"));
// Clear all refId references pointing to this struct
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
auto& n = m_doc->tree.nodes[i];
if (n.refId == structId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{n.id, n.refId, (uint64_t)0}));
}
}
// Remove the struct + subtree (re-lookup since commands may shift indices)
ni = m_doc->tree.indexOfId(structId);
if (ni >= 0)
removeNode(ni);
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
// Switch view if we just deleted the viewed root
if (m_viewRootId == structId) {
uint64_t nextRoot = 0;
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
nextRoot = n.id;
break;
}
}
setViewRootId(nextRoot);
}
if (!m_suppressRefresh) refresh();
}
void RcxController::toggleCollapse(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];

View File

@@ -99,6 +99,7 @@ public:
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
void batchRemoveNodes(const QVector<int>& nodeIndices);
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
void deleteRootStruct(uint64_t structId);
void applyCommand(const Command& cmd, bool isUndo);
void refresh();

View File

@@ -1698,7 +1698,53 @@ void MainWindow::createWorkspaceDock() {
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
if (chosen == actDelete) {
tab.ctrl->removeNode(ni);
QString typeName = tab.doc->tree.nodes[ni].structTypeName.isEmpty()
? tab.doc->tree.nodes[ni].name
: tab.doc->tree.nodes[ni].structTypeName;
if (typeName.isEmpty()) typeName = QStringLiteral("(unnamed)");
// Collect detailed reference info
QStringList refDetails;
for (const auto& n : tab.doc->tree.nodes) {
if (n.refId == structId) {
QString ownerName;
uint64_t pid = n.parentId;
while (pid != 0) {
int pi = tab.doc->tree.indexOfId(pid);
if (pi < 0) break;
if (tab.doc->tree.nodes[pi].parentId == 0) {
ownerName = tab.doc->tree.nodes[pi].structTypeName.isEmpty()
? tab.doc->tree.nodes[pi].name
: tab.doc->tree.nodes[pi].structTypeName;
break;
}
pid = tab.doc->tree.nodes[pi].parentId;
}
QString fieldDesc = ownerName.isEmpty()
? n.name
: QStringLiteral("%1::%2").arg(ownerName, n.name);
refDetails << QStringLiteral(" \u2022 %1 (%2)")
.arg(fieldDesc, kindToString(n.kind));
}
}
QString msg;
if (refDetails.isEmpty()) {
msg = QString("Delete '%1'?").arg(typeName);
} else {
msg = QString("Delete '%1'?\n\n"
"The following %2 field(s) reference this type "
"and will become untyped (void):\n\n%3")
.arg(typeName)
.arg(refDetails.size())
.arg(refDetails.join('\n'));
}
auto answer = QMessageBox::question(this, "Delete Type", msg,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer != QMessageBox::Yes) return;
tab.ctrl->deleteRootStruct(structId);
rebuildWorkspaceModel();
} else if (chosen && chosen == actConvert) {
QString newKw = kw == QStringLiteral("class")