Added custom title bar & border color when focused

This commit is contained in:
Sen66
2026-02-13 19:09:11 +01:00
parent 9a342286ee
commit 0df52e82b8
9 changed files with 303 additions and 13 deletions

View File

@@ -60,6 +60,8 @@ add_executable(Reclass
src/themes/themeeditor.h
src/themes/themeeditor.cpp
src/mainwindow.h
src/titlebar.h
src/titlebar.cpp
src/mcp/mcp_bridge.h
src/mcp/mcp_bridge.cpp
$<$<PLATFORM_ID:Windows>:src/app.rc>

View File

@@ -46,6 +46,7 @@
#ifdef _WIN32
#include <windows.h>
#include <windowsx.h>
#include <dwmapi.h>
#include <dbghelp.h>
#include <cstdio>
@@ -230,6 +231,21 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
qApp->setStyleSheet(QString());
}
class BorderOverlay : public QWidget {
public:
QColor color;
explicit BorderOverlay(QWidget* parent) : QWidget(parent) {
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_NoSystemBackground);
setFocusPolicy(Qt::NoFocus);
}
void paintEvent(QPaintEvent*) override {
QPainter p(this);
p.setPen(color);
p.drawRect(0, 0, width() - 1, height() - 1);
}
};
namespace rcx {
// MainWindow class declaration is in mainwindow.h
@@ -238,6 +254,32 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle("Reclass");
resize(1200, 800);
// Frameless window with system menu (Alt+Space) and min/max/close support
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint
| Qt::WindowMinMaxButtonsHint);
// Custom title bar (replaces native menu bar area in QMainWindow)
m_titleBar = new TitleBarWidget(this);
m_titleBar->applyTheme(ThemeManager::instance().current());
setMenuWidget(m_titleBar);
#ifdef _WIN32
// 1px top margin preserves DWM drop shadow on the frameless window
{
auto hwnd = reinterpret_cast<HWND>(winId());
MARGINS margins = {0, 0, 1, 0};
DwmExtendFrameIntoClientArea(hwnd, &margins);
}
#endif
// Border overlay — draws a 1px colored border on top of everything
auto* overlay = new BorderOverlay(this);
m_borderOverlay = overlay;
overlay->color = ThemeManager::instance().current().borderFocused;
overlay->setGeometry(rect());
overlay->raise();
overlay->show();
m_mdiArea = new QMdiArea(this);
m_mdiArea->setViewMode(QMdiArea::TabbedView);
m_mdiArea->setTabsClosable(true);
@@ -306,7 +348,7 @@ QIcon MainWindow::makeIcon(const QString& svgPath) {
void MainWindow::createMenus() {
// File
auto* file = menuBar()->addMenu("&File");
auto* file = m_titleBar->menuBar()->addMenu("&File");
file->addAction("&New", this, &MainWindow::newDocument, QKeySequence::New);
file->addAction("New &Tab", this, &MainWindow::newFile, QKeySequence(Qt::CTRL | Qt::Key_T));
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", this, &MainWindow::openFile, QKeySequence::Open);
@@ -321,14 +363,14 @@ void MainWindow::createMenus() {
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close));
// Edit
auto* edit = menuBar()->addMenu("&Edit");
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", this, &MainWindow::undo, QKeySequence::Undo);
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", this, &MainWindow::redo, QKeySequence::Redo);
edit->addSeparator();
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
// View
auto* view = menuBar()->addMenu("&View");
auto* view = m_titleBar->menuBar()->addMenu("&View");
view->addAction(makeIcon(":/vsicons/split-horizontal.svg"), "Split &Horizontal", this, &MainWindow::splitView);
view->addAction(makeIcon(":/vsicons/chrome-close.svg"), "&Unsplit", this, &MainWindow::unsplitView);
view->addSeparator();
@@ -371,7 +413,7 @@ void MainWindow::createMenus() {
view->addAction(m_workspaceDock->toggleViewAction());
// Node
auto* node = menuBar()->addMenu("&Node");
auto* node = m_titleBar->menuBar()->addMenu("&Node");
node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", this, &MainWindow::addNode, QKeySequence(Qt::Key_Insert));
node->addAction(makeIcon(":/vsicons/remove.svg"), "&Remove Field", this, &MainWindow::removeNode, QKeySequence::Delete);
node->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "Change &Type", this, &MainWindow::changeNodeType, QKeySequence(Qt::Key_T));
@@ -379,11 +421,11 @@ void MainWindow::createMenus() {
node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
// Plugins
auto* plugins = menuBar()->addMenu("&Plugins");
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
plugins->addAction("&Manage Plugins...", this, &MainWindow::showPluginsDialog);
// Help
auto* help = menuBar()->addMenu("&Help");
auto* help = m_titleBar->menuBar()->addMenu("&Help");
help->addAction(makeIcon(":/vsicons/question.svg"), "&About Reclass", this, &MainWindow::about);
}
@@ -879,6 +921,12 @@ void MainWindow::toggleMcp() {
void MainWindow::applyTheme(const Theme& theme) {
applyGlobalTheme(theme);
// Custom title bar
m_titleBar->applyTheme(theme);
// Update border overlay color
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
// MDI area tabs
m_mdiArea->setStyleSheet(QStringLiteral(
"QTabBar::tab {"
@@ -984,6 +1032,7 @@ MainWindow::TabState* MainWindow::tabByIndex(int index) {
}
void MainWindow::updateWindowTitle() {
QString title;
auto* sub = m_mdiArea->activeSubWindow();
if (sub && m_tabs.contains(sub)) {
auto& tab = m_tabs[sub];
@@ -991,10 +1040,12 @@ void MainWindow::updateWindowTitle() {
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
: QFileInfo(tab.doc->filePath).fileName();
if (tab.doc->modified) name += " *";
setWindowTitle(name + " - Reclass");
title = name + " - Reclass";
} else {
setWindowTitle("Reclass");
title = "Reclass";
}
setWindowTitle(title);
m_titleBar->setTitle(title);
}
// ── Rendered view setup ──
@@ -1474,6 +1525,29 @@ void MainWindow::showPluginsDialog() {
dialog.exec();
}
void MainWindow::changeEvent(QEvent* event) {
QMainWindow::changeEvent(event);
if (event->type() == QEvent::ActivationChange) {
const auto& t = ThemeManager::instance().current();
updateBorderColor(isActiveWindow() ? t.borderFocused : t.border);
}
if (event->type() == QEvent::WindowStateChange)
m_titleBar->updateMaximizeIcon();
}
void MainWindow::resizeEvent(QResizeEvent* event) {
QMainWindow::resizeEvent(event);
if (m_borderOverlay) {
m_borderOverlay->setGeometry(rect());
m_borderOverlay->raise();
}
}
void MainWindow::updateBorderColor(const QColor& color) {
static_cast<BorderOverlay*>(m_borderOverlay)->color = color;
m_borderOverlay->update();
}
} // namespace rcx
// ── Entry point ──

View File

@@ -1,5 +1,6 @@
#pragma once
#include "controller.h"
#include "titlebar.h"
#include "pluginmanager.h"
#include <QMainWindow>
#include <QMdiArea>
@@ -59,11 +60,13 @@ public:
private:
enum ViewMode { VM_Reclass, VM_Rendered };
QMdiArea* m_mdiArea;
QLabel* m_statusLabel;
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
QMdiArea* m_mdiArea;
QLabel* m_statusLabel;
TitleBarWidget* m_titleBar = nullptr;
QWidget* m_borderOverlay = nullptr;
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
struct SplitPane {
QTabWidget* tabWidget = nullptr;
@@ -114,6 +117,11 @@ private:
QStandardItemModel* m_workspaceModel = nullptr;
void createWorkspaceDock();
void rebuildWorkspaceModel();
void updateBorderColor(const QColor& color);
protected:
void changeEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
};
} // namespace rcx

View File

@@ -20,6 +20,9 @@
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
<file alias="split-horizontal.svg">vsicons/split-horizontal.svg</file>
<file alias="chrome-close.svg">vsicons/chrome-close.svg</file>
<file alias="chrome-minimize.svg">vsicons/chrome-minimize.svg</file>
<file alias="chrome-maximize.svg">vsicons/chrome-maximize.svg</file>
<file alias="chrome-restore.svg">vsicons/chrome-restore.svg</file>
<file alias="text-size.svg">vsicons/text-size.svg</file>
<file alias="add.svg">vsicons/add.svg</file>
<file alias="remove.svg">vsicons/remove.svg</file>

View File

@@ -11,6 +11,7 @@ static const ColorField kFields[] = {
{"backgroundAlt", &Theme::backgroundAlt},
{"surface", &Theme::surface},
{"border", &Theme::border},
{"borderFocused", &Theme::borderFocused},
{"button", &Theme::button},
{"text", &Theme::text},
{"textDim", &Theme::textDim},
@@ -61,6 +62,7 @@ Theme Theme::reclassDark() {
t.backgroundAlt = QColor("#252526");
t.surface = QColor("#2a2d2e");
t.border = QColor("#3c3c3c");
t.borderFocused = QColor("#64e6b450"); // indHoverSpan at ~40% alpha
t.button = QColor("#333333");
t.text = QColor("#d4d4d4");
t.textDim = QColor("#858585");
@@ -92,6 +94,7 @@ Theme Theme::warm() {
t.backgroundAlt = QColor("#2a2a2a");
t.surface = QColor("#2a2a2a");
t.border = QColor("#373737");
t.borderFocused = QColor("#64aa9565"); // indHoverSpan at ~40% alpha
t.button = QColor("#373737");
t.text = QColor("#AAA99F");
t.textDim = QColor("#7a7a6e");

View File

@@ -13,6 +13,7 @@ struct Theme {
QColor backgroundAlt; // panels, tab selected, tooltips
QColor surface; // alternateBase
QColor border; // separators, menu borders
QColor borderFocused; // window border when focused
QColor button; // button bg
// ── Text ──

View File

@@ -124,6 +124,7 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
{"Background Alt", &Theme::backgroundAlt},
{"Surface", &Theme::surface},
{"Border", &Theme::border},
{"Border Focused", &Theme::borderFocused},
{"Button", &Theme::button},
});
addGroup("Text", {

158
src/titlebar.cpp Normal file
View File

@@ -0,0 +1,158 @@
#include "titlebar.h"
#include <QMouseEvent>
#include <QPainter>
#include <QStyle>
#include <QWindow>
namespace rcx {
TitleBarWidget::TitleBarWidget(QWidget* parent)
: QWidget(parent)
, m_theme(Theme::reclassDark())
{
setFixedHeight(32);
auto* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
// App icon
auto* iconLabel = new QLabel(this);
iconLabel->setPixmap(QPixmap(":/icons/class.png").scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation));
iconLabel->setFixedSize(32, 32);
iconLabel->setAlignment(Qt::AlignCenter);
iconLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
layout->addWidget(iconLabel);
// Menu bar
m_menuBar = new QMenuBar(this);
m_menuBar->setNativeMenuBar(false);
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
layout->addWidget(m_menuBar);
layout->addStretch();
// Title label (centered, transparent to mouse so drag works through it)
m_titleLabel = new QLabel(this);
m_titleLabel->setAlignment(Qt::AlignCenter);
m_titleLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
layout->addWidget(m_titleLabel);
layout->addStretch();
// Chrome buttons
m_btnMin = makeChromeButton(":/vsicons/chrome-minimize.svg");
m_btnMax = makeChromeButton(":/vsicons/chrome-maximize.svg");
m_btnClose = makeChromeButton(":/vsicons/chrome-close.svg");
layout->addWidget(m_btnMin);
layout->addWidget(m_btnMax);
layout->addWidget(m_btnClose);
connect(m_btnMin, &QToolButton::clicked, this, [this]() {
window()->showMinimized();
});
connect(m_btnMax, &QToolButton::clicked, this, [this]() {
toggleMaximize();
});
connect(m_btnClose, &QToolButton::clicked, this, [this]() {
window()->close();
});
}
QToolButton* TitleBarWidget::makeChromeButton(const QString& iconPath) {
auto* btn = new QToolButton(this);
btn->setIcon(QIcon(iconPath));
btn->setIconSize(QSize(16, 16));
btn->setFixedSize(46, 32);
btn->setAutoRaise(true);
btn->setFocusPolicy(Qt::NoFocus);
return btn;
}
void TitleBarWidget::setTitle(const QString& title) {
m_titleLabel->setText(title);
}
void TitleBarWidget::applyTheme(const Theme& theme) {
m_theme = theme;
// Title bar background
setAutoFillBackground(true);
QPalette pal = palette();
pal.setColor(QPalette::Window, theme.background);
setPalette(pal);
// Title text
m_titleLabel->setStyleSheet(
QStringLiteral("QLabel { color: %1; font-size: 12px; }")
.arg(theme.textDim.name()));
// Menu bar styling — transparent background, themed text
m_menuBar->setStyleSheet(
QStringLiteral(
"QMenuBar { background: transparent; border: none; }"
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }"
"QMenuBar::item:selected { background: %2; }"
"QMenuBar::item:pressed { background: %2; }")
.arg(theme.textDim.name(), theme.hover.name()));
// Chrome buttons
QString btnStyle = QStringLiteral(
"QToolButton { background: transparent; border: none; }"
"QToolButton:hover { background: %1; }")
.arg(theme.hover.name());
m_btnMin->setStyleSheet(btnStyle);
m_btnMax->setStyleSheet(btnStyle);
// Close button: red hover
m_btnClose->setStyleSheet(QStringLiteral(
"QToolButton { background: transparent; border: none; }"
"QToolButton:hover { background: #c42b1c; }"));
update();
}
void TitleBarWidget::updateMaximizeIcon() {
if (window()->isMaximized())
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));
else
m_btnMax->setIcon(QIcon(":/vsicons/chrome-maximize.svg"));
}
void TitleBarWidget::toggleMaximize() {
if (window()->isMaximized())
window()->showNormal();
else
window()->showMaximized();
updateMaximizeIcon();
}
void TitleBarWidget::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
window()->windowHandle()->startSystemMove();
event->accept();
} else {
QWidget::mousePressEvent(event);
}
}
void TitleBarWidget::mouseDoubleClickEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
toggleMaximize();
event->accept();
} else {
QWidget::mouseDoubleClickEvent(event);
}
}
void TitleBarWidget::paintEvent(QPaintEvent* event) {
QWidget::paintEvent(event);
// 1px bottom border
QPainter p(this);
p.setPen(m_theme.border);
p.drawLine(0, height() - 1, width() - 1, height() - 1);
}
} // namespace rcx

40
src/titlebar.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include "themes/theme.h"
#include <QWidget>
#include <QMenuBar>
#include <QToolButton>
#include <QLabel>
#include <QHBoxLayout>
namespace rcx {
class TitleBarWidget : public QWidget {
Q_OBJECT
public:
explicit TitleBarWidget(QWidget* parent = nullptr);
QMenuBar* menuBar() const { return m_menuBar; }
void setTitle(const QString& title);
void applyTheme(const Theme& theme);
void updateMaximizeIcon();
protected:
void mousePressEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
private:
QMenuBar* m_menuBar = nullptr;
QLabel* m_titleLabel = nullptr;
QToolButton* m_btnMin = nullptr;
QToolButton* m_btnMax = nullptr;
QToolButton* m_btnClose = nullptr;
Theme m_theme;
QToolButton* makeChromeButton(const QString& iconPath);
void toggleMaximize();
};
} // namespace rcx