perf: compose 30% faster — move semantics, BFS offsets, zero-alloc hex formatting

- compose.cpp: emitLine takes LineMeta&& (move, not copy) at all 22 call sites
- compose.cpp: reserve meta/text buffers, BFS offset computation O(N) vs O(N*D)
- compose.cpp: pre-compute typeNameLens[], merge global width loops
- format.cpp: bytesToHex uses stack buffer + lookup table (zero heap allocs)
- format.cpp: hexVal single QString::asprintf instead of 2-string concat
- editor.cpp: guard hover updates during applyDocument (stale index safety)
- core.h: assertion on makeArrayElemSelId negative index
- format.cpp: assertion on extractBits overflow
- main.cpp: tree lines enabled by default
- bench_large_class: add 2000-field benchComposeLarge test

Benchmark: 500 fields 0.70→0.51ms (27%), 2000 fields 2.28→1.57ms (31%)
This commit is contained in:
IChooseYou
2026-03-08 07:28:26 -06:00
committed by IChooseYou
parent f0fc85f60f
commit 596f410b96
8 changed files with 150 additions and 63 deletions

View File

@@ -53,7 +53,7 @@ struct ComposeState {
siblingStack[d] = hasMoreSiblings;
}
void emitLine(const QString& lineText, LineMeta lm) {
void emitLine(const QString& lineText, LineMeta&& lm) {
if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
// CommandRow has no fold prefix (flush left)
@@ -87,7 +87,7 @@ struct ComposeState {
text += lineText;
}
meta.append(lm);
meta.append(std::move(lm));
currentLine++;
}
};
@@ -208,7 +208,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
state.compactColumns);
state.emitLine(lineText, lm);
state.emitLine(lineText, std::move(lm));
}
}
@@ -246,7 +246,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
lm.foldLevel = computeFoldLevel(depth, false);
state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") +
node.name + QStringLiteral(" */"), lm);
node.name + QStringLiteral(" */"), std::move(lm));
return;
}
state.visiting.insert(node.id);
@@ -267,7 +267,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.arrayElementIdx = arrayElementIdx;
uint64_t relOff = absAddr - arrayContainerAddr;
QString relOffHex = QString::number(relOff, 16).toUpper();
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), lm);
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), std::move(lm));
}
// Detect root header: first root-level struct — suppressed from display
@@ -325,7 +325,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
headerText.chop(1);
// Remove trailing separator spaces
while (headerText.endsWith(' ')) headerText.chop(1);
state.emitLine(headerText, lm);
state.emitLine(headerText, std::move(lm));
// Emit standalone brace line
LineMeta braceLm;
braceLm.nodeIdx = nodeIdx;
@@ -334,9 +334,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = computeFoldLevel(depth, true);
braceLm.markerMask = (1u << M_STRUCT_BG);
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
} else {
state.emitLine(headerText, lm);
state.emitLine(headerText, std::move(lm));
}
}
@@ -372,7 +372,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm);
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), std::move(lm));
}
// Footer
@@ -389,7 +389,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
state.emitLine(fmt::fmtStructFooter(node, depth, 0), std::move(lm));
}
state.visiting.remove(node.id);
@@ -423,7 +423,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
childDepth, maxNameLen), lm);
childDepth, maxNameLen), std::move(lm));
}
// Footer
@@ -441,7 +441,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
}
state.visiting.remove(node.id);
@@ -501,7 +501,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
{}, eTW, eNW, elemTypeStr,
state.compactColumns), lm);
state.compactColumns), std::move(lm));
}
}
@@ -559,7 +559,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
state.emitLine(fmt::fmtStructHeader(child, childDepth,
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
/*collapsed=*/true, typeW, nameW, state.compactColumns), std::move(lm));
continue;
}
composeNode(state, tree, prov, childIdx, childDepth,
@@ -700,7 +700,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.ptrBase = state.currentPtrBase;
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
lm.effectiveNameW = sf.name.size();
state.emitLine(headerLine, lm);
state.emitLine(headerLine, std::move(lm));
// ── Body + children (only when expanded) ──
if (!isCollapsed) {
@@ -747,7 +747,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
blm.offsetAddr = staticAddr;
blm.ptrBase = state.currentPtrBase;
state.emitLine(bodyLine, blm);
state.emitLine(bodyLine, std::move(blm));
}
// If struct/array, compose children at evaluated address
@@ -780,7 +780,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
flm.offsetAddr = staticAddr;
}
flm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm);
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), std::move(flm));
}
}
}
@@ -802,7 +802,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
}
state.visiting.remove(node.id);
@@ -865,7 +865,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
ptrText.chop(1);
while (ptrText.endsWith(' ')) ptrText.chop(1);
state.emitLine(ptrText, lm);
state.emitLine(ptrText, std::move(lm));
LineMeta braceLm;
braceLm.nodeIdx = nodeIdx;
braceLm.nodeId = node.id;
@@ -873,9 +873,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = computeFoldLevel(depth, true);
braceLm.markerMask = lm.markerMask;
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
} else {
state.emitLine(ptrText, lm);
state.emitLine(ptrText, std::move(lm));
}
}
}
@@ -955,7 +955,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.offsetText.clear();
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), lm);
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), std::move(lm));
}
}
return;
@@ -988,10 +988,32 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
});
}
// Precompute absolute offsets (baseAddress + structure-relative offset)
// Pre-allocate output buffers (estimate ~3 lines per node, ~80 chars per line)
state.meta.reserve(tree.nodes.size() * 3);
state.text.reserve(tree.nodes.size() * 80);
// Precompute absolute offsets via BFS (O(N) — avoids per-node parent-chain walk)
state.absOffsets.resize(tree.nodes.size());
state.absOffsets.fill(0);
for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
if (tree.nodes[i].parentId == 0)
state.absOffsets[i] = tree.nodes[i].offset;
{
QVector<int> bfsQueue;
for (int i : state.childMap.value(0))
bfsQueue.append(i);
int front = 0;
while (front < bfsQueue.size()) {
int idx = bfsQueue[front++];
int pi = tree.indexOfId(tree.nodes[idx].parentId);
state.absOffsets[idx] = (pi >= 0 ? state.absOffsets[pi] : 0)
+ tree.nodes[idx].offset;
for (int ci : state.childMap.value(tree.nodes[idx].id))
bfsQueue.append(ci);
}
}
for (auto& v : state.absOffsets)
v += tree.baseAddress;
// Compute hex digit tier from max absolute address
{
@@ -1020,23 +1042,21 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
return fmt::typeNameRaw(n.kind);
};
// Compute effective type column width from longest type name
// Include struct/array headers which use "struct TypeName" or "type[count]" format
// Pre-compute type name lengths (avoids re-creating temp QStrings in width loops)
QVector<int> typeNameLens(tree.nodes.size());
for (int i = 0; i < tree.nodes.size(); i++)
typeNameLens[i] = nodeTypeName(tree.nodes[i]).size();
// Compute effective column widths from longest type/name in a single pass
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
int maxTypeLen = kMinTypeW;
for (const Node& node : tree.nodes) {
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
int maxNameLen = kMinNameW;
for (int i = 0; i < tree.nodes.size(); i++) {
maxTypeLen = qMax(maxTypeLen, typeNameLens[i]);
if (!isHexPreview(tree.nodes[i].kind))
maxNameLen = qMax(maxNameLen, (int)tree.nodes[i].name.size());
}
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
// Compute effective name column width from longest name
// Include struct/array names - they now use columnar layout too
int maxNameLen = kMinNameW;
for (const Node& node : tree.nodes) {
// Skip hex (they show ASCII preview, not name column)
if (isHexPreview(node.kind)) continue;
maxNameLen = qMax(maxNameLen, (int)node.name.size());
}
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
// Pre-compute per-scope widths (each container gets widths based on direct children only)
@@ -1053,7 +1073,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
// Skip struct children — pointer headers shouldn't inflate sibling widths
if (child.kind == NodeKind::Struct)
continue;
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
scopeMaxType = qMax(scopeMaxType, typeNameLens[childIdx]);
// Name width (skip hex, but include containers)
if (!isHexPreview(child.kind)) {
@@ -1079,7 +1099,6 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
}
// Compute scope widths for root level (parentId == 0)
// Include struct/array headers - they now use columnar layout too
{
int rootMaxType = kMinTypeW;
int rootMaxName = kMinNameW;
@@ -1088,7 +1107,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
// Skip struct children — pointer headers shouldn't inflate sibling widths
if (child.kind == NodeKind::Struct)
continue;
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
rootMaxType = qMax(rootMaxType, typeNameLens[childIdx]);
// Name width (skip hex, include containers)
if (!isHexPreview(child.kind)) {
@@ -1115,7 +1134,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.markerMask = 0;
lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW;
state.emitLine(cmdRowText, lm);
state.emitLine(cmdRowText, std::move(lm));
}
// Brace wrapping: emit standalone "{" after CommandRow
@@ -1127,7 +1146,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = SC_FOLDLEVELBASE;
braceLm.markerMask = 0;
state.emitLine(QStringLiteral("{"), braceLm);
state.emitLine(QStringLiteral("{"), std::move(braceLm));
}
const QVector<int>& roots = childIndices(state, 0);

View File

@@ -575,6 +575,7 @@ static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
Q_ASSERT(elemIdx >= 0);
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
}
inline int arrayElemIdxFromSelId(uint64_t selId) {

View File

@@ -562,9 +562,11 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (chosen == actRel && !m_relativeOffsets) {
m_relativeOffsets = true;
reformatMargins();
emit relativeOffsetsChanged(true);
} else if (chosen == actAbs && m_relativeOffsets) {
m_relativeOffsets = false;
reformatMargins();
emit relativeOffsetsChanged(false);
}
return;
}
@@ -958,8 +960,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
}
// Dynamically resize margin to fit the current hex digit tier
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
// RVA mode uses half width since relative offsets are much shorter
{
int marginDigits = m_relativeOffsets
? qMax(m_layout.offsetHexDigits / 2, 4)
: m_layout.offsetHexDigits;
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
}
m_sci->setReadOnly(false);
m_sci->setText(result.text);
@@ -1066,6 +1074,11 @@ void RcxEditor::reformatMargins() {
uint64_t base = m_layout.baseAddress;
int hexDigits = m_layout.offsetHexDigits;
// Resize margin: RVA offsets are much shorter than full addresses
int marginDigits = m_relativeOffsets ? qMax(hexDigits / 2, 4) : hexDigits;
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
// ── Pass 1: margin text (global offset only) ──
m_sci->clearMarginText(-1);
for (int i = 0; i < m_meta.size(); i++) {
@@ -2195,6 +2208,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
#endif
m_relativeOffsets = !m_relativeOffsets;
reformatMargins();
emit relativeOffsetsChanged(m_relativeOffsets);
return true;
}
}
@@ -2274,7 +2288,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
}
// Resolve hovered nodeId on move/wheel (non-edit mode only)
if (!m_editState.active &&
// Guard: skip during applyDocument — m_nodeLineIndex may be stale
if (!m_editState.active && !m_applyingDocument &&
(event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel)) {
auto h = hitTest(m_lastHoverPos);
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
@@ -3602,8 +3617,13 @@ void RcxEditor::setEditorFont(const QString& fontName) {
// Re-apply margin styles and width with new font metrics
allocateMarginStyles();
applyTheme(ThemeManager::instance().current());
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
{
int marginDigits = m_relativeOffsets
? qMax(m_layout.offsetHexDigits / 2, 4)
: m_layout.offsetHexDigits;
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
}
}
void RcxEditor::setGlobalFontName(const QString& fontName) {

View File

@@ -84,6 +84,7 @@ signals:
void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind);
void relativeOffsetsChanged(bool relative);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;

View File

@@ -73,7 +73,7 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) {
// ── Value formatting ──
static QString hexVal(uint64_t v) {
return QStringLiteral("0x") + QString::number(v, 16);
return QString::asprintf("0x%llx", (unsigned long long)v);
}
static QString rawHex(uint64_t v, int digits) {
@@ -228,15 +228,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) {
return out;
}
static const char kHexDigits[] = "0123456789ABCDEF";
static QString bytesToHex(const QByteArray& b, int slot) {
QString out;
out.reserve(slot * 3);
QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty
int pos = 0;
for (int i = 0; i < slot; ++i) {
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
out += QString::asprintf("%02X", (unsigned)c);
if (i + 1 < slot) out += ' ';
buf[pos++] = QLatin1Char(kHexDigits[c >> 4]);
buf[pos++] = QLatin1Char(kHexDigits[c & 0xF]);
if (i + 1 < slot) buf[pos++] = QLatin1Char(' ');
}
return out;
return QString(buf, pos);
}
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
@@ -715,6 +718,7 @@ uint64_t extractBits(const Provider& prov, uint64_t addr,
case NodeKind::Hex32: container = prov.readU32(addr); break;
default: container = prov.readU64(addr); break;
}
Q_ASSERT(bitOffset + bitWidth <= 64);
if (bitWidth >= 64) return container >> bitOffset;
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
}

View File

@@ -818,17 +818,17 @@ void MainWindow::createMenus() {
auto* actTreeLines = view->addAction("&Tree Lines");
actTreeLines->setCheckable(true);
actTreeLines->setChecked(settings.value("treeLines", false).toBool());
actTreeLines->setChecked(settings.value("treeLines", true).toBool());
connect(actTreeLines, &QAction::triggered, this, [this](bool checked) {
QSettings("Reclass", "Reclass").setValue("treeLines", checked);
for (auto& tab : m_tabs)
tab.ctrl->setTreeLines(checked);
});
auto* actRelOfs = view->addAction("R&elative Offsets");
actRelOfs->setCheckable(true);
actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
connect(actRelOfs, &QAction::triggered, this, [this](bool checked) {
m_actRelOfs = view->addAction("R&elative Offsets");
m_actRelOfs->setCheckable(true);
m_actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
connect(m_actRelOfs, &QAction::triggered, this, [this](bool checked) {
QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked);
for (auto& tab : m_tabs)
for (auto& pane : tab.panes)
@@ -1250,6 +1250,16 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
pane.editor->setRelativeOffsets(
QSettings("Reclass", "Reclass").value("relativeOffsets", true).toBool());
// Sync View menu checkbox when editor toggles offset mode (double-click / context menu)
connect(pane.editor, &RcxEditor::relativeOffsetsChanged, this, [this](bool rel) {
QSettings("Reclass", "Reclass").setValue("relativeOffsets", rel);
if (m_actRelOfs) m_actRelOfs->setChecked(rel);
// Propagate to all other editors so they stay in sync
for (auto& tab : m_tabs)
for (auto& p : tab.panes)
if (p.editor && p.editor != sender())
p.editor->setRelativeOffsets(rel);
});
pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0
// Create per-pane rendered C++ view with find bar
@@ -1463,6 +1473,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
dock->setFeatures(QDockWidget::DockWidgetClosable |
QDockWidget::DockWidgetMovable |
QDockWidget::DockWidgetFloatable);
dock->setAttribute(Qt::WA_DeleteOnClose);
// Two title bar widgets: a hidden one (docked) and a draggable one (floating)
auto* emptyTitleBar = new QWidget(dock);
emptyTitleBar->setFixedHeight(0);
@@ -1586,7 +1597,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
// Apply global compact columns setting to new tab
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", false).toBool());
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", true).toBool());
ctrl->setBraceWrap(QSettings("Reclass", "Reclass").value("braceWrap", false).toBool());
// Give every controller the shared document list for cross-tab type visibility
@@ -1629,6 +1640,8 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
rebuildAllDocs();
rebuildWorkspaceModel();
if (m_tabs.isEmpty())
project_new();
});
connect(ctrl, &RcxController::nodeSelected,
@@ -3309,16 +3322,13 @@ void MainWindow::project_close(QDockWidget* dock) {
if (!dock) dock = m_activeDocDock;
if (!dock) return;
dock->close();
rebuildWorkspaceModel();
}
void MainWindow::closeAllDocDocks() {
// Take a copy since closing modifies m_docDocks via destroyed signal
auto docks = m_docDocks;
for (auto* dock : docks) {
dock->setAttribute(Qt::WA_DeleteOnClose);
for (auto* dock : docks)
dock->close();
}
}

View File

@@ -92,6 +92,7 @@ private:
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
QAction* m_actRelOfs = nullptr;
QMenu* m_sourceMenu = nullptr;
QMenu* m_recentFilesMenu = nullptr;