feat: floating windows like old windbg

This commit is contained in:
Sen66
2026-03-05 13:23:00 +01:00
parent 9a716444f4
commit 636176ee8c
7 changed files with 459 additions and 245 deletions

View File

@@ -1766,6 +1766,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QApplication::clipboard()->setText(addrs.join('\n'));
});
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
return;
}
@@ -2282,6 +2283,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
});
emit contextMenuAboutToShow(&menu, line);
menu.exec(globalPos);
}

View File

@@ -158,6 +158,7 @@ public:
signals:
void nodeSelected(int nodeIdx);
void selectionChanged(int count);
void contextMenuAboutToShow(QMenu* menu, int line);
private:
RcxDocument* m_doc;

View File

@@ -23,6 +23,7 @@
#include <QScreen>
#include <QScrollBar>
#include <QDateTime>
#include <algorithm>
#include <functional>
#include "themes/thememanager.h"
@@ -938,9 +939,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
int maxLen = 0;
const QStringList lines = result.text.split(QChar('\n'));
for (const auto& line : lines) {
int len = line.size();
int len = (int)line.size();
while (len > 0 && line[len - 1] == QChar(' ')) --len;
if (len > maxLen) maxLen = len;
maxLen = std::max(len, maxLen);
}
QFontMetrics fm(editorFont());
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));

View File

