mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Added custom title bar & border color when focused
This commit is contained in:
90
src/main.cpp
90
src/main.cpp
@@ -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 ──
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
@@ -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
158
src/titlebar.cpp
Normal 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
40
src/titlebar.h
Normal 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
|
||||
Reference in New Issue
Block a user