From 0df52e82b8e79c0a1e71a14618b0d46c9b9d6cac Mon Sep 17 00:00:00 2001 From: Sen66 Date: Fri, 13 Feb 2026 19:09:11 +0100 Subject: [PATCH] Added custom title bar & border color when focused --- CMakeLists.txt | 2 + src/main.cpp | 90 +++++++++++++++++++-- src/mainwindow.h | 18 +++-- src/resources.qrc | 3 + src/themes/theme.cpp | 3 + src/themes/theme.h | 1 + src/themes/themeeditor.cpp | 1 + src/titlebar.cpp | 158 +++++++++++++++++++++++++++++++++++++ src/titlebar.h | 40 ++++++++++ 9 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 src/titlebar.cpp create mode 100644 src/titlebar.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8eacf2c..db0b937 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $<$:src/app.rc> diff --git a/src/main.cpp b/src/main.cpp index 1e3bc70..cc0c30a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,7 @@ #ifdef _WIN32 #include +#include #include #include #include @@ -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(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(m_borderOverlay)->color = color; + m_borderOverlay->update(); +} + } // namespace rcx // ── Entry point ── diff --git a/src/mainwindow.h b/src/mainwindow.h index 385212a..4c0ffd4 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,5 +1,6 @@ #pragma once #include "controller.h" +#include "titlebar.h" #include "pluginmanager.h" #include #include @@ -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 diff --git a/src/resources.qrc b/src/resources.qrc index 550feb7..0c64cc9 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -20,6 +20,9 @@ vsicons/arrow-right.svg vsicons/split-horizontal.svg vsicons/chrome-close.svg + vsicons/chrome-minimize.svg + vsicons/chrome-maximize.svg + vsicons/chrome-restore.svg vsicons/text-size.svg vsicons/add.svg vsicons/remove.svg diff --git a/src/themes/theme.cpp b/src/themes/theme.cpp index 3704a14..634176d 100644 --- a/src/themes/theme.cpp +++ b/src/themes/theme.cpp @@ -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"); diff --git a/src/themes/theme.h b/src/themes/theme.h index 6eed657..7555bce 100644 --- a/src/themes/theme.h +++ b/src/themes/theme.h @@ -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 ── diff --git a/src/themes/themeeditor.cpp b/src/themes/themeeditor.cpp index f4e646f..4a2db83 100644 --- a/src/themes/themeeditor.cpp +++ b/src/themes/themeeditor.cpp @@ -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", { diff --git a/src/titlebar.cpp b/src/titlebar.cpp new file mode 100644 index 0000000..b1a2f4c --- /dev/null +++ b/src/titlebar.cpp @@ -0,0 +1,158 @@ +#include "titlebar.h" +#include +#include +#include +#include + +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 diff --git a/src/titlebar.h b/src/titlebar.h new file mode 100644 index 0000000..99ce817 --- /dev/null +++ b/src/titlebar.h @@ -0,0 +1,40 @@ +#pragma once +#include "themes/theme.h" +#include +#include +#include +#include +#include + +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