@@ -9,8 +9,6 @@
#include "mcp/mcp_bridge.h"
#include <QApplication>
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
@@ -469,23 +467,12 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
overlay->raise();
overlay->show();
m_mdiArea = new QMdiArea(this);
m_mdiArea->setFrameShape(QFrame::NoFrame);
m_mdiArea->setViewMode(QMdiArea::TabbedView);
m_mdiArea->setTabsClosable(true);
m_mdiArea->setTabsMovable(true);
{
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);
m_centralPlaceholder = new QWidget(this);
m_centralPlaceholder->setFixedSize(0, 0);
m_centralPlaceholder->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
setCentralWidget(m_centralPlaceholder);
setDockNestingEnabled(true);
setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North);
createWorkspaceDock();
createScannerDock();
@@ -521,10 +508,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
if (QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool())
m_mcp->start();
connect(m_mdiArea, &QMdiArea::subWindowActivated,
this, [this](QMdiSubWindow*) {
updateWindowTitle();
rebuildWorkspaceModel();
// Active doc tracking is handled per dock in createTab() via visibilityChanged
// Ensure border overlay is on top after initial layout settles
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)
@@ -1006,31 +997,21 @@ private:
int m_divX = -1;
void manualLayout() {
if (!tabRow || !label) return;
const int h = height();
const int tw = tabRow->sizeHint().width();
if (!label) return;
const int h = height();
const int gutter = 6;
tabRow->setGeometry(0, 0, tw, h);
m_divX = tw;
label->setGeometry(tw + 1 + gutter, 0,
qMax(0, width() - (tw + 1 + gutter)), h);
// Shared baseline so tab text and status text align.
// Nudge up by half the accent-line height so text centres
// 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;
if (tabRow) {
const int tw = tabRow->sizeHint().width();
tabRow->setGeometry(0, 0, tw, h);
m_divX = tw;
label->setGeometry(tw + 1 + gutter, 0,
qMax(0, width() - (tw + 1 + gutter)), h);
} else {
m_divX = -1;
label->setGeometry(gutter, 0, qMax(0, width() - gutter), h);
}
// Align label: set top margin so text baseline matches
int labelTop = by - fm.ascent();
label->setContentsMargins(0, labelTop, 0, 0);
label->setAlignment(Qt::AlignLeft | Qt::AlignTop);
label->setContentsMargins(0, 0, 0, 0);
label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
}
};
@@ -1045,35 +1026,11 @@ void MainWindow::createStatusBar() {
m_statusLabel->setContentsMargins(0, 0, 0, 0);
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
// View toggle buttons (Reclass / C/C++) — custom painted, no CSS
m_viewBtnGroup = new QButtonGroup(this);
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;
// View toggle is now per-pane via QTabWidget tab bar (Reclass / C/C++ tabs)
sb->tabRow = nullptr;
sb->label = m_statusLabel;
sb->setMinimumHeight(qMax(m_btnReclass->sizeHint().height(),
sb->fontMetrics().height() + 6));
connect(m_viewBtnGroup, &QButtonGroup::idClicked, this, [this](int id) {
setViewMode(id == 1 ? VM_Rendered : VM_Reclass);
});
sb->setMinimumHeight(sb->fontMetrics().height() + 6);
// 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.
@@ -1092,19 +1049,6 @@ void MainWindow::createStatusBar() {
sb->setTopLineColor(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->colBright = t.indHoverSpan;
}
@@ -1141,45 +1085,31 @@ void MainWindow::clearMcpStatus() {
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) {
SplitPane pane;
pane.tabWidget = new QTabWidget;
pane.tabWidget->setTabPosition(QTabWidget::South);
pane.tabWidget->tabBar()->setVisible(false);
pane.tabWidget->tabBar()->setVisible(true);
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)
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
pane.editor->setRelativeOffsets(
@@ -1342,6 +1272,32 @@ RcxEditor* MainWindow::activePaneEditor() {
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) {
if (viewRootId != 0) {
int idx = tree.indexOfId(viewRootId);
@@ -1360,20 +1316,133 @@ static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
return QStringLiteral("Untitled");
}
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto* splitter = new QSplitter(Qt::Horizontal);
splitter->setHandleWidth(1);
auto* ctrl = new RcxController(doc, splitter);
auto* sub = m_mdiArea->addSubWindow(splitter);
sub->setWindowIcon(QIcon()); // suppress app icon in MDI tabs
sub->setWindowTitle(doc->filePath.isEmpty()
? rootName(doc->tree) : QFileInfo(doc->filePath).fileName());
sub->setAttribute(Qt::WA_DeleteOnClose);
sub->showMaximized();
QString title = doc->filePath.isEmpty()
? rootName(doc->tree) : QFileInfo(doc->filePath).fileName();
auto* dock = new QDockWidget(title, this);
dock->setObjectName(QStringLiteral("DocDock_%1").arg(quintptr(dock), 0, 16));
dock->setFeatures(QDockWidget::DockWidgetClosable |
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& tab = m_tabs[sub];
auto* floatTitleBar = new QWidget(dock);
{
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
tab.panes.append(createSplitPane(tab));
@@ -1386,18 +1455,40 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
ctrl->setProjectDocuments(&m_allDocs);
rebuildAllDocs();
connect(sub, &QObject::destroyed, this, [this, sub]() {
auto it = m_tabs.find(sub);
// Track active tab via visibility
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()) {
it->doc->deleteLater();
m_tabs.erase(it);
}
m_docDocks.removeOne(dock);
if (m_activeDocDock == dock)
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
rebuildAllDocs();
rebuildWorkspaceModel();
});
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()) {
auto& node = ctrl->document()->tree.nodes[nodeIdx];
auto* ap = findActiveSplitPane();
@@ -1415,7 +1506,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
.arg(node.byteSize()));
}
// Update all rendered panes on selection change
auto it = m_tabs.find(sub);
auto it = m_tabs.find(dock);
if (it != m_tabs.end())
updateAllRenderedPanes(*it);
});
@@ -1425,32 +1516,42 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
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
connect(doc, &RcxDocument::documentChanged,
this, [this, sub]() {
auto it = m_tabs.find(sub);
this, [this, dock]() {
auto it = m_tabs.find(dock);
if (it != m_tabs.end())
QTimer::singleShot(0, this, [this, sub]() {
auto it2 = m_tabs.find(sub);
QTimer::singleShot(0, this, [this, dock]() {
auto it2 = m_tabs.find(dock);
if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2);
if (it2->doc->filePath.isEmpty())
sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
dock->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
}
rebuildWorkspaceModel();
updateWindowTitle();
});
});
connect(&doc->undoStack, &QUndoStack::indexChanged,
this, [this, sub](int) {
auto it = m_tabs.find(sub);
this, [this, dock](int) {
auto it = m_tabs.find(dock);
if (it != m_tabs.end())
QTimer::singleShot(0, this, [this, sub]() {
auto it2 = m_tabs.find(sub);
QTimer::singleShot(0, this, [this, dock]() {
auto it2 = m_tabs.find(dock);
if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2);
if (it2->doc->filePath.isEmpty())
sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
dock->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
}
updateWindowTitle();
rebuildWorkspaceModel();
@@ -1467,8 +1568,37 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
ctrl->refresh();
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
@@ -1527,7 +1657,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.
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
disconnect(it.key(), &QObject::destroyed, this, nullptr);
@@ -1666,11 +1796,13 @@ void MainWindow::selfTest() {
// Tab 1: Empty class for user work (created second, becomes active)
auto* userTab = project_new(QStringLiteral("class"));
m_mdiArea->setActiveSubWindow(userTab);
userTab->raise();
userTab->show();
#else
project_new();
auto* userTab = project_new(QStringLiteral("class"));
m_mdiArea->setActiveSubWindow(userTab);
userTab->raise();
userTab->show();
#endif
}
@@ -1849,24 +1981,50 @@ void MainWindow::applyTheme(const Theme& theme) {
// Update border overlay color
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
// MDI area tabs — text color + height handled by MenuBarStyle QProxyStyle
m_mdiArea->setStyleSheet(QStringLiteral(
"QTabBar::tab {"
" background: %1; padding: 0px 16px; border: none;"
"}"
"QTabBar::tab:selected { background: %2; }"
"QTabBar::tab:hover { background: %3; }")
.arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.name()));
// Style doc dock tab bars and remove dock borders
setStyleSheet(QStringLiteral(
"QMainWindow::separator { width: 1px; height: 1px; background: %4; }"
"QDockWidget { border: none; }"
"QDockWidget > QWidget { border: none; }")
.arg(theme.border.name()));
// Dim MDI tab text via palette (Fusion reads WindowText, not CSS color:)
if (auto* tabBar = m_mdiArea->findChild<QTabBar*>()) {
QPalette tp = tabBar->palette();
tp.setColor(QPalette::WindowText, theme.textDim);
tabBar->setPalette(tp);
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 {"
" background: %1; padding: 0px 16px; border: none;"
"}"
"QTabBar::tab:selected { background: %2; }"
"QTabBar::tab:hover { background: %3; }")
.arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.name()));
QPalette tp = tabBar->palette();
tp.setColor(QPalette::WindowText, theme.textDim);
tabBar->setPalette(tp);
}
}
// Re-style ✕ close buttons on MDI tabs
styleTabCloseButtons();
// Restyle per-pane view tab bars (Reclass / C++)
{
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
{
@@ -1875,26 +2033,11 @@ void MainWindow::applyTheme(const Theme& theme) {
sbPal.setColor(QPalette::WindowText, theme.textDim);
statusBar()->setPalette(sbPal);
}
// View toggle buttons + status bar chrome
// Status bar chrome
{
auto applyColors = [&](ViewTabButton* btn) {
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->setDividerColor(theme.border);
}
auto* fsb = static_cast<FlatStatusBar*>(statusBar());
fsb->setTopLineColor(theme.border);
fsb->setDividerColor(theme.border);
}
// Resize grip (direct child of main window, not in status bar)
if (auto* w = findChild<QWidget*>("resizeGrip"))
@@ -1955,6 +2098,33 @@ void MainWindow::applyTheme(const Theme& theme) {
if (m_scanDockGrip)
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
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
@@ -2079,28 +2249,30 @@ void MainWindow::setEditorFont(const QString& fontName) {
m_scannerPanel->setEditorFont(f);
if (m_scanDockTitle)
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 {
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub))
return m_tabs[sub].ctrl;
if (m_activeDocDock && m_tabs.contains(m_activeDocDock))
return m_tabs[m_activeDocDock].ctrl;
return nullptr;
}
MainWindow::TabState* MainWindow::activeTab() {
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub))
return &m_tabs[sub];
if (m_activeDocDock && m_tabs.contains(m_activeDocDock))
return &m_tabs[m_activeDocDock];
return nullptr;
}
MainWindow::TabState* MainWindow::tabByIndex(int index) {
auto subs = m_mdiArea->subWindowList();
if (index < 0 || index >= subs.size()) return nullptr;
auto* sub = subs[index];
if (m_tabs.contains(sub))
return &m_tabs[sub];
if (index < 0 || index >= m_docDocks.size()) return nullptr;
auto* dock = m_docDocks[index];
if (m_tabs.contains(dock))
return &m_tabs[dock];
return nullptr;
}
@@ -2109,9 +2281,9 @@ void MainWindow::updateWindowTitle() {
setWindowTitle(QStringLiteral("Reclass"));
#else
QString title;
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub)) {
auto& tab = m_tabs[sub];
auto* activeDock = m_activeDocDock;
if (activeDock && m_tabs.contains(activeDock)) {
auto& tab = m_tabs[activeDock];
QString name = tab.doc->filePath.isEmpty()
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
: QFileInfo(tab.doc->filePath).fileName();
@@ -2196,10 +2368,8 @@ void MainWindow::setViewMode(ViewMode mode) {
syncViewButtons(mode);
}
void MainWindow::syncViewButtons(ViewMode mode) {
QSignalBlocker block(m_viewBtnGroup);
if (mode == VM_Rendered) m_btnRendered->setChecked(true);
else m_btnReclass->setChecked(true);
void MainWindow::syncViewButtons(ViewMode /*mode*/) {
// View toggle is now per-pane via QTabWidget tab bar — nothing to sync globally
}
// ── Find the root-level struct ancestor for a node ──
@@ -2380,7 +2550,7 @@ void MainWindow::importReclassXml() {
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
m_mdiArea->closeAllSubWindows();
closeAllDocDocks();
createTab(doc);
rebuildWorkspaceModel();
setAppStatus(QStringLiteral("Imported %1 classes from %2")
@@ -2429,7 +2599,7 @@ void MainWindow::importFromSource() {
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
m_mdiArea->closeAllSubWindows();
closeAllDocDocks();
createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
@@ -2479,7 +2649,7 @@ void MainWindow::importPdb() {
auto* doc = new rcx::RcxDocument(this);
doc->tree = std::move(tree);
m_mdiArea->closeAllSubWindows();
closeAllDocDocks();
createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
@@ -2607,7 +2777,7 @@ void MainWindow::showTypeAliasesDialog() {
// ── Project Lifecycle API ──
QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
QDockWidget* MainWindow::project_new(const QString& classKeyword) {
auto* doc = new RcxDocument(this);
QByteArray data(256, '\0');
@@ -2623,20 +2793,20 @@ QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
doc->provider = currentCtrl->document()->provider;
}
auto* sub = createTab(doc);
auto* dock = createTab(doc);
// Copy saved sources to new tab's controller
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
auto& newTab = m_tabs[sub];
auto& newTab = m_tabs[dock];
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
currentCtrl->activeSourceIndex());
}
rebuildWorkspaceModel();
return sub;
return dock;
}
QMdiSubWindow* MainWindow::project_open(const QString& path) {
QDockWidget* MainWindow::project_open(const QString& path) {
QString filePath = path;
if (filePath.isEmpty()) {
filePath = QFileDialog::getOpenFileName(this,
@@ -2670,8 +2840,8 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
}
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
m_mdiArea->closeAllSubWindows();
auto* sub = createTab(doc);
closeAllDocDocks();
auto* dock = createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
int classCount = 0;
@@ -2680,7 +2850,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
setAppStatus(QStringLiteral("Imported %1 classes from %2")
.arg(classCount).arg(QFileInfo(filePath).fileName()));
addRecentFile(filePath);
return sub;
return dock;
}
auto* doc = new RcxDocument(this);
@@ -2691,19 +2861,19 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
}
// Close all existing tabs so the project replaces the current state
m_mdiArea->closeAllSubWindows();
closeAllDocDocks();
auto* sub = createTab(doc);
auto* dock = createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
addRecentFile(filePath);
return sub;
return dock;
}
bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
if (!sub) sub = m_mdiArea->activeSubWindow();
if (!sub || !m_tabs.contains(sub)) return false;
auto& tab = m_tabs[sub];
bool MainWindow::project_save(QDockWidget* dock, bool saveAs) {
if (!dock) dock = m_activeDocDock;
if (!dock || !m_tabs.contains(dock)) return false;
auto& tab = m_tabs[dock];
if (saveAs || tab.doc->filePath.isEmpty()) {
QString path = QFileDialog::getSaveFileName(this,
@@ -2719,13 +2889,23 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
return true;
}
void MainWindow::project_close(QMdiSubWindow* sub) {
if (!sub) sub = m_mdiArea->activeSubWindow();
if (!sub) return;
sub->close();
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);
dock->close();
}
}
// ── Workspace Dock ──
void MainWindow::createWorkspaceDock() {
@@ -2855,10 +3035,10 @@ void MainWindow::createWorkspaceDock() {
auto subVar = index.data(Qt::UserRole);
if (!subVar.isValid()) return;
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
if (!sub || !m_tabs.contains(sub)) return;
auto* dock = static_cast<QDockWidget*>(subVar.value<void*>());
if (!dock || !m_tabs.contains(dock)) return;
auto& tab = m_tabs[sub];
auto& tab = m_tabs[dock];
int ni = tab.doc->tree.indexOfId(structId);
if (ni < 0) return;
QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
@@ -2960,16 +3140,18 @@ void MainWindow::createWorkspaceDock() {
auto subVar = index.data(Qt::UserRole);
if (!subVar.isValid()) return;
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
if (!sub || !m_tabs.contains(sub)) return;
auto* dock = static_cast<QDockWidget*>(subVar.value<void*>());
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);
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
uint64_t parentId = tree.nodes[ni].parentId;
@@ -2986,9 +3168,9 @@ void MainWindow::createWorkspaceDock() {
}
// If active pane is in C/C++ mode, refresh after navigation settles
QTimer::singleShot(0, this, [this, sub]() {
if (!m_tabs.contains(sub)) return;
auto& t = m_tabs[sub];
QTimer::singleShot(0, this, [this, dock]() {
if (!m_tabs.contains(dock)) return;
auto& t = m_tabs[dock];
if (t.activePaneIdx >= 0 && t.activePaneIdx < t.panes.size()) {
auto& p = t.panes[t.activePaneIdx];
if (p.viewMode == VM_Rendered)
@@ -3066,6 +3248,31 @@ void MainWindow::createScannerDock() {
addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock);
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
m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr<rcx::Provider> {
auto* ctrl = activeController();
@@ -3334,6 +3541,11 @@ void MainWindow::changeEvent(QEvent* event) {
}
if (event->type() == QEvent::WindowStateChange && m_titleBar)
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) {

View File

@@ -4,8 +4,6 @@
#include "pluginmanager.h"
#include "scannerpanel.h"
#include <QMainWindow>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QLabel>
#include <QSplitter>
#include <QTabWidget>
@@ -72,22 +70,19 @@ public:
void clearMcpStatus();
// Project Lifecycle API
QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);
QDockWidget* project_new(const QString& classKeyword = QString());
QDockWidget* project_open(const QString& path = {});
bool project_save(QDockWidget* dock = nullptr, bool saveAs = false);
void project_close(QDockWidget* dock = nullptr);
private:
enum ViewMode { VM_Reclass, VM_Rendered };
QMdiArea* m_mdiArea;
QWidget* m_centralPlaceholder;
ShimmerLabel* m_statusLabel;
QString m_appStatus;
bool m_mcpBusy = false;
QTimer* m_mcpClearTimer = nullptr;
QButtonGroup* m_viewBtnGroup = nullptr;
QPushButton* m_btnReclass = nullptr;
QPushButton* m_btnRendered = nullptr;
TitleBarWidget* m_titleBar = nullptr;
QMenuBar* m_menuBar = nullptr;
bool m_menuBarTitleCase = false;
@@ -117,7 +112,9 @@ private:
QVector<SplitPane> panes;
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
void rebuildAllDocs();
@@ -134,8 +131,9 @@ private:
TabState* activeTab();
TabState* tabByIndex(int index);
int tabCount() const { return m_tabs.size(); }
QMdiSubWindow* createTab(RcxDocument* doc);
QDockWidget* createTab(RcxDocument* doc);
void updateWindowTitle();
void closeAllDocDocks();
void setViewMode(ViewMode mode);
void updateRenderedView(TabState& tab, SplitPane& pane);
@@ -145,7 +143,6 @@ private:
SplitPane createSplitPane(TabState& tab);
void applyTheme(const Theme& theme);
void styleTabCloseButtons();
void syncViewButtons(ViewMode mode);
SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane();

View File

@@ -183,6 +183,7 @@ ScannerPanel::ScannerPanel(QWidget* parent)
QStringLiteral("Copy Address"), this);
m_copyBtn->setEnabled(false);
actionRow->addWidget(m_copyBtn);
actionRow->addSpacing(20); // room for resize grip when floating
mainLayout->addLayout(actionRow);

View File

@@ -10,7 +10,7 @@ namespace rcx {
struct TabInfo {
const NodeTree* tree;
QString name;
void* subPtr; // QMdiSubWindow* as void*
void* subPtr; // QDockWidget* as void*
};
// Sentinel value stored in UserRole+1 to mark the Project group node.