From c7afe363f34e31eee4e1802bd2bc121617533a1c Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Thu, 19 Feb 2026 18:10:52 -0700 Subject: [PATCH] feat: custom dock titlebar, resize grip symmetry fix, status bar font sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace default dock widget titlebar with custom label + themed ✕ close button - Remove float/popout button from project tree dock - Fix resize grip corner symmetry (bottom margin 4→0) - Sync editor font to status bar and dock titlebar at startup - Add testResizeGripCornerSymmetry test --- src/main.cpp | 108 +++++++++++++++++++++++++++++++++++++++++- src/mainwindow.h | 2 + tests/test_editor.cpp | 100 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index e69e7a4..07ac38f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,6 +45,8 @@ #include #include #include +#include +#include #include "themes/thememanager.h" #include "themes/themeeditor.h" #include "optionsdialog.h" @@ -496,11 +498,55 @@ void MainWindow::createMenus() { Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about); } +// ── Themed resize grip (replaces ugly default QSizeGrip) ── +class ResizeGrip : public QWidget { +public: + explicit ResizeGrip(QWidget* parent) : QWidget(parent) { + setFixedSize(16, 16); + setCursor(Qt::SizeFDiagCursor); + m_color = rcx::ThemeManager::instance().current().textFaint; + } + void setGripColor(const QColor& c) { m_color = c; update(); } +protected: + void paintEvent(QPaintEvent*) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + p.setBrush(m_color); + // 6 dots in a triangle pointing bottom-right (VS2022 style) + const double r = 1.0, s = 4.0; + double bx = width() - 5, by = height() - 4; + // bottom row: 3 dots + p.drawEllipse(QPointF(bx, by), r, r); + p.drawEllipse(QPointF(bx - s, by), r, r); + p.drawEllipse(QPointF(bx - 2 * s, by), r, r); + // middle row: 2 dots + p.drawEllipse(QPointF(bx, by - s), r, r); + p.drawEllipse(QPointF(bx - s, by - s), r, r); + // top row: 1 dot + p.drawEllipse(QPointF(bx, by - 2 * s), r, r); + } + void mousePressEvent(QMouseEvent* e) override { + if (e->button() == Qt::LeftButton) { + window()->windowHandle()->startSystemResize(Qt::BottomEdge | Qt::RightEdge); + e->accept(); + } + } +private: + QColor m_color; +}; + void MainWindow::createStatusBar() { m_statusLabel = new QLabel("Ready"); m_statusLabel->setContentsMargins(10, 0, 0, 0); - statusBar()->setContentsMargins(0, 4, 0, 4); + statusBar()->setContentsMargins(0, 4, 0, 0); + statusBar()->setSizeGripEnabled(false); // disable ugly default grip statusBar()->addWidget(m_statusLabel, 1); + + auto* grip = new ResizeGrip(this); + grip->setObjectName("resizeGrip"); + statusBar()->addPermanentWidget(grip); + { const auto& t = ThemeManager::instance().current(); QPalette sbPal = statusBar()->palette(); @@ -509,6 +555,14 @@ void MainWindow::createStatusBar() { statusBar()->setPalette(sbPal); statusBar()->setAutoFillBackground(true); } + + // Sync status bar font with editor font at startup + { + QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString(); + QFont f(fontName, 12); + f.setFixedPitch(true); + statusBar()->setFont(f); + } } void MainWindow::applyTabWidgetStyle(QTabWidget* tw) { @@ -1062,12 +1116,14 @@ void MainWindow::applyTheme(const Theme& theme) { // Re-style ✕ close buttons on MDI tabs styleTabCloseButtons(); - // Status bar + // Status bar + resize grip { QPalette sbPal = statusBar()->palette(); sbPal.setColor(QPalette::Window, theme.background); sbPal.setColor(QPalette::WindowText, theme.textDim); statusBar()->setPalette(sbPal); + auto* grip = statusBar()->findChild("resizeGrip"); + if (grip) grip->setGripColor(theme.textFaint); } // Workspace tree: text color matches menu bar @@ -1077,6 +1133,15 @@ void MainWindow::applyTheme(const Theme& theme) { m_workspaceTree->setPalette(tp); } + // Dock titlebar: restyle label + close button + if (m_dockTitleLabel) + m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name())); + if (m_dockCloseBtn) + m_dockCloseBtn->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())); + // Split pane tab widgets for (auto& state : m_tabs) { for (auto& pane : state.panes) { @@ -1165,6 +1230,9 @@ void MainWindow::setEditorFont(const QString& fontName) { // Sync workspace tree font if (m_workspaceTree) m_workspaceTree->setFont(f); + // Sync dock titlebar font + if (m_dockTitleLabel) + m_dockTitleLabel->setFont(f); // Sync status bar font statusBar()->setFont(f); } @@ -1644,6 +1712,42 @@ void MainWindow::createWorkspaceDock() { m_workspaceDock = new QDockWidget("Project Tree", this); m_workspaceDock->setObjectName("WorkspaceDock"); m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + m_workspaceDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable); + + // Custom titlebar: label + ✕ close button (matches MDI tab style) + { + const auto& t = ThemeManager::instance().current(); + + auto* titleBar = new QWidget(m_workspaceDock); + auto* layout = new QHBoxLayout(titleBar); + layout->setContentsMargins(6, 2, 2, 2); + layout->setSpacing(0); + + m_dockTitleLabel = new QLabel("Project Tree", titleBar); + m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(t.textDim.name())); + { + QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString(); + QFont f(fontName, 12); + f.setFixedPitch(true); + m_dockTitleLabel->setFont(f); + } + layout->addWidget(m_dockTitleLabel); + + layout->addStretch(); + + m_dockCloseBtn = new QToolButton(titleBar); + m_dockCloseBtn->setText(QStringLiteral("\u2715")); + m_dockCloseBtn->setAutoRaise(true); + m_dockCloseBtn->setCursor(Qt::PointingHandCursor); + m_dockCloseBtn->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(m_dockCloseBtn, &QToolButton::clicked, m_workspaceDock, &QDockWidget::close); + layout->addWidget(m_dockCloseBtn); + + m_workspaceDock->setTitleBarWidget(titleBar); + } m_workspaceTree = new QTreeView(m_workspaceDock); m_workspaceModel = new QStandardItemModel(this); diff --git a/src/mainwindow.h b/src/mainwindow.h index 102320e..598f677 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -124,6 +124,8 @@ private: QDockWidget* m_workspaceDock = nullptr; QTreeView* m_workspaceTree = nullptr; QStandardItemModel* m_workspaceModel = nullptr; + QLabel* m_dockTitleLabel = nullptr; + QToolButton* m_dockCloseBtn = nullptr; void createWorkspaceDock(); void rebuildWorkspaceModel(); void updateBorderColor(const QColor& color); diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index c19d72d..6daa7da 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include "editor.h" @@ -2045,6 +2047,104 @@ private slots: m_editor->applyDocument(m_result); } + + // ── Test: resize grip equidistant from right and bottom window edges ── + void testResizeGripCornerSymmetry() { + // Reproduce the exact MainWindow status bar + grip setup + QMainWindow win; + win.resize(400, 300); + win.statusBar()->setSizeGripEnabled(false); + win.statusBar()->setContentsMargins(0, 4, 0, 0); + + // Inline replica of the ResizeGrip paint (same constants as main.cpp) + class Grip : public QWidget { + public: + explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(16, 16); } + protected: + void paintEvent(QPaintEvent*) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + p.setBrush(Qt::red); // high-contrast so we can find it + const double r = 1.0, s = 4.0; + double bx = width() - 5, by = height() - 4; + p.drawEllipse(QPointF(bx, by), r, r); + p.drawEllipse(QPointF(bx - s, by), r, r); + p.drawEllipse(QPointF(bx - 2 * s, by), r, r); + p.drawEllipse(QPointF(bx, by - s), r, r); + p.drawEllipse(QPointF(bx - s, by - s), r, r); + p.drawEllipse(QPointF(bx, by - 2 * s), r, r); + } + }; + + auto* grip = new Grip(&win); + win.statusBar()->addPermanentWidget(grip); + + // Use a known background so non-grip pixels are easy to identify + QPalette pal = win.statusBar()->palette(); + pal.setColor(QPalette::Window, QColor(30, 30, 30)); + win.statusBar()->setPalette(pal); + win.statusBar()->setAutoFillBackground(true); + + win.show(); + QVERIFY(QTest::qWaitForWindowExposed(&win)); + QTest::qWait(100); // let paint settle + + // Grab just the window contents (no DWM shadow) + QPixmap px = win.grab(); + QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32); + int W = img.width(); + int H = img.height(); + QVERIFY(W > 50); + QVERIFY(H > 50); + + // Scan from bottom-right to find the bottommost-rightmost red pixel + // (the corner dot of the grip triangle) + int gripRight = -1, gripBottom = -1; + for (int y = H - 1; y >= H - 40 && gripBottom < 0; --y) { + for (int x = W - 1; x >= W - 40; --x) { + QColor c(img.pixel(x, y)); + if (c.red() > 180 && c.green() < 80 && c.blue() < 80) { + gripRight = x; + gripBottom = y; + break; + } + } + if (gripBottom >= 0) break; + } + + QVERIFY2(gripRight >= 0 && gripBottom >= 0, + "Could not find red grip dot in bottom-right corner"); + + int gapRight = (W - 1) - gripRight; + int gapBottom = (H - 1) - gripBottom; + + // Save diagnostic image with markers + { + QImage diag = img.copy(); + QPainter dp(&diag); + dp.setPen(QPen(Qt::cyan, 1)); + // Mark the found dot + dp.drawRect(gripRight - 3, gripBottom - 3, 6, 6); + // Draw gap measurement lines + dp.setPen(QPen(Qt::yellow, 1)); + dp.drawLine(gripRight, gripBottom, W - 1, gripBottom); // right gap + dp.drawLine(gripRight, gripBottom, gripRight, H - 1); // bottom gap + dp.end(); + diag.save("grip_corner_diag.png"); + } + + QString msg = QString("gapRight=%1 gapBottom=%2 (diff=%3) gripPos=(%4,%5) winSize=%6x%7") + .arg(gapRight).arg(gapBottom).arg(qAbs(gapRight - gapBottom)) + .arg(gripRight).arg(gripBottom).arg(W).arg(H); + + // The gaps must be equal (symmetric corner placement) + QVERIFY2(qAbs(gapRight - gapBottom) <= 1, + qPrintable("Grip not equidistant from edges: " + msg)); + + // Also log the values even on pass + qDebug() << "Grip corner symmetry:" << msg; + } }; QTEST_MAIN(TestEditor)