mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Merge remote-tracking branch 'origin/floating'
This commit is contained in:
@@ -25,6 +25,7 @@ struct ComposeState {
|
|||||||
bool baseEmitted = false; // only first root struct shows base address
|
bool baseEmitted = false; // only first root struct shows base address
|
||||||
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||||
bool treeLines = false; // draw Unicode tree connectors in indentation
|
bool treeLines = false; // draw Unicode tree connectors in indentation
|
||||||
|
bool braceWrap = false; // opening brace on its own line
|
||||||
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
||||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||||
|
|
||||||
@@ -319,7 +320,24 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
||||||
}
|
}
|
||||||
|
// Brace wrapping: move trailing '{' to its own line
|
||||||
|
if (state.braceWrap && !node.collapsed && headerText.endsWith(QChar('{'))) {
|
||||||
|
headerText.chop(1);
|
||||||
|
// Remove trailing separator spaces
|
||||||
|
while (headerText.endsWith(' ')) headerText.chop(1);
|
||||||
state.emitLine(headerText, lm);
|
state.emitLine(headerText, lm);
|
||||||
|
// Emit standalone brace line
|
||||||
|
LineMeta braceLm;
|
||||||
|
braceLm.nodeIdx = nodeIdx;
|
||||||
|
braceLm.nodeId = node.id;
|
||||||
|
braceLm.depth = depth;
|
||||||
|
braceLm.lineKind = LineKind::Header;
|
||||||
|
braceLm.foldLevel = computeFoldLevel(depth, true);
|
||||||
|
braceLm.markerMask = (1u << M_STRUCT_BG);
|
||||||
|
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
|
||||||
|
} else {
|
||||||
|
state.emitLine(headerText, lm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node.collapsed || isArrayChild || isRootHeader) {
|
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||||
@@ -840,9 +858,26 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
lm.pointerTargetName = ptrTargetName;
|
lm.pointerTargetName = ptrTargetName;
|
||||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
{
|
||||||
|
QString ptrText = fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||||
prov, absAddr, ptrTypeOverride,
|
prov, absAddr, ptrTypeOverride,
|
||||||
typeW, nameW, state.compactColumns), lm);
|
typeW, nameW, state.compactColumns);
|
||||||
|
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
|
||||||
|
ptrText.chop(1);
|
||||||
|
while (ptrText.endsWith(' ')) ptrText.chop(1);
|
||||||
|
state.emitLine(ptrText, lm);
|
||||||
|
LineMeta braceLm;
|
||||||
|
braceLm.nodeIdx = nodeIdx;
|
||||||
|
braceLm.nodeId = node.id;
|
||||||
|
braceLm.depth = depth;
|
||||||
|
braceLm.lineKind = LineKind::Header;
|
||||||
|
braceLm.foldLevel = computeFoldLevel(depth, true);
|
||||||
|
braceLm.markerMask = lm.markerMask;
|
||||||
|
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
|
||||||
|
} else {
|
||||||
|
state.emitLine(ptrText, lm);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!effectiveCollapsed) {
|
if (!effectiveCollapsed) {
|
||||||
@@ -936,10 +971,11 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||||
bool compactColumns, bool treeLines) {
|
bool compactColumns, bool treeLines, bool braceWrap) {
|
||||||
ComposeState state;
|
ComposeState state;
|
||||||
state.compactColumns = compactColumns;
|
state.compactColumns = compactColumns;
|
||||||
state.treeLines = treeLines;
|
state.treeLines = treeLines;
|
||||||
|
state.braceWrap = braceWrap;
|
||||||
|
|
||||||
// Precompute parent→children map
|
// Precompute parent→children map
|
||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
@@ -1082,6 +1118,18 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
state.emitLine(cmdRowText, lm);
|
state.emitLine(cmdRowText, lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Brace wrapping: emit standalone "{" after CommandRow
|
||||||
|
if (state.braceWrap) {
|
||||||
|
LineMeta braceLm;
|
||||||
|
braceLm.nodeIdx = -1;
|
||||||
|
braceLm.nodeId = 0; // not associated with any node (no hover)
|
||||||
|
braceLm.depth = 0;
|
||||||
|
braceLm.lineKind = LineKind::Header;
|
||||||
|
braceLm.foldLevel = SC_FOLDLEVELBASE;
|
||||||
|
braceLm.markerMask = 0;
|
||||||
|
state.emitLine(QStringLiteral("{"), braceLm);
|
||||||
|
}
|
||||||
|
|
||||||
const QVector<int>& roots = childIndices(state, 0);
|
const QVector<int>& roots = childIndices(state, 0);
|
||||||
|
|
||||||
for (int idx : roots) {
|
for (int idx : roots) {
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
||||||
bool treeLines) const {
|
bool treeLines, bool braceWrap) const {
|
||||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines);
|
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RcxDocument::save(const QString& path) {
|
bool RcxDocument::save(const QString& path) {
|
||||||
@@ -558,9 +558,9 @@ void RcxController::refresh() {
|
|||||||
|
|
||||||
// Compose against snapshot provider if active, otherwise real provider
|
// Compose against snapshot provider if active, otherwise real provider
|
||||||
if (m_snapshotProv)
|
if (m_snapshotProv)
|
||||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines);
|
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
|
||||||
else
|
else
|
||||||
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines);
|
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
|
||||||
|
|
||||||
s_composeDoc = nullptr;
|
s_composeDoc = nullptr;
|
||||||
|
|
||||||
@@ -1766,6 +1766,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
QApplication::clipboard()->setText(addrs.join('\n'));
|
QApplication::clipboard()->setText(addrs.join('\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emit contextMenuAboutToShow(&menu, line);
|
||||||
menu.exec(globalPos);
|
menu.exec(globalPos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2282,6 +2283,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
emit contextMenuAboutToShow(&menu, line);
|
||||||
menu.exec(globalPos);
|
menu.exec(globalPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2442,6 +2444,7 @@ void RcxController::updateCommandRow() {
|
|||||||
.arg(elide(src, 40), elide(addr, 24));
|
.arg(elide(src, 40), elide(addr, 24));
|
||||||
|
|
||||||
// Build row 2: root class type + name (uses current view root)
|
// Build row 2: root class type + name (uses current view root)
|
||||||
|
QString brace = m_braceWrap ? QString() : QStringLiteral(" {");
|
||||||
QString row2;
|
QString row2;
|
||||||
if (m_viewRootId != 0) {
|
if (m_viewRootId != 0) {
|
||||||
int vi = m_doc->tree.indexOfId(m_viewRootId);
|
int vi = m_doc->tree.indexOfId(m_viewRootId);
|
||||||
@@ -2449,8 +2452,8 @@ 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 %2 {")
|
row2 = QStringLiteral("%1 %2%3")
|
||||||
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
|
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (row2.isEmpty()) {
|
if (row2.isEmpty()) {
|
||||||
@@ -2460,14 +2463,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 %2 {")
|
row2 = QStringLiteral("%1 %2%3")
|
||||||
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
|
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className, brace);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (row2.isEmpty())
|
if (row2.isEmpty())
|
||||||
row2 = QStringLiteral("struct NoName {");
|
row2 = QStringLiteral("struct NoName") + brace;
|
||||||
|
|
||||||
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
||||||
|
|
||||||
@@ -3259,6 +3262,11 @@ void RcxController::setTreeLines(bool v) {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::setBraceWrap(bool v) {
|
||||||
|
m_braceWrap = v;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::setupAutoRefresh() {
|
void RcxController::setupAutoRefresh() {
|
||||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
m_refreshTimer = new QTimer(this);
|
m_refreshTimer = new QTimer(this);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
||||||
bool treeLines = false) const;
|
bool treeLines = false, bool braceWrap = false) const;
|
||||||
bool save(const QString& path);
|
bool save(const QString& path);
|
||||||
bool load(const QString& path);
|
bool load(const QString& path);
|
||||||
void loadData(const QString& binaryPath);
|
void loadData(const QString& binaryPath);
|
||||||
@@ -130,6 +130,7 @@ public:
|
|||||||
void setRefreshInterval(int ms);
|
void setRefreshInterval(int ms);
|
||||||
void setCompactColumns(bool v);
|
void setCompactColumns(bool v);
|
||||||
void setTreeLines(bool v);
|
void setTreeLines(bool v);
|
||||||
|
void setBraceWrap(bool v);
|
||||||
void resetProvider();
|
void resetProvider();
|
||||||
|
|
||||||
// MCP bridge accessors
|
// MCP bridge accessors
|
||||||
@@ -158,6 +159,7 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void nodeSelected(int nodeIdx);
|
void nodeSelected(int nodeIdx);
|
||||||
void selectionChanged(int count);
|
void selectionChanged(int count);
|
||||||
|
void contextMenuAboutToShow(QMenu* menu, int line);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RcxDocument* m_doc;
|
RcxDocument* m_doc;
|
||||||
@@ -168,6 +170,7 @@ private:
|
|||||||
bool m_suppressRefresh = false;
|
bool m_suppressRefresh = false;
|
||||||
bool m_compactColumns = false;
|
bool m_compactColumns = false;
|
||||||
bool m_treeLines = false;
|
bool m_treeLines = false;
|
||||||
|
bool m_braceWrap = false;
|
||||||
uint64_t m_viewRootId = 0;
|
uint64_t m_viewRootId = 0;
|
||||||
|
|
||||||
// ── Saved sources for quick-switch ──
|
// ── Saved sources for quick-switch ──
|
||||||
|
|||||||
@@ -1031,6 +1031,7 @@ namespace fmt {
|
|||||||
// ── Compose function forward declaration ──
|
// ── Compose function forward declaration ──
|
||||||
|
|
||||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||||
bool compactColumns = false, bool treeLines = false);
|
bool compactColumns = false, bool treeLines = false,
|
||||||
|
bool braceWrap = false);
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
@@ -938,9 +939,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
int maxLen = 0;
|
int maxLen = 0;
|
||||||
const QStringList lines = result.text.split(QChar('\n'));
|
const QStringList lines = result.text.split(QChar('\n'));
|
||||||
for (const auto& line : lines) {
|
for (const auto& line : lines) {
|
||||||
int len = line.size();
|
int len = (int)line.size();
|
||||||
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
||||||
if (len > maxLen) maxLen = len;
|
maxLen = std::max(len, maxLen);
|
||||||
}
|
}
|
||||||
QFontMetrics fm(editorFont());
|
QFontMetrics fm(editorFont());
|
||||||
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
||||||
|
|||||||
648
src/main.cpp
648
src/main.cpp
@@ -9,8 +9,6 @@
|
|||||||
#include "mcp/mcp_bridge.h"
|
#include "mcp/mcp_bridge.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMdiArea>
|
|
||||||
#include <QMdiSubWindow>
|
|
||||||
#include <QMenuBar>
|
#include <QMenuBar>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QStatusBar>
|
#include <QStatusBar>
|
||||||
@@ -469,23 +467,12 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
overlay->raise();
|
overlay->raise();
|
||||||
overlay->show();
|
overlay->show();
|
||||||
|
|
||||||
m_mdiArea = new QMdiArea(this);
|
m_centralPlaceholder = new QWidget(this);
|
||||||
m_mdiArea->setFrameShape(QFrame::NoFrame);
|
m_centralPlaceholder->setFixedSize(0, 0);
|
||||||
m_mdiArea->setViewMode(QMdiArea::TabbedView);
|
m_centralPlaceholder->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||||
m_mdiArea->setTabsClosable(true);
|
setCentralWidget(m_centralPlaceholder);
|
||||||
m_mdiArea->setTabsMovable(true);
|
setDockNestingEnabled(true);
|
||||||
{
|
setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North);
|
||||||
const auto& t = ThemeManager::instance().current();
|
|
||||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
|
||||||
"QTabBar::tab {"
|
|
||||||
" background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;"
|
|
||||||
"}"
|
|
||||||
"QTabBar::tab:selected { color: %3; background: %4; }"
|
|
||||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
|
||||||
.arg(t.background.name(), t.textMuted.name(), t.text.name(),
|
|
||||||
t.backgroundAlt.name(), t.hover.name()));
|
|
||||||
}
|
|
||||||
setCentralWidget(m_mdiArea);
|
|
||||||
|
|
||||||
createWorkspaceDock();
|
createWorkspaceDock();
|
||||||
createScannerDock();
|
createScannerDock();
|
||||||
@@ -521,10 +508,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
if (QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool())
|
if (QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool())
|
||||||
m_mcp->start();
|
m_mcp->start();
|
||||||
|
|
||||||
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
// Active doc tracking is handled per dock in createTab() via visibilityChanged
|
||||||
this, [this](QMdiSubWindow*) {
|
|
||||||
updateWindowTitle();
|
// Ensure border overlay is on top after initial layout settles
|
||||||
rebuildWorkspaceModel();
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
if (m_borderOverlay) {
|
||||||
|
m_borderOverlay->setGeometry(rect());
|
||||||
|
m_borderOverlay->raise();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track which split pane has focus (for menu-driven view switching)
|
// Track which split pane has focus (for menu-driven view switching)
|
||||||
@@ -1006,31 +997,21 @@ private:
|
|||||||
int m_divX = -1;
|
int m_divX = -1;
|
||||||
|
|
||||||
void manualLayout() {
|
void manualLayout() {
|
||||||
if (!tabRow || !label) return;
|
if (!label) return;
|
||||||
const int h = height();
|
const int h = height();
|
||||||
const int tw = tabRow->sizeHint().width();
|
|
||||||
const int gutter = 6;
|
const int gutter = 6;
|
||||||
|
if (tabRow) {
|
||||||
|
const int tw = tabRow->sizeHint().width();
|
||||||
tabRow->setGeometry(0, 0, tw, h);
|
tabRow->setGeometry(0, 0, tw, h);
|
||||||
m_divX = tw;
|
m_divX = tw;
|
||||||
label->setGeometry(tw + 1 + gutter, 0,
|
label->setGeometry(tw + 1 + gutter, 0,
|
||||||
qMax(0, width() - (tw + 1 + gutter)), h);
|
qMax(0, width() - (tw + 1 + gutter)), h);
|
||||||
|
} else {
|
||||||
// Shared baseline so tab text and status text align.
|
m_divX = -1;
|
||||||
// Nudge up by half the accent-line height so text centres
|
label->setGeometry(gutter, 0, qMax(0, width() - gutter), h);
|
||||||
// in the visible area below the accent bar, not in the full bar.
|
|
||||||
QFontMetrics fm(font());
|
|
||||||
int by = (h + fm.ascent()) / 2 - (ViewTabButton::kAccentH + 1) / 2;
|
|
||||||
|
|
||||||
// Push baseline to buttons
|
|
||||||
auto* lay = tabRow->layout();
|
|
||||||
if (lay) {
|
|
||||||
for (int i = 0; i < lay->count(); i++)
|
|
||||||
static_cast<ViewTabButton*>(lay->itemAt(i)->widget())->baselineY = by;
|
|
||||||
}
|
}
|
||||||
// Align label: set top margin so text baseline matches
|
label->setContentsMargins(0, 0, 0, 0);
|
||||||
int labelTop = by - fm.ascent();
|
label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
||||||
label->setContentsMargins(0, labelTop, 0, 0);
|
|
||||||
label->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1045,35 +1026,11 @@ void MainWindow::createStatusBar() {
|
|||||||
m_statusLabel->setContentsMargins(0, 0, 0, 0);
|
m_statusLabel->setContentsMargins(0, 0, 0, 0);
|
||||||
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
||||||
|
|
||||||
// View toggle buttons (Reclass / C/C++) — custom painted, no CSS
|
// View toggle is now per-pane via QTabWidget tab bar (Reclass / C/C++ tabs)
|
||||||
m_viewBtnGroup = new QButtonGroup(this);
|
sb->tabRow = nullptr;
|
||||||
m_viewBtnGroup->setExclusive(true);
|
|
||||||
|
|
||||||
m_btnReclass = new ViewTabButton("Reclass");
|
|
||||||
m_btnReclass->setChecked(true);
|
|
||||||
|
|
||||||
m_btnRendered = new ViewTabButton("C/C++");
|
|
||||||
|
|
||||||
m_viewBtnGroup->addButton(m_btnReclass, 0);
|
|
||||||
m_viewBtnGroup->addButton(m_btnRendered, 1);
|
|
||||||
|
|
||||||
// Wrap buttons in a plain container — FlatStatusBar paints the chrome
|
|
||||||
auto* tabRow = new QWidget(sb);
|
|
||||||
auto* tabLay = new QHBoxLayout(tabRow);
|
|
||||||
tabLay->setContentsMargins(0, 0, 0, 0);
|
|
||||||
tabLay->setSpacing(0);
|
|
||||||
tabLay->addWidget(m_btnReclass);
|
|
||||||
tabLay->addWidget(m_btnRendered);
|
|
||||||
|
|
||||||
sb->tabRow = tabRow;
|
|
||||||
sb->label = m_statusLabel;
|
sb->label = m_statusLabel;
|
||||||
|
|
||||||
sb->setMinimumHeight(qMax(m_btnReclass->sizeHint().height(),
|
sb->setMinimumHeight(sb->fontMetrics().height() + 6);
|
||||||
sb->fontMetrics().height() + 6));
|
|
||||||
|
|
||||||
connect(m_viewBtnGroup, &QButtonGroup::idClicked, this, [this](int id) {
|
|
||||||
setViewMode(id == 1 ? VM_Rendered : VM_Reclass);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Grip is a direct child of the main window, NOT in the status bar layout.
|
// Grip is a direct child of the main window, NOT in the status bar layout.
|
||||||
// Positioned via reposition() in resizeEvent — immune to font/margin changes.
|
// Positioned via reposition() in resizeEvent — immune to font/margin changes.
|
||||||
@@ -1092,19 +1049,6 @@ void MainWindow::createStatusBar() {
|
|||||||
sb->setTopLineColor(t.border);
|
sb->setTopLineColor(t.border);
|
||||||
sb->setDividerColor(t.border);
|
sb->setDividerColor(t.border);
|
||||||
|
|
||||||
auto applyViewTabColors = [&](ViewTabButton* btn) {
|
|
||||||
btn->colBg = t.background;
|
|
||||||
btn->colBgChecked = t.backgroundAlt;
|
|
||||||
btn->colBgHover = t.hover;
|
|
||||||
btn->colBgPressed = t.hover.darker(130);
|
|
||||||
btn->colText = t.text;
|
|
||||||
btn->colTextMuted = t.textMuted;
|
|
||||||
btn->colAccent = t.indHoverSpan;
|
|
||||||
btn->colBorder = t.border;
|
|
||||||
};
|
|
||||||
applyViewTabColors(static_cast<ViewTabButton*>(m_btnReclass));
|
|
||||||
applyViewTabColors(static_cast<ViewTabButton*>(m_btnRendered));
|
|
||||||
|
|
||||||
m_statusLabel->colBase = t.textDim;
|
m_statusLabel->colBase = t.textDim;
|
||||||
m_statusLabel->colBright = t.indHoverSpan;
|
m_statusLabel->colBright = t.indHoverSpan;
|
||||||
}
|
}
|
||||||
@@ -1141,45 +1085,31 @@ void MainWindow::clearMcpStatus() {
|
|||||||
m_mcpClearTimer->start(750);
|
m_mcpClearTimer->start(750);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::styleTabCloseButtons() {
|
|
||||||
auto* tabBar = m_mdiArea->findChild<QTabBar*>();
|
|
||||||
if (!tabBar) return;
|
|
||||||
|
|
||||||
const auto& t = ThemeManager::instance().current();
|
|
||||||
QString style = QStringLiteral(
|
|
||||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
|
||||||
"QToolButton:hover { color: %2; }")
|
|
||||||
.arg(t.textDim.name(), t.indHoverSpan.name());
|
|
||||||
|
|
||||||
auto subs = m_mdiArea->subWindowList();
|
|
||||||
for (int i = 0; i < tabBar->count() && i < subs.size(); i++) {
|
|
||||||
auto* existing = qobject_cast<QToolButton*>(
|
|
||||||
tabBar->tabButton(i, QTabBar::RightSide));
|
|
||||||
if (existing && existing->text() == QStringLiteral("\u2715")) {
|
|
||||||
// Already our button, just restyle
|
|
||||||
existing->setStyleSheet(style);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Replace with ✕ text button
|
|
||||||
auto* btn = new QToolButton(tabBar);
|
|
||||||
btn->setText(QStringLiteral("\u2715"));
|
|
||||||
btn->setAutoRaise(true);
|
|
||||||
btn->setCursor(Qt::PointingHandCursor);
|
|
||||||
btn->setStyleSheet(style);
|
|
||||||
QMdiSubWindow* sub = subs[i];
|
|
||||||
connect(btn, &QToolButton::clicked, sub, &QMdiSubWindow::close);
|
|
||||||
tabBar->setTabButton(i, QTabBar::RightSide, btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||||
SplitPane pane;
|
SplitPane pane;
|
||||||
|
|
||||||
pane.tabWidget = new QTabWidget;
|
pane.tabWidget = new QTabWidget;
|
||||||
pane.tabWidget->setTabPosition(QTabWidget::South);
|
pane.tabWidget->setTabPosition(QTabWidget::South);
|
||||||
pane.tabWidget->tabBar()->setVisible(false);
|
pane.tabWidget->tabBar()->setVisible(true);
|
||||||
pane.tabWidget->setDocumentMode(true); // kill QTabWidget frame border
|
pane.tabWidget->setDocumentMode(true); // kill QTabWidget frame border
|
||||||
|
|
||||||
|
// Style to match the top dock tab bar, with accent line on selected tab
|
||||||
|
{
|
||||||
|
const auto& t = ThemeManager::instance().current();
|
||||||
|
pane.tabWidget->setStyleSheet(QStringLiteral(
|
||||||
|
"QTabBar { border: none; }"
|
||||||
|
"QTabBar::tab {"
|
||||||
|
" background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;"
|
||||||
|
"}"
|
||||||
|
"QTabBar::tab:selected { color: %3; background: %4;"
|
||||||
|
" border-top: 3px solid %6; padding-top: -3px; }"
|
||||||
|
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||||
|
.arg(t.background.name(), t.textMuted.name(), t.text.name(),
|
||||||
|
t.backgroundAlt.name(), t.hover.name(), t.indHoverSpan.name()));
|
||||||
|
}
|
||||||
|
|
||||||
// Create editor via controller (parent = tabWidget for ownership)
|
// Create editor via controller (parent = tabWidget for ownership)
|
||||||
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
|
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
|
||||||
pane.editor->setRelativeOffsets(
|
pane.editor->setRelativeOffsets(
|
||||||
@@ -1342,6 +1272,32 @@ RcxEditor* MainWindow::activePaneEditor() {
|
|||||||
return pane ? pane->editor : nullptr;
|
return pane ? pane->editor : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event filter to manage border overlay + resize grip on floating dock widgets
|
||||||
|
class DockBorderFilter : public QObject {
|
||||||
|
public:
|
||||||
|
BorderOverlay* border;
|
||||||
|
ResizeGrip* grip;
|
||||||
|
DockBorderFilter(BorderOverlay* b, ResizeGrip* g, QObject* parent)
|
||||||
|
: QObject(parent), border(b), grip(g) {}
|
||||||
|
bool eventFilter(QObject* obj, QEvent* ev) override {
|
||||||
|
auto* dock = qobject_cast<QDockWidget*>(obj);
|
||||||
|
if (!dock || !dock->isFloating()) return false;
|
||||||
|
if (ev->type() == QEvent::Resize) {
|
||||||
|
border->setGeometry(0, 0, dock->width(), dock->height());
|
||||||
|
border->raise();
|
||||||
|
grip->reposition();
|
||||||
|
grip->raise();
|
||||||
|
} else if (ev->type() == QEvent::WindowActivate) {
|
||||||
|
border->color = ThemeManager::instance().current().borderFocused;
|
||||||
|
border->update();
|
||||||
|
} else if (ev->type() == QEvent::WindowDeactivate) {
|
||||||
|
border->color = ThemeManager::instance().current().border;
|
||||||
|
border->update();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
|
static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
|
||||||
if (viewRootId != 0) {
|
if (viewRootId != 0) {
|
||||||
int idx = tree.indexOfId(viewRootId);
|
int idx = tree.indexOfId(viewRootId);
|
||||||
@@ -1360,20 +1316,133 @@ static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
|
|||||||
return QStringLiteral("Untitled");
|
return QStringLiteral("Untitled");
|
||||||
}
|
}
|
||||||
|
|
||||||
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||||
auto* splitter = new QSplitter(Qt::Horizontal);
|
auto* splitter = new QSplitter(Qt::Horizontal);
|
||||||
splitter->setHandleWidth(1);
|
splitter->setHandleWidth(1);
|
||||||
auto* ctrl = new RcxController(doc, splitter);
|
auto* ctrl = new RcxController(doc, splitter);
|
||||||
|
|
||||||
auto* sub = m_mdiArea->addSubWindow(splitter);
|
QString title = doc->filePath.isEmpty()
|
||||||
sub->setWindowIcon(QIcon()); // suppress app icon in MDI tabs
|
? rootName(doc->tree) : QFileInfo(doc->filePath).fileName();
|
||||||
sub->setWindowTitle(doc->filePath.isEmpty()
|
auto* dock = new QDockWidget(title, this);
|
||||||
? rootName(doc->tree) : QFileInfo(doc->filePath).fileName());
|
dock->setObjectName(QStringLiteral("DocDock_%1").arg(quintptr(dock), 0, 16));
|
||||||
sub->setAttribute(Qt::WA_DeleteOnClose);
|
dock->setFeatures(QDockWidget::DockWidgetClosable |
|
||||||
sub->showMaximized();
|
QDockWidget::DockWidgetMovable |
|
||||||
|
QDockWidget::DockWidgetFloatable);
|
||||||
|
// Two title bar widgets: a hidden one (docked) and a draggable one (floating)
|
||||||
|
auto* emptyTitleBar = new QWidget(dock);
|
||||||
|
emptyTitleBar->setFixedHeight(0);
|
||||||
|
|
||||||
m_tabs[sub] = { doc, ctrl, splitter, {}, 0 };
|
auto* floatTitleBar = new QWidget(dock);
|
||||||
auto& tab = m_tabs[sub];
|
{
|
||||||
|
const auto& t = ThemeManager::instance().current();
|
||||||
|
floatTitleBar->setFixedHeight(24);
|
||||||
|
floatTitleBar->setAutoFillBackground(true);
|
||||||
|
{
|
||||||
|
QPalette tbPal = floatTitleBar->palette();
|
||||||
|
tbPal.setColor(QPalette::Window, t.backgroundAlt);
|
||||||
|
floatTitleBar->setPalette(tbPal);
|
||||||
|
}
|
||||||
|
auto* hl = new QHBoxLayout(floatTitleBar);
|
||||||
|
hl->setContentsMargins(4, 2, 2, 2);
|
||||||
|
hl->setSpacing(4);
|
||||||
|
|
||||||
|
auto* grip = new DockGripWidget(floatTitleBar);
|
||||||
|
grip->setObjectName("dockFloatGrip");
|
||||||
|
hl->addWidget(grip);
|
||||||
|
|
||||||
|
auto* lbl = new QLabel(title, floatTitleBar);
|
||||||
|
lbl->setObjectName("dockFloatTitle");
|
||||||
|
{
|
||||||
|
QPalette lp = lbl->palette();
|
||||||
|
lp.setColor(QPalette::WindowText, t.textDim);
|
||||||
|
lbl->setPalette(lp);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSettings settings("Reclass", "Reclass");
|
||||||
|
QFont f(settings.value("font", "JetBrains Mono").toString(), 12);
|
||||||
|
f.setFixedPitch(true);
|
||||||
|
lbl->setFont(f);
|
||||||
|
}
|
||||||
|
hl->addWidget(lbl);
|
||||||
|
hl->addStretch();
|
||||||
|
auto* closeBtn = new QToolButton(floatTitleBar);
|
||||||
|
closeBtn->setObjectName("dockFloatClose");
|
||||||
|
closeBtn->setText(QStringLiteral("\u2715"));
|
||||||
|
closeBtn->setAutoRaise(true);
|
||||||
|
closeBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
closeBtn->setStyleSheet(QStringLiteral(
|
||||||
|
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||||
|
"QToolButton:hover { color: %2; }")
|
||||||
|
.arg(t.textDim.name(), t.indHoverSpan.name()));
|
||||||
|
connect(closeBtn, &QToolButton::clicked, dock, &QDockWidget::close);
|
||||||
|
hl->addWidget(closeBtn);
|
||||||
|
}
|
||||||
|
floatTitleBar->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
connect(floatTitleBar, &QWidget::customContextMenuRequested,
|
||||||
|
this, [this, dock, floatTitleBar](const QPoint& pos) {
|
||||||
|
QMenu menu;
|
||||||
|
menu.addAction("Dock", [dock]() { dock->setFloating(false); });
|
||||||
|
menu.addSeparator();
|
||||||
|
auto* alwaysFloat = menu.addAction("Always Floating");
|
||||||
|
alwaysFloat->setCheckable(true);
|
||||||
|
bool locked = !(dock->features() & QDockWidget::DockWidgetMovable);
|
||||||
|
alwaysFloat->setChecked(locked);
|
||||||
|
connect(alwaysFloat, &QAction::toggled, dock, [dock](bool checked) {
|
||||||
|
auto features = dock->features();
|
||||||
|
if (checked)
|
||||||
|
features &= ~QDockWidget::DockWidgetMovable;
|
||||||
|
else
|
||||||
|
features |= QDockWidget::DockWidgetMovable;
|
||||||
|
dock->setFeatures(features);
|
||||||
|
});
|
||||||
|
menu.addSeparator();
|
||||||
|
menu.addAction("Close", [dock]() { dock->close(); });
|
||||||
|
menu.exec(floatTitleBar->mapToGlobal(pos));
|
||||||
|
});
|
||||||
|
|
||||||
|
dock->setTitleBarWidget(emptyTitleBar);
|
||||||
|
dock->setWidget(splitter);
|
||||||
|
|
||||||
|
// Border overlay and resize grip for floating state
|
||||||
|
auto* dockBorder = new BorderOverlay(dock);
|
||||||
|
dockBorder->color = ThemeManager::instance().current().borderFocused;
|
||||||
|
dockBorder->hide();
|
||||||
|
|
||||||
|
auto* dockGrip = new ResizeGrip(dock);
|
||||||
|
dockGrip->hide();
|
||||||
|
|
||||||
|
// Swap title bar when floating/docking, show/hide border + grip
|
||||||
|
connect(dock, &QDockWidget::topLevelChanged, this, [dock, emptyTitleBar, floatTitleBar, dockBorder, dockGrip](bool floating) {
|
||||||
|
dock->setTitleBarWidget(floating ? floatTitleBar : emptyTitleBar);
|
||||||
|
if (floating) {
|
||||||
|
dockBorder->setGeometry(0, 0, dock->width(), dock->height());
|
||||||
|
dockBorder->raise();
|
||||||
|
dockBorder->show();
|
||||||
|
dockGrip->reposition();
|
||||||
|
dockGrip->raise();
|
||||||
|
dockGrip->show();
|
||||||
|
} else {
|
||||||
|
dockBorder->hide();
|
||||||
|
dockGrip->hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dock->installEventFilter(new DockBorderFilter(dockBorder, dockGrip, dock));
|
||||||
|
// Keep float title bar label in sync with dock title
|
||||||
|
connect(dock, &QDockWidget::windowTitleChanged, floatTitleBar, [floatTitleBar](const QString& t) {
|
||||||
|
if (auto* lbl = floatTitleBar->findChild<QLabel*>("dockFloatTitle"))
|
||||||
|
lbl->setText(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tabify with existing doc docks, or add to top area
|
||||||
|
if (!m_docDocks.isEmpty())
|
||||||
|
tabifyDockWidget(m_docDocks.last(), dock);
|
||||||
|
else
|
||||||
|
addDockWidget(Qt::TopDockWidgetArea, dock);
|
||||||
|
|
||||||
|
m_docDocks.append(dock);
|
||||||
|
m_tabs[dock] = { doc, ctrl, splitter, {}, 0 };
|
||||||
|
m_activeDocDock = dock;
|
||||||
|
auto& tab = m_tabs[dock];
|
||||||
|
|
||||||
// Create the initial split pane
|
// Create the initial split pane
|
||||||
tab.panes.append(createSplitPane(tab));
|
tab.panes.append(createSplitPane(tab));
|
||||||
@@ -1381,23 +1450,46 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
// Apply global compact columns setting to new tab
|
// Apply global compact columns setting to new tab
|
||||||
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
|
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", false).toBool());
|
||||||
|
ctrl->setBraceWrap(QSettings("Reclass", "Reclass").value("braceWrap", false).toBool());
|
||||||
|
|
||||||
// Give every controller the shared document list for cross-tab type visibility
|
// Give every controller the shared document list for cross-tab type visibility
|
||||||
ctrl->setProjectDocuments(&m_allDocs);
|
ctrl->setProjectDocuments(&m_allDocs);
|
||||||
rebuildAllDocs();
|
rebuildAllDocs();
|
||||||
|
|
||||||
connect(sub, &QObject::destroyed, this, [this, sub]() {
|
// Track active tab via visibility
|
||||||
auto it = m_tabs.find(sub);
|
connect(dock, &QDockWidget::visibilityChanged, this, [this, dock](bool visible) {
|
||||||
|
if (visible) {
|
||||||
|
m_activeDocDock = dock;
|
||||||
|
updateWindowTitle();
|
||||||
|
rebuildWorkspaceModel();
|
||||||
|
// Sync view toggle buttons to this tab's active pane
|
||||||
|
auto it = m_tabs.find(dock);
|
||||||
|
if (it != m_tabs.end()) {
|
||||||
|
auto& tab = *it;
|
||||||
|
if (tab.activePaneIdx >= 0 && tab.activePaneIdx < tab.panes.size())
|
||||||
|
syncViewButtons(tab.panes[tab.activePaneIdx].viewMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep border overlay on top after dock rearrangements
|
||||||
|
if (m_borderOverlay) m_borderOverlay->raise();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup on close
|
||||||
|
connect(dock, &QObject::destroyed, this, [this, dock]() {
|
||||||
|
auto it = m_tabs.find(dock);
|
||||||
if (it != m_tabs.end()) {
|
if (it != m_tabs.end()) {
|
||||||
it->doc->deleteLater();
|
it->doc->deleteLater();
|
||||||
m_tabs.erase(it);
|
m_tabs.erase(it);
|
||||||
}
|
}
|
||||||
|
m_docDocks.removeOne(dock);
|
||||||
|
if (m_activeDocDock == dock)
|
||||||
|
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
|
||||||
rebuildAllDocs();
|
rebuildAllDocs();
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(ctrl, &RcxController::nodeSelected,
|
connect(ctrl, &RcxController::nodeSelected,
|
||||||
this, [this, ctrl, sub](int nodeIdx) {
|
this, [this, ctrl, dock](int nodeIdx) {
|
||||||
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
|
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
|
||||||
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
||||||
auto* ap = findActiveSplitPane();
|
auto* ap = findActiveSplitPane();
|
||||||
@@ -1415,7 +1507,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
.arg(node.byteSize()));
|
.arg(node.byteSize()));
|
||||||
}
|
}
|
||||||
// Update all rendered panes on selection change
|
// Update all rendered panes on selection change
|
||||||
auto it = m_tabs.find(sub);
|
auto it = m_tabs.find(dock);
|
||||||
if (it != m_tabs.end())
|
if (it != m_tabs.end())
|
||||||
updateAllRenderedPanes(*it);
|
updateAllRenderedPanes(*it);
|
||||||
});
|
});
|
||||||
@@ -1425,32 +1517,42 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
setAppStatus(QString("%1 nodes selected").arg(count));
|
setAppStatus(QString("%1 nodes selected").arg(count));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Append Float/Close actions to any editor context menu
|
||||||
|
connect(ctrl, &RcxController::contextMenuAboutToShow,
|
||||||
|
this, [this, dock](QMenu* menu, int /*line*/) {
|
||||||
|
menu->addSeparator();
|
||||||
|
menu->addAction(dock->isFloating() ? "Dock" : "Float", [dock]() {
|
||||||
|
dock->setFloating(!dock->isFloating());
|
||||||
|
});
|
||||||
|
menu->addAction("Close Tab", [dock]() { dock->close(); });
|
||||||
|
});
|
||||||
|
|
||||||
// Update rendered panes and workspace on document changes and undo/redo
|
// Update rendered panes and workspace on document changes and undo/redo
|
||||||
connect(doc, &RcxDocument::documentChanged,
|
connect(doc, &RcxDocument::documentChanged,
|
||||||
this, [this, sub]() {
|
this, [this, dock]() {
|
||||||
auto it = m_tabs.find(sub);
|
auto it = m_tabs.find(dock);
|
||||||
if (it != m_tabs.end())
|
if (it != m_tabs.end())
|
||||||
QTimer::singleShot(0, this, [this, sub]() {
|
QTimer::singleShot(0, this, [this, dock]() {
|
||||||
auto it2 = m_tabs.find(sub);
|
auto it2 = m_tabs.find(dock);
|
||||||
if (it2 != m_tabs.end()) {
|
if (it2 != m_tabs.end()) {
|
||||||
updateAllRenderedPanes(*it2);
|
updateAllRenderedPanes(*it2);
|
||||||
if (it2->doc->filePath.isEmpty())
|
if (it2->doc->filePath.isEmpty())
|
||||||
sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
dock->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||||
}
|
}
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
updateWindowTitle();
|
updateWindowTitle();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
connect(&doc->undoStack, &QUndoStack::indexChanged,
|
connect(&doc->undoStack, &QUndoStack::indexChanged,
|
||||||
this, [this, sub](int) {
|
this, [this, dock](int) {
|
||||||
auto it = m_tabs.find(sub);
|
auto it = m_tabs.find(dock);
|
||||||
if (it != m_tabs.end())
|
if (it != m_tabs.end())
|
||||||
QTimer::singleShot(0, this, [this, sub]() {
|
QTimer::singleShot(0, this, [this, dock]() {
|
||||||
auto it2 = m_tabs.find(sub);
|
auto it2 = m_tabs.find(dock);
|
||||||
if (it2 != m_tabs.end()) {
|
if (it2 != m_tabs.end()) {
|
||||||
updateAllRenderedPanes(*it2);
|
updateAllRenderedPanes(*it2);
|
||||||
if (it2->doc->filePath.isEmpty())
|
if (it2->doc->filePath.isEmpty())
|
||||||
sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
dock->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||||
}
|
}
|
||||||
updateWindowTitle();
|
updateWindowTitle();
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
@@ -1467,8 +1569,37 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
|
|
||||||
ctrl->refresh();
|
ctrl->refresh();
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
styleTabCloseButtons();
|
|
||||||
return sub;
|
dock->raise();
|
||||||
|
dock->show();
|
||||||
|
|
||||||
|
// Install context menu on dock tab bars (deferred — tab bar created after tabification)
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
for (auto* tabBar : findChildren<QTabBar*>()) {
|
||||||
|
if (tabBar->parent() != this) continue;
|
||||||
|
if (tabBar->contextMenuPolicy() == Qt::CustomContextMenu) continue;
|
||||||
|
tabBar->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
connect(tabBar, &QTabBar::customContextMenuRequested,
|
||||||
|
this, [this, tabBar](const QPoint& pos) {
|
||||||
|
int idx = tabBar->tabAt(pos);
|
||||||
|
if (idx < 0) return;
|
||||||
|
// Match tab to dock by title (tab bar only shows docked tabs)
|
||||||
|
QString tabTitle = tabBar->tabText(idx);
|
||||||
|
QDockWidget* target = nullptr;
|
||||||
|
for (auto* d : m_docDocks) {
|
||||||
|
if (d->windowTitle() == tabTitle) { target = d; break; }
|
||||||
|
}
|
||||||
|
if (!target) return;
|
||||||
|
QMenu menu;
|
||||||
|
menu.addAction("Float", [target]() { target->setFloating(true); });
|
||||||
|
menu.addSeparator();
|
||||||
|
menu.addAction("Close", [target]() { target->close(); });
|
||||||
|
menu.exec(tabBar->mapToGlobal(pos));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a minimal empty struct for new documents
|
// Build a minimal empty struct for new documents
|
||||||
@@ -1527,7 +1658,7 @@ MainWindow::~MainWindow() {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Disconnect all subwindow destroyed signals before members are torn down,
|
// Disconnect all dock destroyed signals before members are torn down,
|
||||||
// so the lambdas capturing 'this' never fire on a half-destroyed object.
|
// so the lambdas capturing 'this' never fire on a half-destroyed object.
|
||||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||||
disconnect(it.key(), &QObject::destroyed, this, nullptr);
|
disconnect(it.key(), &QObject::destroyed, this, nullptr);
|
||||||
@@ -1666,11 +1797,13 @@ void MainWindow::selfTest() {
|
|||||||
|
|
||||||
// Tab 1: Empty class for user work (created second, becomes active)
|
// Tab 1: Empty class for user work (created second, becomes active)
|
||||||
auto* userTab = project_new(QStringLiteral("class"));
|
auto* userTab = project_new(QStringLiteral("class"));
|
||||||
m_mdiArea->setActiveSubWindow(userTab);
|
userTab->raise();
|
||||||
|
userTab->show();
|
||||||
#else
|
#else
|
||||||
project_new();
|
project_new();
|
||||||
auto* userTab = project_new(QStringLiteral("class"));
|
auto* userTab = project_new(QStringLiteral("class"));
|
||||||
m_mdiArea->setActiveSubWindow(userTab);
|
userTab->raise();
|
||||||
|
userTab->show();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1849,24 +1982,50 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
// Update border overlay color
|
// Update border overlay color
|
||||||
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
||||||
|
|
||||||
// MDI area tabs — text color + height handled by MenuBarStyle QProxyStyle
|
// Style doc dock tab bars and remove dock borders
|
||||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
setStyleSheet(QStringLiteral(
|
||||||
|
"QMainWindow::separator { width: 1px; height: 1px; background: %4; }"
|
||||||
|
"QDockWidget { border: none; }"
|
||||||
|
"QDockWidget > QWidget { border: none; }")
|
||||||
|
.arg(theme.border.name()));
|
||||||
|
|
||||||
|
for (auto* tabBar : findChildren<QTabBar*>()) {
|
||||||
|
// Only style tab bars owned directly by this QMainWindow (dock tabs),
|
||||||
|
// skip ones inside SplitPane QTabWidgets etc.
|
||||||
|
if (tabBar->parent() == this) {
|
||||||
|
tabBar->setStyleSheet(QStringLiteral(
|
||||||
|
"QTabBar { border: none; }"
|
||||||
"QTabBar::tab {"
|
"QTabBar::tab {"
|
||||||
" background: %1; padding: 0px 16px; border: none;"
|
" background: %1; padding: 0px 16px; border: none;"
|
||||||
"}"
|
"}"
|
||||||
"QTabBar::tab:selected { background: %2; }"
|
"QTabBar::tab:selected { background: %2; }"
|
||||||
"QTabBar::tab:hover { background: %3; }")
|
"QTabBar::tab:hover { background: %3; }")
|
||||||
.arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.name()));
|
.arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.name()));
|
||||||
|
|
||||||
// Dim MDI tab text via palette (Fusion reads WindowText, not CSS color:)
|
|
||||||
if (auto* tabBar = m_mdiArea->findChild<QTabBar*>()) {
|
|
||||||
QPalette tp = tabBar->palette();
|
QPalette tp = tabBar->palette();
|
||||||
tp.setColor(QPalette::WindowText, theme.textDim);
|
tp.setColor(QPalette::WindowText, theme.textDim);
|
||||||
tabBar->setPalette(tp);
|
tabBar->setPalette(tp);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Re-style ✕ close buttons on MDI tabs
|
// Restyle per-pane view tab bars (Reclass / C++)
|
||||||
styleTabCloseButtons();
|
{
|
||||||
|
QString paneTabStyle = QStringLiteral(
|
||||||
|
"QTabBar { border: none; }"
|
||||||
|
"QTabBar::tab {"
|
||||||
|
" background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;"
|
||||||
|
"}"
|
||||||
|
"QTabBar::tab:selected { color: %3; background: %4;"
|
||||||
|
" border-top: 3px solid %6; padding-top: -3px; }"
|
||||||
|
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||||
|
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
||||||
|
theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name());
|
||||||
|
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||||
|
for (auto& pane : it->panes) {
|
||||||
|
if (pane.tabWidget)
|
||||||
|
pane.tabWidget->setStyleSheet(paneTabStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
{
|
{
|
||||||
@@ -1875,27 +2034,12 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
sbPal.setColor(QPalette::WindowText, theme.textDim);
|
sbPal.setColor(QPalette::WindowText, theme.textDim);
|
||||||
statusBar()->setPalette(sbPal);
|
statusBar()->setPalette(sbPal);
|
||||||
}
|
}
|
||||||
// View toggle buttons + status bar chrome
|
// Status bar chrome
|
||||||
{
|
{
|
||||||
auto applyColors = [&](ViewTabButton* btn) {
|
auto* fsb = static_cast<FlatStatusBar*>(statusBar());
|
||||||
btn->colBg = theme.background;
|
|
||||||
btn->colBgChecked = theme.backgroundAlt;
|
|
||||||
btn->colBgHover = theme.hover;
|
|
||||||
btn->colBgPressed = theme.hover.darker(130);
|
|
||||||
btn->colText = theme.text;
|
|
||||||
btn->colTextMuted = theme.textMuted;
|
|
||||||
btn->colAccent = theme.indHoverSpan;
|
|
||||||
btn->colBorder = theme.border;
|
|
||||||
btn->update();
|
|
||||||
};
|
|
||||||
applyColors(static_cast<ViewTabButton*>(m_btnReclass));
|
|
||||||
applyColors(static_cast<ViewTabButton*>(m_btnRendered));
|
|
||||||
|
|
||||||
{ auto* fsb = static_cast<FlatStatusBar*>(statusBar());
|
|
||||||
fsb->setTopLineColor(theme.border);
|
fsb->setTopLineColor(theme.border);
|
||||||
fsb->setDividerColor(theme.border);
|
fsb->setDividerColor(theme.border);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Resize grip (direct child of main window, not in status bar)
|
// Resize grip (direct child of main window, not in status bar)
|
||||||
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
||||||
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
||||||
@@ -1955,6 +2099,33 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
if (m_scanDockGrip)
|
if (m_scanDockGrip)
|
||||||
m_scanDockGrip->setGripColor(theme.textFaint);
|
m_scanDockGrip->setGripColor(theme.textFaint);
|
||||||
|
|
||||||
|
// Doc dock floating title bars
|
||||||
|
for (auto* dock : m_docDocks) {
|
||||||
|
// The float title bar is stored alongside the empty one; find by object name
|
||||||
|
for (auto* child : dock->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly)) {
|
||||||
|
if (auto* lbl = child->findChild<QLabel*>("dockFloatTitle")) {
|
||||||
|
// Restyle the float title bar background
|
||||||
|
QPalette tbPal = child->palette();
|
||||||
|
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||||
|
child->setPalette(tbPal);
|
||||||
|
// Label color
|
||||||
|
QPalette lp = lbl->palette();
|
||||||
|
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||||
|
lbl->setPalette(lp);
|
||||||
|
}
|
||||||
|
if (auto* closeBtn = child->findChild<QToolButton*>("dockFloatClose")) {
|
||||||
|
closeBtn->setStyleSheet(QStringLiteral(
|
||||||
|
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||||
|
"QToolButton:hover { color: %2; }")
|
||||||
|
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||||
|
}
|
||||||
|
if (auto* gripW = child->findChild<QWidget*>("dockFloatGrip")) {
|
||||||
|
if (auto* grip = dynamic_cast<DockGripWidget*>(gripW))
|
||||||
|
grip->setGripColor(theme.textFaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rendered C/C++ views: update lexer colors, paper, margins
|
// Rendered C/C++ views: update lexer colors, paper, margins
|
||||||
for (auto& tab : m_tabs) {
|
for (auto& tab : m_tabs) {
|
||||||
for (auto& pane : tab.panes) {
|
for (auto& pane : tab.panes) {
|
||||||
@@ -2012,6 +2183,7 @@ void MainWindow::showOptionsDialog() {
|
|||||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
||||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
current.generatorAsserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
current.generatorAsserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||||
|
current.braceWrap = QSettings("Reclass", "Reclass").value("braceWrap", false).toBool();
|
||||||
|
|
||||||
OptionsDialog dlg(current, this);
|
OptionsDialog dlg(current, this);
|
||||||
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
||||||
@@ -2046,6 +2218,12 @@ void MainWindow::showOptionsDialog() {
|
|||||||
|
|
||||||
if (r.generatorAsserts != current.generatorAsserts)
|
if (r.generatorAsserts != current.generatorAsserts)
|
||||||
QSettings("Reclass", "Reclass").setValue("generatorAsserts", r.generatorAsserts);
|
QSettings("Reclass", "Reclass").setValue("generatorAsserts", r.generatorAsserts);
|
||||||
|
|
||||||
|
if (r.braceWrap != current.braceWrap) {
|
||||||
|
QSettings("Reclass", "Reclass").setValue("braceWrap", r.braceWrap);
|
||||||
|
for (auto& tab : m_tabs)
|
||||||
|
tab.ctrl->setBraceWrap(r.braceWrap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setEditorFont(const QString& fontName) {
|
void MainWindow::setEditorFont(const QString& fontName) {
|
||||||
@@ -2079,28 +2257,30 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
|||||||
m_scannerPanel->setEditorFont(f);
|
m_scannerPanel->setEditorFont(f);
|
||||||
if (m_scanDockTitle)
|
if (m_scanDockTitle)
|
||||||
m_scanDockTitle->setFont(f);
|
m_scanDockTitle->setFont(f);
|
||||||
|
// Sync doc dock float title fonts
|
||||||
|
for (auto* dock : m_docDocks) {
|
||||||
|
if (auto* lbl = dock->findChild<QLabel*>("dockFloatTitle"))
|
||||||
|
lbl->setFont(f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RcxController* MainWindow::activeController() const {
|
RcxController* MainWindow::activeController() const {
|
||||||
auto* sub = m_mdiArea->activeSubWindow();
|
if (m_activeDocDock && m_tabs.contains(m_activeDocDock))
|
||||||
if (sub && m_tabs.contains(sub))
|
return m_tabs[m_activeDocDock].ctrl;
|
||||||
return m_tabs[sub].ctrl;
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::TabState* MainWindow::activeTab() {
|
MainWindow::TabState* MainWindow::activeTab() {
|
||||||
auto* sub = m_mdiArea->activeSubWindow();
|
if (m_activeDocDock && m_tabs.contains(m_activeDocDock))
|
||||||
if (sub && m_tabs.contains(sub))
|
return &m_tabs[m_activeDocDock];
|
||||||
return &m_tabs[sub];
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::TabState* MainWindow::tabByIndex(int index) {
|
MainWindow::TabState* MainWindow::tabByIndex(int index) {
|
||||||
auto subs = m_mdiArea->subWindowList();
|
if (index < 0 || index >= m_docDocks.size()) return nullptr;
|
||||||
if (index < 0 || index >= subs.size()) return nullptr;
|
auto* dock = m_docDocks[index];
|
||||||
auto* sub = subs[index];
|
if (m_tabs.contains(dock))
|
||||||
if (m_tabs.contains(sub))
|
return &m_tabs[dock];
|
||||||
return &m_tabs[sub];
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2109,9 +2289,9 @@ void MainWindow::updateWindowTitle() {
|
|||||||
setWindowTitle(QStringLiteral("Reclass"));
|
setWindowTitle(QStringLiteral("Reclass"));
|
||||||
#else
|
#else
|
||||||
QString title;
|
QString title;
|
||||||
auto* sub = m_mdiArea->activeSubWindow();
|
auto* activeDock = m_activeDocDock;
|
||||||
if (sub && m_tabs.contains(sub)) {
|
if (activeDock && m_tabs.contains(activeDock)) {
|
||||||
auto& tab = m_tabs[sub];
|
auto& tab = m_tabs[activeDock];
|
||||||
QString name = tab.doc->filePath.isEmpty()
|
QString name = tab.doc->filePath.isEmpty()
|
||||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||||
: QFileInfo(tab.doc->filePath).fileName();
|
: QFileInfo(tab.doc->filePath).fileName();
|
||||||
@@ -2196,10 +2376,8 @@ void MainWindow::setViewMode(ViewMode mode) {
|
|||||||
syncViewButtons(mode);
|
syncViewButtons(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::syncViewButtons(ViewMode mode) {
|
void MainWindow::syncViewButtons(ViewMode /*mode*/) {
|
||||||
QSignalBlocker block(m_viewBtnGroup);
|
// View toggle is now per-pane via QTabWidget tab bar — nothing to sync globally
|
||||||
if (mode == VM_Rendered) m_btnRendered->setChecked(true);
|
|
||||||
else m_btnReclass->setChecked(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Find the root-level struct ancestor for a node ──
|
// ── Find the root-level struct ancestor for a node ──
|
||||||
@@ -2380,7 +2558,7 @@ void MainWindow::importReclassXml() {
|
|||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
m_mdiArea->closeAllSubWindows();
|
closeAllDocDocks();
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||||
@@ -2429,7 +2607,7 @@ void MainWindow::importFromSource() {
|
|||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
m_mdiArea->closeAllSubWindows();
|
closeAllDocDocks();
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_workspaceDock->show();
|
m_workspaceDock->show();
|
||||||
@@ -2479,7 +2657,7 @@ void MainWindow::importPdb() {
|
|||||||
auto* doc = new rcx::RcxDocument(this);
|
auto* doc = new rcx::RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
m_mdiArea->closeAllSubWindows();
|
closeAllDocDocks();
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_workspaceDock->show();
|
m_workspaceDock->show();
|
||||||
@@ -2607,7 +2785,7 @@ void MainWindow::showTypeAliasesDialog() {
|
|||||||
|
|
||||||
// ── Project Lifecycle API ──
|
// ── Project Lifecycle API ──
|
||||||
|
|
||||||
QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
|
QDockWidget* MainWindow::project_new(const QString& classKeyword) {
|
||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
|
|
||||||
QByteArray data(256, '\0');
|
QByteArray data(256, '\0');
|
||||||
@@ -2623,20 +2801,20 @@ QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
|
|||||||
doc->provider = currentCtrl->document()->provider;
|
doc->provider = currentCtrl->document()->provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* sub = createTab(doc);
|
auto* dock = createTab(doc);
|
||||||
|
|
||||||
// Copy saved sources to new tab's controller
|
// Copy saved sources to new tab's controller
|
||||||
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
|
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
|
||||||
auto& newTab = m_tabs[sub];
|
auto& newTab = m_tabs[dock];
|
||||||
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
|
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
|
||||||
currentCtrl->activeSourceIndex());
|
currentCtrl->activeSourceIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
return sub;
|
return dock;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
QDockWidget* MainWindow::project_open(const QString& path) {
|
||||||
QString filePath = path;
|
QString filePath = path;
|
||||||
if (filePath.isEmpty()) {
|
if (filePath.isEmpty()) {
|
||||||
filePath = QFileDialog::getOpenFileName(this,
|
filePath = QFileDialog::getOpenFileName(this,
|
||||||
@@ -2670,8 +2848,8 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
}
|
}
|
||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
m_mdiArea->closeAllSubWindows();
|
closeAllDocDocks();
|
||||||
auto* sub = createTab(doc);
|
auto* dock = createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_workspaceDock->show();
|
m_workspaceDock->show();
|
||||||
int classCount = 0;
|
int classCount = 0;
|
||||||
@@ -2680,7 +2858,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||||
addRecentFile(filePath);
|
addRecentFile(filePath);
|
||||||
return sub;
|
return dock;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
@@ -2691,19 +2869,19 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close all existing tabs so the project replaces the current state
|
// Close all existing tabs so the project replaces the current state
|
||||||
m_mdiArea->closeAllSubWindows();
|
closeAllDocDocks();
|
||||||
|
|
||||||
auto* sub = createTab(doc);
|
auto* dock = createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_workspaceDock->show();
|
m_workspaceDock->show();
|
||||||
addRecentFile(filePath);
|
addRecentFile(filePath);
|
||||||
return sub;
|
return dock;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
|
bool MainWindow::project_save(QDockWidget* dock, bool saveAs) {
|
||||||
if (!sub) sub = m_mdiArea->activeSubWindow();
|
if (!dock) dock = m_activeDocDock;
|
||||||
if (!sub || !m_tabs.contains(sub)) return false;
|
if (!dock || !m_tabs.contains(dock)) return false;
|
||||||
auto& tab = m_tabs[sub];
|
auto& tab = m_tabs[dock];
|
||||||
|
|
||||||
if (saveAs || tab.doc->filePath.isEmpty()) {
|
if (saveAs || tab.doc->filePath.isEmpty()) {
|
||||||
QString path = QFileDialog::getSaveFileName(this,
|
QString path = QFileDialog::getSaveFileName(this,
|
||||||
@@ -2719,13 +2897,23 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::project_close(QMdiSubWindow* sub) {
|
void MainWindow::project_close(QDockWidget* dock) {
|
||||||
if (!sub) sub = m_mdiArea->activeSubWindow();
|
if (!dock) dock = m_activeDocDock;
|
||||||
if (!sub) return;
|
if (!dock) return;
|
||||||
sub->close();
|
dock->close();
|
||||||
rebuildWorkspaceModel();
|
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);
|
||||||
|
dock->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Workspace Dock ──
|
// ── Workspace Dock ──
|
||||||
|
|
||||||
void MainWindow::createWorkspaceDock() {
|
void MainWindow::createWorkspaceDock() {
|
||||||
@@ -2855,10 +3043,10 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
|
|
||||||
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* dock = static_cast<QDockWidget*>(subVar.value<void*>());
|
||||||
if (!sub || !m_tabs.contains(sub)) return;
|
if (!dock || !m_tabs.contains(dock)) return;
|
||||||
|
|
||||||
auto& tab = m_tabs[sub];
|
auto& tab = m_tabs[dock];
|
||||||
int ni = tab.doc->tree.indexOfId(structId);
|
int ni = tab.doc->tree.indexOfId(structId);
|
||||||
if (ni < 0) return;
|
if (ni < 0) return;
|
||||||
QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
|
QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
|
||||||
@@ -2960,16 +3148,18 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
|
|
||||||
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* dock = static_cast<QDockWidget*>(subVar.value<void*>());
|
||||||
if (!sub || !m_tabs.contains(sub)) return;
|
if (!dock || !m_tabs.contains(dock)) return;
|
||||||
|
|
||||||
m_mdiArea->setActiveSubWindow(sub);
|
dock->raise();
|
||||||
|
dock->show();
|
||||||
|
m_activeDocDock = dock;
|
||||||
|
|
||||||
auto& tree = m_tabs[sub].doc->tree;
|
auto& tree = m_tabs[dock].doc->tree;
|
||||||
int ni = tree.indexOfId(structId);
|
int ni = tree.indexOfId(structId);
|
||||||
if (ni < 0) return;
|
if (ni < 0) return;
|
||||||
|
|
||||||
auto& tab = m_tabs[sub];
|
auto& tab = m_tabs[dock];
|
||||||
|
|
||||||
// Child member item: navigate to parent struct, then scroll to this member
|
// Child member item: navigate to parent struct, then scroll to this member
|
||||||
uint64_t parentId = tree.nodes[ni].parentId;
|
uint64_t parentId = tree.nodes[ni].parentId;
|
||||||
@@ -2986,9 +3176,9 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If active pane is in C/C++ mode, refresh after navigation settles
|
// If active pane is in C/C++ mode, refresh after navigation settles
|
||||||
QTimer::singleShot(0, this, [this, sub]() {
|
QTimer::singleShot(0, this, [this, dock]() {
|
||||||
if (!m_tabs.contains(sub)) return;
|
if (!m_tabs.contains(dock)) return;
|
||||||
auto& t = m_tabs[sub];
|
auto& t = m_tabs[dock];
|
||||||
if (t.activePaneIdx >= 0 && t.activePaneIdx < t.panes.size()) {
|
if (t.activePaneIdx >= 0 && t.activePaneIdx < t.panes.size()) {
|
||||||
auto& p = t.panes[t.activePaneIdx];
|
auto& p = t.panes[t.activePaneIdx];
|
||||||
if (p.viewMode == VM_Rendered)
|
if (p.viewMode == VM_Rendered)
|
||||||
@@ -3066,6 +3256,31 @@ void MainWindow::createScannerDock() {
|
|||||||
addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock);
|
addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock);
|
||||||
m_scannerDock->hide();
|
m_scannerDock->hide();
|
||||||
|
|
||||||
|
// Border overlay and resize grip for floating state
|
||||||
|
{
|
||||||
|
auto* border = new BorderOverlay(m_scannerDock);
|
||||||
|
border->color = ThemeManager::instance().current().borderFocused;
|
||||||
|
border->hide();
|
||||||
|
auto* grip = new ResizeGrip(m_scannerDock);
|
||||||
|
grip->hide();
|
||||||
|
|
||||||
|
connect(m_scannerDock, &QDockWidget::topLevelChanged,
|
||||||
|
this, [this, border, grip](bool floating) {
|
||||||
|
if (floating) {
|
||||||
|
border->setGeometry(0, 0, m_scannerDock->width(), m_scannerDock->height());
|
||||||
|
border->raise();
|
||||||
|
border->show();
|
||||||
|
grip->reposition();
|
||||||
|
grip->raise();
|
||||||
|
grip->show();
|
||||||
|
} else {
|
||||||
|
border->hide();
|
||||||
|
grip->hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
m_scannerDock->installEventFilter(new DockBorderFilter(border, grip, m_scannerDock));
|
||||||
|
}
|
||||||
|
|
||||||
// Wire provider getter: lazily captures the active tab's provider at scan time
|
// Wire provider getter: lazily captures the active tab's provider at scan time
|
||||||
m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr<rcx::Provider> {
|
m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr<rcx::Provider> {
|
||||||
auto* ctrl = activeController();
|
auto* ctrl = activeController();
|
||||||
@@ -3334,6 +3549,11 @@ void MainWindow::changeEvent(QEvent* event) {
|
|||||||
}
|
}
|
||||||
if (event->type() == QEvent::WindowStateChange && m_titleBar)
|
if (event->type() == QEvent::WindowStateChange && m_titleBar)
|
||||||
m_titleBar->updateMaximizeIcon();
|
m_titleBar->updateMaximizeIcon();
|
||||||
|
// Keep border overlay on top after any state change
|
||||||
|
if (m_borderOverlay) {
|
||||||
|
m_borderOverlay->setGeometry(rect());
|
||||||
|
m_borderOverlay->raise();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::resizeEvent(QResizeEvent* event) {
|
void MainWindow::resizeEvent(QResizeEvent* event) {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
#include "pluginmanager.h"
|
#include "pluginmanager.h"
|
||||||
#include "scannerpanel.h"
|
#include "scannerpanel.h"
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMdiArea>
|
|
||||||
#include <QMdiSubWindow>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
@@ -72,22 +70,19 @@ public:
|
|||||||
void clearMcpStatus();
|
void clearMcpStatus();
|
||||||
|
|
||||||
// Project Lifecycle API
|
// Project Lifecycle API
|
||||||
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
QDockWidget* project_new(const QString& classKeyword = QString());
|
||||||
QMdiSubWindow* project_open(const QString& path = {});
|
QDockWidget* project_open(const QString& path = {});
|
||||||
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
|
bool project_save(QDockWidget* dock = nullptr, bool saveAs = false);
|
||||||
void project_close(QMdiSubWindow* sub = nullptr);
|
void project_close(QDockWidget* dock = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||||
|
|
||||||
QMdiArea* m_mdiArea;
|
QWidget* m_centralPlaceholder;
|
||||||
ShimmerLabel* m_statusLabel;
|
ShimmerLabel* m_statusLabel;
|
||||||
QString m_appStatus;
|
QString m_appStatus;
|
||||||
bool m_mcpBusy = false;
|
bool m_mcpBusy = false;
|
||||||
QTimer* m_mcpClearTimer = nullptr;
|
QTimer* m_mcpClearTimer = nullptr;
|
||||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
|
||||||
QPushButton* m_btnReclass = nullptr;
|
|
||||||
QPushButton* m_btnRendered = nullptr;
|
|
||||||
TitleBarWidget* m_titleBar = nullptr;
|
TitleBarWidget* m_titleBar = nullptr;
|
||||||
QMenuBar* m_menuBar = nullptr;
|
QMenuBar* m_menuBar = nullptr;
|
||||||
bool m_menuBarTitleCase = false;
|
bool m_menuBarTitleCase = false;
|
||||||
@@ -117,7 +112,9 @@ private:
|
|||||||
QVector<SplitPane> panes;
|
QVector<SplitPane> panes;
|
||||||
int activePaneIdx = 0;
|
int activePaneIdx = 0;
|
||||||
};
|
};
|
||||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
QMap<QDockWidget*, TabState> m_tabs;
|
||||||
|
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
|
||||||
|
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
|
||||||
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||||
void rebuildAllDocs();
|
void rebuildAllDocs();
|
||||||
|
|
||||||
@@ -134,8 +131,9 @@ private:
|
|||||||
TabState* activeTab();
|
TabState* activeTab();
|
||||||
TabState* tabByIndex(int index);
|
TabState* tabByIndex(int index);
|
||||||
int tabCount() const { return m_tabs.size(); }
|
int tabCount() const { return m_tabs.size(); }
|
||||||
QMdiSubWindow* createTab(RcxDocument* doc);
|
QDockWidget* createTab(RcxDocument* doc);
|
||||||
void updateWindowTitle();
|
void updateWindowTitle();
|
||||||
|
void closeAllDocDocks();
|
||||||
|
|
||||||
void setViewMode(ViewMode mode);
|
void setViewMode(ViewMode mode);
|
||||||
void updateRenderedView(TabState& tab, SplitPane& pane);
|
void updateRenderedView(TabState& tab, SplitPane& pane);
|
||||||
@@ -145,7 +143,6 @@ private:
|
|||||||
|
|
||||||
SplitPane createSplitPane(TabState& tab);
|
SplitPane createSplitPane(TabState& tab);
|
||||||
void applyTheme(const Theme& theme);
|
void applyTheme(const Theme& theme);
|
||||||
void styleTabCloseButtons();
|
|
||||||
void syncViewButtons(ViewMode mode);
|
void syncViewButtons(ViewMode mode);
|
||||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||||
SplitPane* findActiveSplitPane();
|
SplitPane* findActiveSplitPane();
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
m_showIconCheck->setChecked(current.showIcon);
|
m_showIconCheck->setChecked(current.showIcon);
|
||||||
visualLayout->addRow(m_showIconCheck);
|
visualLayout->addRow(m_showIconCheck);
|
||||||
|
|
||||||
|
m_braceWrapCheck = new QCheckBox("Opening brace on new line");
|
||||||
|
m_braceWrapCheck->setChecked(current.braceWrap);
|
||||||
|
visualLayout->addRow(m_braceWrapCheck);
|
||||||
|
|
||||||
generalLayout->addWidget(visualGroup);
|
generalLayout->addWidget(visualGroup);
|
||||||
generalLayout->addStretch();
|
generalLayout->addStretch();
|
||||||
|
|
||||||
@@ -212,6 +216,7 @@ OptionsResult OptionsDialog::result() const {
|
|||||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||||
r.refreshMs = m_refreshSpin->value();
|
r.refreshMs = m_refreshSpin->value();
|
||||||
r.generatorAsserts = m_assertCheck->isChecked();
|
r.generatorAsserts = m_assertCheck->isChecked();
|
||||||
|
r.braceWrap = m_braceWrapCheck->isChecked();
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct OptionsResult {
|
|||||||
bool autoStartMcp = true;
|
bool autoStartMcp = true;
|
||||||
int refreshMs = 660;
|
int refreshMs = 660;
|
||||||
bool generatorAsserts = false;
|
bool generatorAsserts = false;
|
||||||
|
bool braceWrap = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OptionsDialog : public QDialog {
|
class OptionsDialog : public QDialog {
|
||||||
@@ -41,6 +42,7 @@ private:
|
|||||||
QCheckBox* m_autoMcpCheck = nullptr;
|
QCheckBox* m_autoMcpCheck = nullptr;
|
||||||
QSpinBox* m_refreshSpin = nullptr;
|
QSpinBox* m_refreshSpin = nullptr;
|
||||||
QCheckBox* m_assertCheck = nullptr;
|
QCheckBox* m_assertCheck = nullptr;
|
||||||
|
QCheckBox* m_braceWrapCheck = nullptr;
|
||||||
|
|
||||||
// searchable keywords per leaf tree item
|
// searchable keywords per leaf tree item
|
||||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
|||||||
QStringLiteral("Copy Address"), this);
|
QStringLiteral("Copy Address"), this);
|
||||||
m_copyBtn->setEnabled(false);
|
m_copyBtn->setEnabled(false);
|
||||||
actionRow->addWidget(m_copyBtn);
|
actionRow->addWidget(m_copyBtn);
|
||||||
|
actionRow->addSpacing(20); // room for resize grip when floating
|
||||||
|
|
||||||
mainLayout->addLayout(actionRow);
|
mainLayout->addLayout(actionRow);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace rcx {
|
|||||||
struct TabInfo {
|
struct TabInfo {
|
||||||
const NodeTree* tree;
|
const NodeTree* tree;
|
||||||
QString name;
|
QString name;
|
||||||
void* subPtr; // QMdiSubWindow* as void*
|
void* subPtr; // QDockWidget* as void*
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||||
|
|||||||
Reference in New Issue
Block a user