mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: custom dock titlebar, resize grip symmetry fix, status bar font sync
- 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
This commit is contained in:
108
src/main.cpp
108
src/main.cpp
@@ -45,6 +45,8 @@
|
||||
#include <Qsci/qscilexercpp.h>
|
||||
#include <QProxyStyle>
|
||||
#include <QDesktopServices>
|
||||
#include <QWindow>
|
||||
#include <QMouseEvent>
|
||||
#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*>("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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <QPainter>
|
||||
#include <QCursor>
|
||||
#include <QScreen>
|
||||
#include <QMainWindow>
|
||||
#include <QStatusBar>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include <Qsci/qsciscintillabase.h>
|
||||
#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)
|
||||
|
||||
Reference in New Issue
Block a user