mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
5 Commits
v2027.02.1
...
v2027.02.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9377c3afd | ||
|
|
a86912add1 | ||
|
|
5a9a6b754f | ||
|
|
0df52e82b8 | ||
|
|
9a342286ee |
@@ -60,6 +60,8 @@ add_executable(Reclass
|
|||||||
src/themes/themeeditor.h
|
src/themes/themeeditor.h
|
||||||
src/themes/themeeditor.cpp
|
src/themes/themeeditor.cpp
|
||||||
src/mainwindow.h
|
src/mainwindow.h
|
||||||
|
src/titlebar.h
|
||||||
|
src/titlebar.cpp
|
||||||
src/mcp/mcp_bridge.h
|
src/mcp/mcp_bridge.h
|
||||||
src/mcp/mcp_bridge.cpp
|
src/mcp/mcp_bridge.cpp
|
||||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||||
@@ -83,6 +85,14 @@ endif()
|
|||||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||||
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
|
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
|
||||||
|
|
||||||
|
# Copy built-in theme JSON files to build directory
|
||||||
|
file(GLOB _theme_files "${CMAKE_SOURCE_DIR}/src/themes/defaults/*.json")
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||||
|
foreach(_tf ${_theme_files})
|
||||||
|
get_filename_component(_name ${_tf} NAME)
|
||||||
|
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
include(deploy)
|
include(deploy)
|
||||||
|
|
||||||
add_custom_target(screenshot ALL
|
add_custom_target(screenshot ALL
|
||||||
|
|||||||
@@ -345,12 +345,36 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
});
|
});
|
||||||
|
// Use the referenced struct's scope widths (children come from there)
|
||||||
|
uint64_t refScopeId = node.refId;
|
||||||
for (int childIdx : refChildren) {
|
for (int childIdx : refChildren) {
|
||||||
// Skip self-referential children (e.g. struct Ball has a field of type Ball)
|
const Node& child = tree.nodes[childIdx];
|
||||||
if (state.visiting.contains(tree.nodes[childIdx].id))
|
// Self-referential child → show as collapsed struct (non-expandable)
|
||||||
|
if (state.visiting.contains(child.id)) {
|
||||||
|
int typeW = state.effectiveTypeW(refScopeId);
|
||||||
|
int nameW = state.effectiveNameW(refScopeId);
|
||||||
|
LineMeta lm;
|
||||||
|
lm.nodeIdx = nodeIdx; // parent struct — materialize target
|
||||||
|
lm.nodeId = child.id;
|
||||||
|
lm.depth = childDepth;
|
||||||
|
lm.lineKind = LineKind::Header;
|
||||||
|
lm.offsetText = fmt::fmtOffsetMargin(
|
||||||
|
tree.baseAddress + absAddr + child.offset, false,
|
||||||
|
state.offsetHexDigits);
|
||||||
|
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
|
||||||
|
lm.nodeKind = child.kind;
|
||||||
|
lm.foldHead = true;
|
||||||
|
lm.foldCollapsed = true;
|
||||||
|
lm.foldLevel = computeFoldLevel(childDepth, true);
|
||||||
|
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
|
||||||
|
lm.effectiveTypeW = typeW;
|
||||||
|
lm.effectiveNameW = nameW;
|
||||||
|
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||||
|
/*collapsed=*/true, typeW, nameW), lm);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
composeNode(state, tree, prov, childIdx, childDepth,
|
composeNode(state, tree, prov, childIdx, childDepth,
|
||||||
absAddr, node.refId, false, node.id);
|
absAddr, node.refId, false, refScopeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -792,6 +792,44 @@ void RcxController::toggleCollapse(int nodeIdx) {
|
|||||||
cmd::Collapse{node.id, node.collapsed, !node.collapsed}));
|
cmd::Collapse{node.id, node.collapsed, !node.collapsed}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::materializeRefChildren(int nodeIdx) {
|
||||||
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
|
auto& tree = m_doc->tree;
|
||||||
|
|
||||||
|
// Snapshot values before addNode invalidates references
|
||||||
|
const uint64_t parentId = tree.nodes[nodeIdx].id;
|
||||||
|
const uint64_t refId = tree.nodes[nodeIdx].refId;
|
||||||
|
const NodeKind parentKind = tree.nodes[nodeIdx].kind;
|
||||||
|
const QString parentName = tree.nodes[nodeIdx].name;
|
||||||
|
|
||||||
|
if (refId == 0) return;
|
||||||
|
if (!tree.childrenOf(parentId).isEmpty()) return; // already materialized
|
||||||
|
|
||||||
|
// Clone all children of the referenced struct as real children of this struct
|
||||||
|
QVector<int> refChildren = tree.childrenOf(refId);
|
||||||
|
for (int ci : refChildren) {
|
||||||
|
Node copy = tree.nodes[ci];
|
||||||
|
copy.id = 0; // auto-assign new ID
|
||||||
|
copy.parentId = parentId;
|
||||||
|
copy.collapsed = true; // start collapsed
|
||||||
|
tree.addNode(copy);
|
||||||
|
}
|
||||||
|
tree.invalidateIdCache();
|
||||||
|
|
||||||
|
// Auto-expand the self-referential child (the one that was the cycle)
|
||||||
|
// so the user gets expand in a single click
|
||||||
|
QVector<int> newChildren = tree.childrenOf(parentId);
|
||||||
|
for (int ci : newChildren) {
|
||||||
|
auto& c = tree.nodes[ci];
|
||||||
|
if (c.kind == parentKind && c.name == parentName && c.refId == refId) {
|
||||||
|
c.collapsed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||||
auto& tree = m_doc->tree;
|
auto& tree = m_doc->tree;
|
||||||
|
|
||||||
@@ -1965,6 +2003,9 @@ void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
|||||||
if (!lm) return;
|
if (!lm) return;
|
||||||
|
|
||||||
if (lm->foldHead && (margin == 0 || margin == 1)) {
|
if (lm->foldHead && (margin == 0 || margin == 1)) {
|
||||||
|
if (lm->markerMask & (1u << M_CYCLE))
|
||||||
|
materializeRefChildren(lm->nodeIdx);
|
||||||
|
else
|
||||||
toggleCollapse(lm->nodeIdx);
|
toggleCollapse(lm->nodeIdx);
|
||||||
} else if (margin == 0 || margin == 1) {
|
} else if (margin == 0 || margin == 1) {
|
||||||
emit nodeSelected(lm->nodeIdx);
|
emit nodeSelected(lm->nodeIdx);
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ public:
|
|||||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||||
void removeNode(int nodeIdx);
|
void removeNode(int nodeIdx);
|
||||||
void toggleCollapse(int nodeIdx);
|
void toggleCollapse(int nodeIdx);
|
||||||
|
void materializeRefChildren(int nodeIdx);
|
||||||
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
|
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
|
||||||
void duplicateNode(int nodeIdx);
|
void duplicateNode(int nodeIdx);
|
||||||
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
||||||
|
|||||||
Binary file not shown.
210
src/main.cpp
210
src/main.cpp
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
#include <dwmapi.h>
|
#include <dwmapi.h>
|
||||||
#include <dbghelp.h>
|
#include <dbghelp.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -230,6 +231,21 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
|||||||
qApp->setStyleSheet(QString());
|
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 {
|
namespace rcx {
|
||||||
|
|
||||||
// MainWindow class declaration is in mainwindow.h
|
// MainWindow class declaration is in mainwindow.h
|
||||||
@@ -238,6 +254,32 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
setWindowTitle("Reclass");
|
setWindowTitle("Reclass");
|
||||||
resize(1200, 800);
|
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 = new QMdiArea(this);
|
||||||
m_mdiArea->setViewMode(QMdiArea::TabbedView);
|
m_mdiArea->setViewMode(QMdiArea::TabbedView);
|
||||||
m_mdiArea->setTabsClosable(true);
|
m_mdiArea->setTabsClosable(true);
|
||||||
@@ -246,21 +288,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||||
"QTabBar::tab {"
|
"QTabBar::tab {"
|
||||||
" background: %1; color: %2; padding: 6px 16px; border: none;"
|
" background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;"
|
||||||
"}"
|
"}"
|
||||||
"QTabBar::tab:selected { color: %3; background: %4; }"
|
"QTabBar::tab:selected { color: %3; background: %4; }"
|
||||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||||
.arg(t.background.name(), t.textMuted.name(), t.text.name(),
|
.arg(t.background.name(), t.textMuted.name(), t.text.name(),
|
||||||
t.backgroundAlt.name(), t.hover.name()));
|
t.backgroundAlt.name(), t.hover.name()));
|
||||||
}
|
}
|
||||||
{
|
|
||||||
QSettings settings("Reclass", "Reclass");
|
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
|
||||||
QFont f(fontName, 12);
|
|
||||||
f.setFixedPitch(true);
|
|
||||||
if (auto* tb = m_mdiArea->findChild<QTabBar*>())
|
|
||||||
tb->setFont(f);
|
|
||||||
}
|
|
||||||
setCentralWidget(m_mdiArea);
|
setCentralWidget(m_mdiArea);
|
||||||
|
|
||||||
createWorkspaceDock();
|
createWorkspaceDock();
|
||||||
@@ -306,7 +340,7 @@ QIcon MainWindow::makeIcon(const QString& svgPath) {
|
|||||||
|
|
||||||
void MainWindow::createMenus() {
|
void MainWindow::createMenus() {
|
||||||
// File
|
// File
|
||||||
auto* file = menuBar()->addMenu("&File");
|
auto* file = m_titleBar->menuBar()->addMenu("&File");
|
||||||
file->addAction("&New", this, &MainWindow::newDocument, QKeySequence::New);
|
file->addAction("&New", this, &MainWindow::newDocument, QKeySequence::New);
|
||||||
file->addAction("New &Tab", this, &MainWindow::newFile, QKeySequence(Qt::CTRL | Qt::Key_T));
|
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);
|
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", this, &MainWindow::openFile, QKeySequence::Open);
|
||||||
@@ -321,14 +355,14 @@ void MainWindow::createMenus() {
|
|||||||
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close));
|
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close));
|
||||||
|
|
||||||
// Edit
|
// 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-left.svg"), "&Undo", this, &MainWindow::undo, QKeySequence::Undo);
|
||||||
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", this, &MainWindow::redo, QKeySequence::Redo);
|
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", this, &MainWindow::redo, QKeySequence::Redo);
|
||||||
edit->addSeparator();
|
edit->addSeparator();
|
||||||
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
||||||
|
|
||||||
// View
|
// 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/split-horizontal.svg"), "Split &Horizontal", this, &MainWindow::splitView);
|
||||||
view->addAction(makeIcon(":/vsicons/chrome-close.svg"), "&Unsplit", this, &MainWindow::unsplitView);
|
view->addAction(makeIcon(":/vsicons/chrome-close.svg"), "&Unsplit", this, &MainWindow::unsplitView);
|
||||||
view->addSeparator();
|
view->addSeparator();
|
||||||
@@ -368,10 +402,19 @@ void MainWindow::createMenus() {
|
|||||||
themeMenu->addAction("Edit Theme...", this, &MainWindow::editTheme);
|
themeMenu->addAction("Edit Theme...", this, &MainWindow::editTheme);
|
||||||
|
|
||||||
view->addSeparator();
|
view->addSeparator();
|
||||||
|
auto* actShowIcon = view->addAction("Show &Icon");
|
||||||
|
actShowIcon->setCheckable(true);
|
||||||
|
actShowIcon->setChecked(settings.value("showIcon", false).toBool());
|
||||||
|
if (actShowIcon->isChecked()) m_titleBar->setShowIcon(true);
|
||||||
|
connect(actShowIcon, &QAction::toggled, this, [this](bool checked) {
|
||||||
|
m_titleBar->setShowIcon(checked);
|
||||||
|
QSettings s("Reclass", "Reclass");
|
||||||
|
s.setValue("showIcon", checked);
|
||||||
|
});
|
||||||
view->addAction(m_workspaceDock->toggleViewAction());
|
view->addAction(m_workspaceDock->toggleViewAction());
|
||||||
|
|
||||||
// Node
|
// 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/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/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));
|
node->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "Change &Type", this, &MainWindow::changeNodeType, QKeySequence(Qt::Key_T));
|
||||||
@@ -379,17 +422,18 @@ void MainWindow::createMenus() {
|
|||||||
node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
auto* plugins = menuBar()->addMenu("&Plugins");
|
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
||||||
plugins->addAction("&Manage Plugins...", this, &MainWindow::showPluginsDialog);
|
plugins->addAction("&Manage Plugins...", this, &MainWindow::showPluginsDialog);
|
||||||
|
|
||||||
// Help
|
// Help
|
||||||
auto* help = menuBar()->addMenu("&Help");
|
auto* help = m_titleBar->menuBar()->addMenu("&Help");
|
||||||
help->addAction(makeIcon(":/vsicons/question.svg"), "&About Reclass", this, &MainWindow::about);
|
help->addAction(makeIcon(":/vsicons/question.svg"), "&About Reclass", this, &MainWindow::about);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::createStatusBar() {
|
void MainWindow::createStatusBar() {
|
||||||
m_statusLabel = new QLabel("Ready");
|
m_statusLabel = new QLabel("Ready");
|
||||||
m_statusLabel->setContentsMargins(10, 0, 0, 0);
|
m_statusLabel->setContentsMargins(10, 0, 0, 0);
|
||||||
|
statusBar()->setContentsMargins(0, 4, 0, 4);
|
||||||
statusBar()->addWidget(m_statusLabel, 1);
|
statusBar()->addWidget(m_statusLabel, 1);
|
||||||
{
|
{
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
@@ -399,20 +443,9 @@ void MainWindow::createStatusBar() {
|
|||||||
statusBar()->setPalette(sbPal);
|
statusBar()->setPalette(sbPal);
|
||||||
statusBar()->setAutoFillBackground(true);
|
statusBar()->setAutoFillBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSettings settings("Reclass", "Reclass");
|
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
|
||||||
QFont f(fontName, 12);
|
|
||||||
f.setFixedPitch(true);
|
|
||||||
statusBar()->setFont(f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
|
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
|
||||||
QSettings settings("Reclass", "Reclass");
|
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
|
||||||
QFont tabFont(fontName, 12);
|
|
||||||
tabFont.setFixedPitch(true);
|
|
||||||
tw->tabBar()->setFont(tabFont);
|
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
tw->setStyleSheet(QStringLiteral(
|
tw->setStyleSheet(QStringLiteral(
|
||||||
"QTabWidget::pane { border: none; }"
|
"QTabWidget::pane { border: none; }"
|
||||||
@@ -426,6 +459,37 @@ void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
|
|||||||
tw->tabBar()->setExpanding(false);
|
tw->tabBar()->setExpanding(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::styleTabCloseButtons() {
|
||||||
|
auto* tabBar = m_mdiArea->findChild<QTabBar*>();
|
||||||
|
if (!tabBar) return;
|
||||||
|
|
||||||
|
const auto& t = ThemeManager::instance().current();
|
||||||
|
QString style = QStringLiteral(
|
||||||
|
"QToolButton { color: %1; border: none; padding: 0px 4px; font-size: 12px; }"
|
||||||
|
"QToolButton:hover { color: %2; }")
|
||||||
|
.arg(t.textDim.name(), t.indHoverSpan.name());
|
||||||
|
|
||||||
|
auto subs = m_mdiArea->subWindowList();
|
||||||
|
for (int i = 0; i < tabBar->count() && i < subs.size(); i++) {
|
||||||
|
auto* existing = qobject_cast<QToolButton*>(
|
||||||
|
tabBar->tabButton(i, QTabBar::RightSide));
|
||||||
|
if (existing && existing->text() == QStringLiteral("\u2715")) {
|
||||||
|
// Already our button, just restyle
|
||||||
|
existing->setStyleSheet(style);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Replace with ✕ text button
|
||||||
|
auto* btn = new QToolButton(tabBar);
|
||||||
|
btn->setText(QStringLiteral("\u2715"));
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
btn->setStyleSheet(style);
|
||||||
|
QMdiSubWindow* sub = subs[i];
|
||||||
|
connect(btn, &QToolButton::clicked, sub, &QMdiSubWindow::close);
|
||||||
|
tabBar->setTabButton(i, QTabBar::RightSide, btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||||
SplitPane pane;
|
SplitPane pane;
|
||||||
|
|
||||||
@@ -614,6 +678,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
|
|
||||||
ctrl->refresh();
|
ctrl->refresh();
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
|
styleTabCloseButtons();
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,7 +785,41 @@ void MainWindow::newDocument() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::selfTest() {
|
void MainWindow::selfTest() {
|
||||||
|
// Tab 1: Ball demo
|
||||||
project_new();
|
project_new();
|
||||||
|
|
||||||
|
// Tab 2: Unnamed struct with hex64 fields
|
||||||
|
{
|
||||||
|
auto* doc = new RcxDocument(this);
|
||||||
|
QByteArray data(256, '\0');
|
||||||
|
doc->loadData(data);
|
||||||
|
doc->tree.baseAddress = 0x00400000;
|
||||||
|
|
||||||
|
Node s;
|
||||||
|
s.kind = NodeKind::Struct;
|
||||||
|
s.name = "instance";
|
||||||
|
s.structTypeName = "Unnamed";
|
||||||
|
s.parentId = 0;
|
||||||
|
s.offset = 0;
|
||||||
|
int si = doc->tree.addNode(s);
|
||||||
|
uint64_t sId = doc->tree.nodes[si].id;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Hex64;
|
||||||
|
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||||
|
n.parentId = sId;
|
||||||
|
n.offset = i * 8;
|
||||||
|
doc->tree.addNode(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTab(doc);
|
||||||
|
rebuildWorkspaceModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus Ball tab
|
||||||
|
if (auto* first = m_mdiArea->subWindowList().value(0))
|
||||||
|
m_mdiArea->setActiveSubWindow(first);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::openFile() {
|
void MainWindow::openFile() {
|
||||||
@@ -879,16 +978,25 @@ void MainWindow::toggleMcp() {
|
|||||||
void MainWindow::applyTheme(const Theme& theme) {
|
void MainWindow::applyTheme(const Theme& theme) {
|
||||||
applyGlobalTheme(theme);
|
applyGlobalTheme(theme);
|
||||||
|
|
||||||
|
// Custom title bar
|
||||||
|
m_titleBar->applyTheme(theme);
|
||||||
|
|
||||||
|
// Update border overlay color
|
||||||
|
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
||||||
|
|
||||||
// MDI area tabs
|
// MDI area tabs
|
||||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||||
"QTabBar::tab {"
|
"QTabBar::tab {"
|
||||||
" background: %1; color: %2; padding: 6px 16px; border: none;"
|
" background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;"
|
||||||
"}"
|
"}"
|
||||||
"QTabBar::tab:selected { color: %3; background: %4; }"
|
"QTabBar::tab:selected { color: %3; background: %4; }"
|
||||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||||
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
||||||
theme.backgroundAlt.name(), theme.hover.name()));
|
theme.backgroundAlt.name(), theme.hover.name()));
|
||||||
|
|
||||||
|
// Re-style ✕ close buttons on MDI tabs
|
||||||
|
styleTabCloseButtons();
|
||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
{
|
{
|
||||||
QPalette sbPal = statusBar()->palette();
|
QPalette sbPal = statusBar()->palette();
|
||||||
@@ -910,16 +1018,7 @@ void MainWindow::editTheme() {
|
|||||||
int idx = tm.currentIndex();
|
int idx = tm.currentIndex();
|
||||||
ThemeEditor dlg(idx, this);
|
ThemeEditor dlg(idx, this);
|
||||||
if (dlg.exec() == QDialog::Accepted) {
|
if (dlg.exec() == QDialog::Accepted) {
|
||||||
tm.revertPreview();
|
tm.updateTheme(dlg.selectedIndex(), dlg.result());
|
||||||
int selectedIdx = dlg.selectedIndex();
|
|
||||||
Theme edited = dlg.result();
|
|
||||||
// Switch to selected theme first (if changed)
|
|
||||||
if (selectedIdx != idx && selectedIdx >= 0 && selectedIdx < tm.themes().size())
|
|
||||||
tm.setCurrent(selectedIdx);
|
|
||||||
// Apply edits
|
|
||||||
int applyIdx = selectedIdx >= 0 ? selectedIdx : idx;
|
|
||||||
if (applyIdx >= 0 && applyIdx < tm.themes().size())
|
|
||||||
tm.updateTheme(applyIdx, edited);
|
|
||||||
} else {
|
} else {
|
||||||
tm.revertPreview();
|
tm.revertPreview();
|
||||||
}
|
}
|
||||||
@@ -943,9 +1042,6 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
|||||||
}
|
}
|
||||||
pane.rendered->setMarginsFont(f);
|
pane.rendered->setMarginsFont(f);
|
||||||
}
|
}
|
||||||
// Update per-pane tab bar font
|
|
||||||
if (pane.tabWidget)
|
|
||||||
applyTabWidgetStyle(pane.tabWidget);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sync workspace tree font
|
// Sync workspace tree font
|
||||||
@@ -953,11 +1049,6 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
|||||||
m_workspaceTree->setFont(f);
|
m_workspaceTree->setFont(f);
|
||||||
// Sync status bar font
|
// Sync status bar font
|
||||||
statusBar()->setFont(f);
|
statusBar()->setFont(f);
|
||||||
// Sync MDI tab bar font
|
|
||||||
if (auto* tb = m_mdiArea->findChild<QTabBar*>())
|
|
||||||
tb->setFont(f);
|
|
||||||
// Sync menu bar / menu font via global stylesheet
|
|
||||||
applyGlobalTheme(ThemeManager::instance().current());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RcxController* MainWindow::activeController() const {
|
RcxController* MainWindow::activeController() const {
|
||||||
@@ -984,6 +1075,7 @@ MainWindow::TabState* MainWindow::tabByIndex(int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::updateWindowTitle() {
|
void MainWindow::updateWindowTitle() {
|
||||||
|
QString title;
|
||||||
auto* sub = m_mdiArea->activeSubWindow();
|
auto* sub = m_mdiArea->activeSubWindow();
|
||||||
if (sub && m_tabs.contains(sub)) {
|
if (sub && m_tabs.contains(sub)) {
|
||||||
auto& tab = m_tabs[sub];
|
auto& tab = m_tabs[sub];
|
||||||
@@ -991,10 +1083,11 @@ void MainWindow::updateWindowTitle() {
|
|||||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||||
: QFileInfo(tab.doc->filePath).fileName();
|
: QFileInfo(tab.doc->filePath).fileName();
|
||||||
if (tab.doc->modified) name += " *";
|
if (tab.doc->modified) name += " *";
|
||||||
setWindowTitle(name + " - Reclass");
|
title = name + " - Reclass";
|
||||||
} else {
|
} else {
|
||||||
setWindowTitle("Reclass");
|
title = "Reclass";
|
||||||
}
|
}
|
||||||
|
setWindowTitle(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rendered view setup ──
|
// ── Rendered view setup ──
|
||||||
@@ -1474,6 +1567,29 @@ void MainWindow::showPluginsDialog() {
|
|||||||
dialog.exec();
|
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
|
} // namespace rcx
|
||||||
|
|
||||||
// ── Entry point ──
|
// ── Entry point ──
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "titlebar.h"
|
||||||
#include "pluginmanager.h"
|
#include "pluginmanager.h"
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMdiArea>
|
#include <QMdiArea>
|
||||||
@@ -61,6 +62,8 @@ private:
|
|||||||
|
|
||||||
QMdiArea* m_mdiArea;
|
QMdiArea* m_mdiArea;
|
||||||
QLabel* m_statusLabel;
|
QLabel* m_statusLabel;
|
||||||
|
TitleBarWidget* m_titleBar = nullptr;
|
||||||
|
QWidget* m_borderOverlay = nullptr;
|
||||||
PluginManager m_pluginManager;
|
PluginManager m_pluginManager;
|
||||||
McpBridge* m_mcp = nullptr;
|
McpBridge* m_mcp = nullptr;
|
||||||
QAction* m_mcpAction = nullptr;
|
QAction* m_mcpAction = nullptr;
|
||||||
@@ -104,6 +107,7 @@ private:
|
|||||||
SplitPane createSplitPane(TabState& tab);
|
SplitPane createSplitPane(TabState& tab);
|
||||||
void applyTheme(const Theme& theme);
|
void applyTheme(const Theme& theme);
|
||||||
void applyTabWidgetStyle(QTabWidget* tw);
|
void applyTabWidgetStyle(QTabWidget* tw);
|
||||||
|
void styleTabCloseButtons();
|
||||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||||
SplitPane* findActiveSplitPane();
|
SplitPane* findActiveSplitPane();
|
||||||
RcxEditor* activePaneEditor();
|
RcxEditor* activePaneEditor();
|
||||||
@@ -114,6 +118,11 @@ private:
|
|||||||
QStandardItemModel* m_workspaceModel = nullptr;
|
QStandardItemModel* m_workspaceModel = nullptr;
|
||||||
void createWorkspaceDock();
|
void createWorkspaceDock();
|
||||||
void rebuildWorkspaceModel();
|
void rebuildWorkspaceModel();
|
||||||
|
void updateBorderColor(const QColor& color);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void changeEvent(QEvent* event) override;
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<file alias="chevron-right.png">icons/chevron-right.png</file>
|
<file alias="chevron-right.png">icons/chevron-right.png</file>
|
||||||
<file alias="chevron-down.png">icons/chevron-down.png</file>
|
<file alias="chevron-down.png">icons/chevron-down.png</file>
|
||||||
<file alias="class.png">icons/class.png</file>
|
<file alias="class.png">icons/class.png</file>
|
||||||
|
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/fonts">
|
<qresource prefix="/fonts">
|
||||||
<file alias="JetBrainsMono.ttf">fonts/JetBrainsMono.ttf</file>
|
<file alias="JetBrainsMono.ttf">fonts/JetBrainsMono.ttf</file>
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
<file alias="arrow-right.svg">vsicons/arrow-right.svg</file>
|
||||||
<file alias="split-horizontal.svg">vsicons/split-horizontal.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-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="text-size.svg">vsicons/text-size.svg</file>
|
||||||
<file alias="add.svg">vsicons/add.svg</file>
|
<file alias="add.svg">vsicons/add.svg</file>
|
||||||
<file alias="remove.svg">vsicons/remove.svg</file>
|
<file alias="remove.svg">vsicons/remove.svg</file>
|
||||||
|
|||||||
29
src/themes/defaults/reclass_dark.json
Normal file
29
src/themes/defaults/reclass_dark.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "Reclass Dark",
|
||||||
|
"background": "#1e1e1e",
|
||||||
|
"backgroundAlt": "#252526",
|
||||||
|
"surface": "#2a2d2e",
|
||||||
|
"border": "#3c3c3c",
|
||||||
|
"borderFocused": "#888888",
|
||||||
|
"button": "#333333",
|
||||||
|
"text": "#d4d4d4",
|
||||||
|
"textDim": "#858585",
|
||||||
|
"textMuted": "#585858",
|
||||||
|
"textFaint": "#505050",
|
||||||
|
"hover": "#2b2b2b",
|
||||||
|
"selected": "#232323",
|
||||||
|
"selection": "#2b2b2b",
|
||||||
|
"syntaxKeyword": "#569cd6",
|
||||||
|
"syntaxNumber": "#b5cea8",
|
||||||
|
"syntaxString": "#ce9178",
|
||||||
|
"syntaxComment": "#6a9955",
|
||||||
|
"syntaxPreproc": "#c586c0",
|
||||||
|
"syntaxType": "#4EC9B0",
|
||||||
|
"indHoverSpan": "#E6B450",
|
||||||
|
"indCmdPill": "#2a2a2a",
|
||||||
|
"indDataChanged": "#8fbc7a",
|
||||||
|
"indHintGreen": "#5a8248",
|
||||||
|
"markerPtr": "#f44747",
|
||||||
|
"markerCycle": "#e5a00d",
|
||||||
|
"markerError": "#7a2e2e"
|
||||||
|
}
|
||||||
29
src/themes/defaults/vs.json
Normal file
29
src/themes/defaults/vs.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "VS2022 Dark",
|
||||||
|
"background": "#1e1e1e",
|
||||||
|
"backgroundAlt": "#2d2d30",
|
||||||
|
"surface": "#333337",
|
||||||
|
"border": "#3f3f46",
|
||||||
|
"borderFocused": "#b180d7",
|
||||||
|
"button": "#3f3f46",
|
||||||
|
"text": "#dcdcdc",
|
||||||
|
"textDim": "#858585",
|
||||||
|
"textMuted": "#636369",
|
||||||
|
"textFaint": "#4d4d55",
|
||||||
|
"hover": "#3e3e42",
|
||||||
|
"selected": "#2d2d30",
|
||||||
|
"selection": "#264f78",
|
||||||
|
"syntaxKeyword": "#569cd6",
|
||||||
|
"syntaxNumber": "#b5cea8",
|
||||||
|
"syntaxString": "#d69d85",
|
||||||
|
"syntaxComment": "#57a64a",
|
||||||
|
"syntaxPreproc": "#9b9b9b",
|
||||||
|
"syntaxType": "#4ec9b0",
|
||||||
|
"indHoverSpan": "#b180d7",
|
||||||
|
"indCmdPill": "#2d2d30",
|
||||||
|
"indDataChanged": "#8fbc7a",
|
||||||
|
"indHintGreen": "#5a8248",
|
||||||
|
"markerPtr": "#f44747",
|
||||||
|
"markerCycle": "#e5a00d",
|
||||||
|
"markerError": "#7a2e2e"
|
||||||
|
}
|
||||||
29
src/themes/defaults/warm.json
Normal file
29
src/themes/defaults/warm.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "Warm",
|
||||||
|
"background": "#212121",
|
||||||
|
"backgroundAlt": "#2a2a2a",
|
||||||
|
"surface": "#2a2a2a",
|
||||||
|
"border": "#373737",
|
||||||
|
"borderFocused": "#888888",
|
||||||
|
"button": "#373737",
|
||||||
|
"text": "#AAA99F",
|
||||||
|
"textDim": "#7a7a6e",
|
||||||
|
"textMuted": "#555550",
|
||||||
|
"textFaint": "#464646",
|
||||||
|
"hover": "#373737",
|
||||||
|
"selected": "#2d2d2d",
|
||||||
|
"selection": "#21213A",
|
||||||
|
"syntaxKeyword": "#AA9565",
|
||||||
|
"syntaxNumber": "#AAA98C",
|
||||||
|
"syntaxString": "#6B3B21",
|
||||||
|
"syntaxComment": "#464646",
|
||||||
|
"syntaxPreproc": "#AA9565",
|
||||||
|
"syntaxType": "#6B959F",
|
||||||
|
"indHoverSpan": "#AA9565",
|
||||||
|
"indCmdPill": "#2a2a2a",
|
||||||
|
"indDataChanged": "#6B959F",
|
||||||
|
"indHintGreen": "#464646",
|
||||||
|
"markerPtr": "#6B3B21",
|
||||||
|
"markerCycle": "#AA9565",
|
||||||
|
"markerError": "#3C2121"
|
||||||
|
}
|
||||||
@@ -1,119 +1,56 @@
|
|||||||
#include "theme.h"
|
#include "theme.h"
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
// ── Field table for DRY serialization ──
|
// ── Shared field metadata (serialization + editor UI) ──
|
||||||
|
|
||||||
struct ColorField { const char* key; QColor Theme::*ptr; };
|
const ThemeFieldMeta kThemeFields[] = {
|
||||||
|
{"background", "Background", "Chrome", &Theme::background},
|
||||||
static const ColorField kFields[] = {
|
{"backgroundAlt", "Background Alt", "Chrome", &Theme::backgroundAlt},
|
||||||
{"background", &Theme::background},
|
{"surface", "Surface", "Chrome", &Theme::surface},
|
||||||
{"backgroundAlt", &Theme::backgroundAlt},
|
{"border", "Border", "Chrome", &Theme::border},
|
||||||
{"surface", &Theme::surface},
|
{"borderFocused", "Border Focused", "Chrome", &Theme::borderFocused},
|
||||||
{"border", &Theme::border},
|
{"button", "Button", "Chrome", &Theme::button},
|
||||||
{"button", &Theme::button},
|
{"text", "Text", "Text", &Theme::text},
|
||||||
{"text", &Theme::text},
|
{"textDim", "Text Dim", "Text", &Theme::textDim},
|
||||||
{"textDim", &Theme::textDim},
|
{"textMuted", "Text Muted", "Text", &Theme::textMuted},
|
||||||
{"textMuted", &Theme::textMuted},
|
{"textFaint", "Text Faint", "Text", &Theme::textFaint},
|
||||||
{"textFaint", &Theme::textFaint},
|
{"hover", "Hover", "Interactive", &Theme::hover},
|
||||||
{"hover", &Theme::hover},
|
{"selected", "Selected", "Interactive", &Theme::selected},
|
||||||
{"selected", &Theme::selected},
|
{"selection", "Selection", "Interactive", &Theme::selection},
|
||||||
{"selection", &Theme::selection},
|
{"syntaxKeyword", "Keyword", "Syntax", &Theme::syntaxKeyword},
|
||||||
{"syntaxKeyword", &Theme::syntaxKeyword},
|
{"syntaxNumber", "Number", "Syntax", &Theme::syntaxNumber},
|
||||||
{"syntaxNumber", &Theme::syntaxNumber},
|
{"syntaxString", "String", "Syntax", &Theme::syntaxString},
|
||||||
{"syntaxString", &Theme::syntaxString},
|
{"syntaxComment", "Comment", "Syntax", &Theme::syntaxComment},
|
||||||
{"syntaxComment", &Theme::syntaxComment},
|
{"syntaxPreproc", "Preprocessor", "Syntax", &Theme::syntaxPreproc},
|
||||||
{"syntaxPreproc", &Theme::syntaxPreproc},
|
{"syntaxType", "Type", "Syntax", &Theme::syntaxType},
|
||||||
{"syntaxType", &Theme::syntaxType},
|
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
||||||
{"indHoverSpan", &Theme::indHoverSpan},
|
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
||||||
{"indCmdPill", &Theme::indCmdPill},
|
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
||||||
{"indDataChanged",&Theme::indDataChanged},
|
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
||||||
{"indHintGreen", &Theme::indHintGreen},
|
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
||||||
{"markerPtr", &Theme::markerPtr},
|
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
||||||
{"markerCycle", &Theme::markerCycle},
|
{"markerError", "Error", "Markers", &Theme::markerError},
|
||||||
{"markerError", &Theme::markerError},
|
|
||||||
};
|
};
|
||||||
|
const int kThemeFieldCount = static_cast<int>(std::extent_v<decltype(kThemeFields)>);
|
||||||
|
|
||||||
QJsonObject Theme::toJson() const {
|
QJsonObject Theme::toJson() const {
|
||||||
QJsonObject o;
|
QJsonObject o;
|
||||||
o["name"] = name;
|
o["name"] = name;
|
||||||
for (const auto& f : kFields)
|
for (int i = 0; i < kThemeFieldCount; i++)
|
||||||
o[f.key] = (this->*f.ptr).name();
|
o[kThemeFields[i].key] = (this->*kThemeFields[i].ptr).name();
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
Theme Theme::fromJson(const QJsonObject& o) {
|
Theme Theme::fromJson(const QJsonObject& o) {
|
||||||
Theme t = reclassDark();
|
Theme t;
|
||||||
t.name = o["name"].toString(t.name);
|
t.name = o["name"].toString("Untitled");
|
||||||
for (const auto& f : kFields) {
|
for (int i = 0; i < kThemeFieldCount; i++) {
|
||||||
if (o.contains(f.key))
|
if (o.contains(kThemeFields[i].key))
|
||||||
t.*f.ptr = QColor(o[f.key].toString());
|
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Built-in themes ──
|
|
||||||
|
|
||||||
Theme Theme::reclassDark() {
|
|
||||||
Theme t;
|
|
||||||
t.name = "Reclass Dark";
|
|
||||||
t.background = QColor("#1e1e1e");
|
|
||||||
t.backgroundAlt = QColor("#252526");
|
|
||||||
t.surface = QColor("#2a2d2e");
|
|
||||||
t.border = QColor("#3c3c3c");
|
|
||||||
t.button = QColor("#333333");
|
|
||||||
t.text = QColor("#d4d4d4");
|
|
||||||
t.textDim = QColor("#858585");
|
|
||||||
t.textMuted = QColor("#585858");
|
|
||||||
t.textFaint = QColor("#505050");
|
|
||||||
t.hover = QColor("#2b2b2b");
|
|
||||||
t.selected = QColor("#232323");
|
|
||||||
t.selection = QColor("#2b2b2b");
|
|
||||||
t.syntaxKeyword = QColor("#569cd6");
|
|
||||||
t.syntaxNumber = QColor("#b5cea8");
|
|
||||||
t.syntaxString = QColor("#ce9178");
|
|
||||||
t.syntaxComment = QColor("#6a9955");
|
|
||||||
t.syntaxPreproc = QColor("#c586c0");
|
|
||||||
t.syntaxType = QColor("#4EC9B0");
|
|
||||||
t.indHoverSpan = QColor("#E6B450");
|
|
||||||
t.indCmdPill = QColor("#2a2a2a");
|
|
||||||
t.indDataChanged= QColor("#8fbc7a");
|
|
||||||
t.indHintGreen = QColor("#5a8248");
|
|
||||||
t.markerPtr = QColor("#f44747");
|
|
||||||
t.markerCycle = QColor("#e5a00d");
|
|
||||||
t.markerError = QColor("#7a2e2e");
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
Theme Theme::warm() {
|
|
||||||
Theme t;
|
|
||||||
t.name = "Warm";
|
|
||||||
t.background = QColor("#212121");
|
|
||||||
t.backgroundAlt = QColor("#2a2a2a");
|
|
||||||
t.surface = QColor("#2a2a2a");
|
|
||||||
t.border = QColor("#373737");
|
|
||||||
t.button = QColor("#373737");
|
|
||||||
t.text = QColor("#AAA99F");
|
|
||||||
t.textDim = QColor("#7a7a6e");
|
|
||||||
t.textMuted = QColor("#555550");
|
|
||||||
t.textFaint = QColor("#464646");
|
|
||||||
t.hover = QColor("#373737");
|
|
||||||
t.selected = QColor("#2d2d2d");
|
|
||||||
t.selection = QColor("#21213A");
|
|
||||||
t.syntaxKeyword = QColor("#AA9565");
|
|
||||||
t.syntaxNumber = QColor("#AAA98C");
|
|
||||||
t.syntaxString = QColor("#6B3B21");
|
|
||||||
t.syntaxComment = QColor("#464646");
|
|
||||||
t.syntaxPreproc = QColor("#AA9565");
|
|
||||||
t.syntaxType = QColor("#6B959F");
|
|
||||||
t.indHoverSpan = QColor("#AA9565");
|
|
||||||
t.indCmdPill = QColor("#2a2a2a");
|
|
||||||
t.indDataChanged= QColor("#6B959F");
|
|
||||||
t.indHintGreen = QColor("#464646");
|
|
||||||
t.markerPtr = QColor("#6B3B21");
|
|
||||||
t.markerCycle = QColor("#AA9565");
|
|
||||||
t.markerError = QColor("#3C2121");
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct Theme {
|
|||||||
QColor backgroundAlt; // panels, tab selected, tooltips
|
QColor backgroundAlt; // panels, tab selected, tooltips
|
||||||
QColor surface; // alternateBase
|
QColor surface; // alternateBase
|
||||||
QColor border; // separators, menu borders
|
QColor border; // separators, menu borders
|
||||||
|
QColor borderFocused; // window border when focused
|
||||||
QColor button; // button bg
|
QColor button; // button bg
|
||||||
|
|
||||||
// ── Text ──
|
// ── Text ──
|
||||||
@@ -47,9 +48,18 @@ struct Theme {
|
|||||||
|
|
||||||
QJsonObject toJson() const;
|
QJsonObject toJson() const;
|
||||||
static Theme fromJson(const QJsonObject& obj);
|
static Theme fromJson(const QJsonObject& obj);
|
||||||
|
|
||||||
static Theme reclassDark();
|
|
||||||
static Theme warm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Shared field metadata (serialization + editor UI) ──
|
||||||
|
|
||||||
|
struct ThemeFieldMeta {
|
||||||
|
const char* key; // JSON key
|
||||||
|
const char* label; // display label
|
||||||
|
const char* group; // section group name
|
||||||
|
QColor Theme::*ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const ThemeFieldMeta kThemeFields[];
|
||||||
|
extern const int kThemeFieldCount;
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
: QStringLiteral("File: %1").arg(path));
|
: QStringLiteral("File: %1").arg(path));
|
||||||
mainLayout->addWidget(m_fileInfoLabel);
|
mainLayout->addWidget(m_fileInfoLabel);
|
||||||
|
|
||||||
// ── Scrollable area for swatches + contrast ──
|
// ── Scrollable area for swatches ──
|
||||||
auto* scroll = new QScrollArea;
|
auto* scroll = new QScrollArea;
|
||||||
scroll->setWidgetResizable(true);
|
scroll->setWidgetResizable(true);
|
||||||
scroll->setFrameShape(QFrame::NoFrame);
|
scroll->setFrameShape(QFrame::NoFrame);
|
||||||
@@ -79,12 +80,17 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
scrollLayout->setContentsMargins(0, 0, 6, 0); // right margin for scrollbar
|
scrollLayout->setContentsMargins(0, 0, 6, 0); // right margin for scrollbar
|
||||||
scrollLayout->setSpacing(2);
|
scrollLayout->setSpacing(2);
|
||||||
|
|
||||||
// ── Color swatches ──
|
// ── Color swatches (driven by kThemeFields) ──
|
||||||
struct FieldDef { const char* label; QColor Theme::*ptr; };
|
const char* currentGroup = nullptr;
|
||||||
|
for (int fi = 0; fi < kThemeFieldCount; fi++) {
|
||||||
|
const auto& f = kThemeFields[fi];
|
||||||
|
|
||||||
|
// Section header on group change
|
||||||
|
if (!currentGroup || std::strcmp(currentGroup, f.group) != 0) {
|
||||||
|
scrollLayout->addWidget(makeSectionLabel(QString::fromLatin1(f.group)));
|
||||||
|
currentGroup = f.group;
|
||||||
|
}
|
||||||
|
|
||||||
auto addGroup = [&](const QString& title, std::initializer_list<FieldDef> fields) {
|
|
||||||
scrollLayout->addWidget(makeSectionLabel(title));
|
|
||||||
for (const auto& f : fields) {
|
|
||||||
int idx = m_swatches.size();
|
int idx = m_swatches.size();
|
||||||
|
|
||||||
auto* row = new QHBoxLayout;
|
auto* row = new QHBoxLayout;
|
||||||
@@ -117,45 +123,6 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
|
|
||||||
scrollLayout->addLayout(row);
|
scrollLayout->addLayout(row);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
addGroup("Chrome", {
|
|
||||||
{"Background", &Theme::background},
|
|
||||||
{"Background Alt", &Theme::backgroundAlt},
|
|
||||||
{"Surface", &Theme::surface},
|
|
||||||
{"Border", &Theme::border},
|
|
||||||
{"Button", &Theme::button},
|
|
||||||
});
|
|
||||||
addGroup("Text", {
|
|
||||||
{"Text", &Theme::text},
|
|
||||||
{"Text Dim", &Theme::textDim},
|
|
||||||
{"Text Muted", &Theme::textMuted},
|
|
||||||
{"Text Faint", &Theme::textFaint},
|
|
||||||
});
|
|
||||||
addGroup("Interactive", {
|
|
||||||
{"Hover", &Theme::hover},
|
|
||||||
{"Selected", &Theme::selected},
|
|
||||||
{"Selection", &Theme::selection},
|
|
||||||
});
|
|
||||||
addGroup("Syntax", {
|
|
||||||
{"Keyword", &Theme::syntaxKeyword},
|
|
||||||
{"Number", &Theme::syntaxNumber},
|
|
||||||
{"String", &Theme::syntaxString},
|
|
||||||
{"Comment", &Theme::syntaxComment},
|
|
||||||
{"Preprocessor", &Theme::syntaxPreproc},
|
|
||||||
{"Type", &Theme::syntaxType},
|
|
||||||
});
|
|
||||||
addGroup("Indicators", {
|
|
||||||
{"Hover Span", &Theme::indHoverSpan},
|
|
||||||
{"Cmd Pill", &Theme::indCmdPill},
|
|
||||||
{"Data Changed", &Theme::indDataChanged},
|
|
||||||
{"Hint Green", &Theme::indHintGreen},
|
|
||||||
});
|
|
||||||
addGroup("Markers", {
|
|
||||||
{"Pointer", &Theme::markerPtr},
|
|
||||||
{"Cycle", &Theme::markerCycle},
|
|
||||||
{"Error", &Theme::markerError},
|
|
||||||
});
|
|
||||||
|
|
||||||
scrollLayout->addStretch();
|
scrollLayout->addStretch();
|
||||||
scroll->setWidget(scrollWidget);
|
scroll->setWidget(scrollWidget);
|
||||||
@@ -163,28 +130,21 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
|||||||
|
|
||||||
// ── Bottom bar ──
|
// ── Bottom bar ──
|
||||||
auto* bottomRow = new QHBoxLayout;
|
auto* bottomRow = new QHBoxLayout;
|
||||||
m_previewBtn = new QPushButton(QStringLiteral("Live Preview"));
|
|
||||||
m_previewBtn->setCheckable(true);
|
|
||||||
connect(m_previewBtn, &QPushButton::toggled, this, [this](bool) { togglePreview(); });
|
|
||||||
bottomRow->addWidget(m_previewBtn);
|
|
||||||
|
|
||||||
bottomRow->addStretch();
|
bottomRow->addStretch();
|
||||||
|
|
||||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
|
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
|
||||||
if (m_previewing) {
|
|
||||||
ThemeManager::instance().revertPreview();
|
ThemeManager::instance().revertPreview();
|
||||||
m_previewing = false;
|
|
||||||
}
|
|
||||||
reject();
|
reject();
|
||||||
});
|
});
|
||||||
bottomRow->addWidget(buttons);
|
bottomRow->addWidget(buttons);
|
||||||
mainLayout->addLayout(bottomRow);
|
mainLayout->addLayout(bottomRow);
|
||||||
|
|
||||||
// Initial update
|
// Initial swatch update + start live preview
|
||||||
for (int i = 0; i < m_swatches.size(); i++)
|
for (int i = 0; i < m_swatches.size(); i++)
|
||||||
updateSwatch(i);
|
updateSwatch(i);
|
||||||
|
tm.previewTheme(m_theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load a different theme into the editor ──
|
// ── Load a different theme into the editor ──
|
||||||
@@ -206,7 +166,6 @@ void ThemeEditor::loadTheme(int index) {
|
|||||||
for (int i = 0; i < m_swatches.size(); i++)
|
for (int i = 0; i < m_swatches.size(); i++)
|
||||||
updateSwatch(i);
|
updateSwatch(i);
|
||||||
|
|
||||||
if (m_previewing)
|
|
||||||
tm.previewTheme(m_theme);
|
tm.previewTheme(m_theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,19 +189,8 @@ void ThemeEditor::pickColor(int idx) {
|
|||||||
if (c.isValid()) {
|
if (c.isValid()) {
|
||||||
m_theme.*s.field = c;
|
m_theme.*s.field = c;
|
||||||
updateSwatch(idx);
|
updateSwatch(idx);
|
||||||
if (m_previewing)
|
|
||||||
ThemeManager::instance().previewTheme(m_theme);
|
ThemeManager::instance().previewTheme(m_theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Live preview toggle ──
|
|
||||||
|
|
||||||
void ThemeEditor::togglePreview() {
|
|
||||||
m_previewing = m_previewBtn->isChecked();
|
|
||||||
if (m_previewing)
|
|
||||||
ThemeManager::instance().previewTheme(m_theme);
|
|
||||||
else
|
|
||||||
ThemeManager::instance().revertPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -36,14 +36,10 @@ private:
|
|||||||
QComboBox* m_themeCombo = nullptr;
|
QComboBox* m_themeCombo = nullptr;
|
||||||
QLineEdit* m_nameEdit = nullptr;
|
QLineEdit* m_nameEdit = nullptr;
|
||||||
QLabel* m_fileInfoLabel = nullptr;
|
QLabel* m_fileInfoLabel = nullptr;
|
||||||
QPushButton* m_previewBtn = nullptr;
|
|
||||||
bool m_previewing = false;
|
|
||||||
|
|
||||||
void loadTheme(int index);
|
void loadTheme(int index);
|
||||||
void rebuildSwatches(QVBoxLayout* swatchLayout);
|
|
||||||
void updateSwatch(int idx);
|
void updateSwatch(int idx);
|
||||||
void pickColor(int idx);
|
void pickColor(int idx);
|
||||||
void togglePreview();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
@@ -13,18 +14,40 @@ ThemeManager& ThemeManager::instance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeManager::ThemeManager() {
|
ThemeManager::ThemeManager() {
|
||||||
m_builtIn.append(Theme::reclassDark());
|
loadBuiltInThemes();
|
||||||
m_builtIn.append(Theme::warm());
|
|
||||||
loadUserThemes();
|
loadUserThemes();
|
||||||
|
|
||||||
QSettings settings("Reclass", "Reclass");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString saved = settings.value("theme", m_builtIn[0].name).toString();
|
QString fallback = m_builtIn.isEmpty() ? QString() : m_builtIn[0].name;
|
||||||
|
QString saved = settings.value("theme", fallback).toString();
|
||||||
auto all = themes();
|
auto all = themes();
|
||||||
for (int i = 0; i < all.size(); i++) {
|
for (int i = 0; i < all.size(); i++) {
|
||||||
if (all[i].name == saved) { m_currentIdx = i; break; }
|
if (all[i].name == saved) { m_currentIdx = i; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Load built-in themes from JSON files next to the executable ──
|
||||||
|
|
||||||
|
QString ThemeManager::builtInDir() const {
|
||||||
|
return QCoreApplication::applicationDirPath() + "/themes";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeManager::loadBuiltInThemes() {
|
||||||
|
m_builtIn.clear();
|
||||||
|
QDir dir(builtInDir());
|
||||||
|
if (!dir.exists()) return;
|
||||||
|
for (const QString& name : dir.entryList({"*.json"}, QDir::Files, QDir::Name)) {
|
||||||
|
QFile f(dir.filePath(name));
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) continue;
|
||||||
|
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
if (jdoc.isObject())
|
||||||
|
m_builtIn.append(Theme::fromJson(jdoc.object()));
|
||||||
|
}
|
||||||
|
m_builtInDefaults = m_builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── themes / current ──
|
||||||
|
|
||||||
QVector<Theme> ThemeManager::themes() const {
|
QVector<Theme> ThemeManager::themes() const {
|
||||||
QVector<Theme> all = m_builtIn;
|
QVector<Theme> all = m_builtIn;
|
||||||
all.append(m_user);
|
all.append(m_user);
|
||||||
@@ -37,7 +60,10 @@ const Theme& ThemeManager::current() const {
|
|||||||
int userIdx = m_currentIdx - m_builtIn.size();
|
int userIdx = m_currentIdx - m_builtIn.size();
|
||||||
if (userIdx >= 0 && userIdx < m_user.size())
|
if (userIdx >= 0 && userIdx < m_user.size())
|
||||||
return m_user[userIdx];
|
return m_user[userIdx];
|
||||||
|
if (!m_builtIn.isEmpty())
|
||||||
return m_builtIn[0];
|
return m_builtIn[0];
|
||||||
|
static const Theme empty;
|
||||||
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::setCurrent(int index) {
|
void ThemeManager::setCurrent(int index) {
|
||||||
@@ -55,16 +81,19 @@ void ThemeManager::addTheme(const Theme& theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::updateTheme(int index, const Theme& theme) {
|
void ThemeManager::updateTheme(int index, const Theme& theme) {
|
||||||
|
m_previewing = false; // commit any active preview
|
||||||
|
|
||||||
if (index < builtInCount()) {
|
if (index < builtInCount()) {
|
||||||
// Can't overwrite built-in; save as user theme instead
|
m_builtIn[index] = theme;
|
||||||
m_user.append(theme);
|
m_currentIdx = index;
|
||||||
} else {
|
} else {
|
||||||
int ui = index - builtInCount();
|
int ui = index - builtInCount();
|
||||||
if (ui >= 0 && ui < m_user.size())
|
if (ui >= 0 && ui < m_user.size())
|
||||||
m_user[ui] = theme;
|
m_user[ui] = theme;
|
||||||
}
|
}
|
||||||
saveUserThemes();
|
saveUserThemes();
|
||||||
if (index == m_currentIdx)
|
QSettings settings("Reclass", "Reclass");
|
||||||
|
settings.setValue("theme", current().name);
|
||||||
emit themeChanged(current());
|
emit themeChanged(current());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +111,9 @@ void ThemeManager::removeTheme(int index) {
|
|||||||
saveUserThemes();
|
saveUserThemes();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ThemeManager::themesDir() const {
|
// ── User theme persistence ──
|
||||||
|
|
||||||
|
QString ThemeManager::userDir() const {
|
||||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
||||||
+ "/themes";
|
+ "/themes";
|
||||||
QDir().mkpath(dir);
|
QDir().mkpath(dir);
|
||||||
@@ -91,37 +122,69 @@ QString ThemeManager::themesDir() const {
|
|||||||
|
|
||||||
void ThemeManager::loadUserThemes() {
|
void ThemeManager::loadUserThemes() {
|
||||||
m_user.clear();
|
m_user.clear();
|
||||||
QDir dir(themesDir());
|
QDir dir(userDir());
|
||||||
for (const QString& name : dir.entryList({"*.json"}, QDir::Files)) {
|
for (const QString& name : dir.entryList({"*.json"}, QDir::Files)) {
|
||||||
QFile f(dir.filePath(name));
|
QFile f(dir.filePath(name));
|
||||||
if (!f.open(QIODevice::ReadOnly)) continue;
|
if (!f.open(QIODevice::ReadOnly)) continue;
|
||||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||||
if (jdoc.isObject())
|
if (!jdoc.isObject()) continue;
|
||||||
m_user.append(Theme::fromJson(jdoc.object()));
|
Theme t = Theme::fromJson(jdoc.object());
|
||||||
|
|
||||||
|
// If this overrides a built-in (same name), replace it in-place
|
||||||
|
bool isOverride = false;
|
||||||
|
for (int i = 0; i < m_builtIn.size(); i++) {
|
||||||
|
if (m_builtIn[i].name == t.name) {
|
||||||
|
m_builtIn[i] = t;
|
||||||
|
isOverride = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isOverride)
|
||||||
|
m_user.append(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::saveUserThemes() const {
|
void ThemeManager::saveUserThemes() const {
|
||||||
QString dir = themesDir();
|
QString dir = userDir();
|
||||||
// Remove old files
|
|
||||||
QDir d(dir);
|
QDir d(dir);
|
||||||
for (const QString& name : d.entryList({"*.json"}, QDir::Files))
|
for (const QString& name : d.entryList({"*.json"}, QDir::Files))
|
||||||
d.remove(name);
|
d.remove(name);
|
||||||
// Write current user themes
|
|
||||||
|
// Save modified built-ins (compare against on-disk originals)
|
||||||
|
for (int i = 0; i < m_builtIn.size() && i < m_builtInDefaults.size(); i++) {
|
||||||
|
if (m_builtIn[i].toJson() != m_builtInDefaults[i].toJson()) {
|
||||||
|
QString filename = m_builtIn[i].name.toLower().replace(' ', '_') + ".json";
|
||||||
|
QFile f(dir + "/" + filename);
|
||||||
|
if (f.open(QIODevice::WriteOnly))
|
||||||
|
f.write(QJsonDocument(m_builtIn[i].toJson()).toJson(QJsonDocument::Indented));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user themes
|
||||||
for (int i = 0; i < m_user.size(); i++) {
|
for (int i = 0; i < m_user.size(); i++) {
|
||||||
QString filename = m_user[i].name.toLower().replace(' ', '_') + ".json";
|
QString filename = m_user[i].name.toLower().replace(' ', '_') + ".json";
|
||||||
QFile f(dir + "/" + filename);
|
QFile f(dir + "/" + filename);
|
||||||
if (!f.open(QIODevice::WriteOnly)) continue;
|
if (f.open(QIODevice::WriteOnly))
|
||||||
f.write(QJsonDocument(m_user[i].toJson()).toJson(QJsonDocument::Indented));
|
f.write(QJsonDocument(m_user[i].toJson()).toJson(QJsonDocument::Indented));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ThemeManager::themeFilePath(int index) const {
|
QString ThemeManager::themeFilePath(int index) const {
|
||||||
if (index < builtInCount()) return {};
|
if (index < builtInCount()) {
|
||||||
|
// Built-in has a user override file only if modified
|
||||||
|
if (index < m_builtInDefaults.size()
|
||||||
|
&& m_builtIn[index].toJson() != m_builtInDefaults[index].toJson()) {
|
||||||
|
QString filename = m_builtIn[index].name.toLower().replace(' ', '_') + ".json";
|
||||||
|
return userDir() + "/" + filename;
|
||||||
|
}
|
||||||
|
// Show the built-in source file
|
||||||
|
QString filename = m_builtIn[index].name.toLower().replace(' ', '_') + ".json";
|
||||||
|
return builtInDir() + "/" + filename;
|
||||||
|
}
|
||||||
int ui = index - builtInCount();
|
int ui = index - builtInCount();
|
||||||
if (ui < 0 || ui >= m_user.size()) return {};
|
if (ui < 0 || ui >= m_user.size()) return {};
|
||||||
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
|
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
|
||||||
return themesDir() + "/" + filename;
|
return userDir() + "/" + filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::previewTheme(const Theme& theme) {
|
void ThemeManager::previewTheme(const Theme& theme) {
|
||||||
|
|||||||
@@ -31,14 +31,17 @@ signals:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
ThemeManager();
|
ThemeManager();
|
||||||
QVector<Theme> m_builtIn;
|
QVector<Theme> m_builtIn; // built-in themes (possibly overridden)
|
||||||
|
QVector<Theme> m_builtInDefaults; // originals loaded from disk
|
||||||
QVector<Theme> m_user;
|
QVector<Theme> m_user;
|
||||||
int m_currentIdx = 0;
|
int m_currentIdx = 0;
|
||||||
|
|
||||||
int builtInCount() const { return m_builtIn.size(); }
|
int builtInCount() const { return m_builtIn.size(); }
|
||||||
QString themesDir() const;
|
void loadBuiltInThemes();
|
||||||
|
QString builtInDir() const;
|
||||||
|
QString userDir() const;
|
||||||
bool m_previewing = false;
|
bool m_previewing = false;
|
||||||
Theme m_savedTheme; // stashed current theme during preview
|
Theme m_savedTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
158
src/titlebar.cpp
Normal file
158
src/titlebar.cpp
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
#include "titlebar.h"
|
||||||
|
#include "themes/thememanager.h"
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
TitleBarWidget::TitleBarWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_theme(ThemeManager::instance().current())
|
||||||
|
{
|
||||||
|
setFixedHeight(32);
|
||||||
|
|
||||||
|
auto* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
|
||||||
|
// App name
|
||||||
|
m_appLabel = new QLabel(QStringLiteral("Reclass"), this);
|
||||||
|
m_appLabel->setContentsMargins(10, 0, 4, 0);
|
||||||
|
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
layout->addWidget(m_appLabel);
|
||||||
|
|
||||||
|
// Menu bar
|
||||||
|
m_menuBar = new QMenuBar(this);
|
||||||
|
m_menuBar->setNativeMenuBar(false);
|
||||||
|
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||||
|
layout->addWidget(m_menuBar);
|
||||||
|
|
||||||
|
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::applyTheme(const Theme& theme) {
|
||||||
|
m_theme = theme;
|
||||||
|
|
||||||
|
// Title bar background
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
QPalette pal = palette();
|
||||||
|
pal.setColor(QPalette::Window, theme.background);
|
||||||
|
setPalette(pal);
|
||||||
|
|
||||||
|
// App label
|
||||||
|
m_appLabel->setStyleSheet(
|
||||||
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
|
.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::setShowIcon(bool show) {
|
||||||
|
if (show) {
|
||||||
|
m_appLabel->setText(QString());
|
||||||
|
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(24, 24));
|
||||||
|
} else {
|
||||||
|
m_appLabel->setPixmap(QPixmap());
|
||||||
|
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||||
|
m_appLabel->setStyleSheet(
|
||||||
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
|
.arg(m_theme.textDim.name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 applyTheme(const Theme& theme);
|
||||||
|
void setShowIcon(bool show);
|
||||||
|
|
||||||
|
void updateMaximizeIcon();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_appLabel = nullptr;
|
||||||
|
QMenuBar* m_menuBar = 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
|
||||||
@@ -11,31 +11,37 @@ class TestTheme : public QObject {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private slots:
|
private slots:
|
||||||
void builtInThemes() {
|
void builtInThemes() {
|
||||||
Theme dark = Theme::reclassDark();
|
auto& tm = ThemeManager::instance();
|
||||||
QCOMPARE(dark.name, "Reclass Dark");
|
auto all = tm.themes();
|
||||||
QVERIFY(dark.background.isValid());
|
QVERIFY(all.size() >= 2);
|
||||||
QVERIFY(dark.text.isValid());
|
|
||||||
QVERIFY(dark.syntaxKeyword.isValid());
|
|
||||||
QVERIFY(dark.markerError.isValid());
|
|
||||||
|
|
||||||
Theme warm = Theme::warm();
|
// Find themes by name
|
||||||
QCOMPARE(warm.name, "Warm");
|
const Theme* dark = nullptr;
|
||||||
QVERIFY(warm.background.isValid());
|
const Theme* warm = nullptr;
|
||||||
QVERIFY(warm.text.isValid());
|
for (const auto& t : all) {
|
||||||
QCOMPARE(warm.background, QColor("#212121"));
|
if (t.name == "Reclass Dark") dark = &t;
|
||||||
QCOMPARE(warm.selection, QColor("#21213A"));
|
if (t.name == "Warm") warm = &t;
|
||||||
QCOMPARE(warm.syntaxKeyword, QColor("#AA9565"));
|
|
||||||
QCOMPARE(warm.syntaxType, QColor("#6B959F"));
|
|
||||||
}
|
}
|
||||||
|
QVERIFY(dark);
|
||||||
|
QCOMPARE(dark->name, QString("Reclass Dark"));
|
||||||
|
QVERIFY(dark->background.isValid());
|
||||||
|
QVERIFY(dark->text.isValid());
|
||||||
|
QVERIFY(dark->syntaxKeyword.isValid());
|
||||||
|
QVERIFY(dark->markerError.isValid());
|
||||||
|
|
||||||
void selectionColorFixed() {
|
QVERIFY(warm);
|
||||||
Theme dark = Theme::reclassDark();
|
QCOMPARE(warm->name, QString("Warm"));
|
||||||
QCOMPARE(dark.selection, QColor("#2b2b2b"));
|
QVERIFY(warm->background.isValid());
|
||||||
QVERIFY(dark.selection != QColor("#264f78"));
|
QVERIFY(warm->text.isValid());
|
||||||
|
QCOMPARE(warm->background, QColor("#212121"));
|
||||||
|
QCOMPARE(warm->selection, QColor("#21213A"));
|
||||||
|
QCOMPARE(warm->syntaxKeyword, QColor("#AA9565"));
|
||||||
|
QCOMPARE(warm->syntaxType, QColor("#6B959F"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void jsonRoundTrip() {
|
void jsonRoundTrip() {
|
||||||
Theme orig = Theme::reclassDark();
|
auto& tm = ThemeManager::instance();
|
||||||
|
Theme orig = tm.themes()[0];
|
||||||
QJsonObject json = orig.toJson();
|
QJsonObject json = orig.toJson();
|
||||||
Theme loaded = Theme::fromJson(json);
|
Theme loaded = Theme::fromJson(json);
|
||||||
|
|
||||||
@@ -54,7 +60,12 @@ private slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void jsonRoundTripWarm() {
|
void jsonRoundTripWarm() {
|
||||||
Theme orig = Theme::warm();
|
auto& tm = ThemeManager::instance();
|
||||||
|
auto all = tm.themes();
|
||||||
|
Theme orig;
|
||||||
|
for (const auto& t : all)
|
||||||
|
if (t.name == "Warm") { orig = t; break; }
|
||||||
|
|
||||||
QJsonObject json = orig.toJson();
|
QJsonObject json = orig.toJson();
|
||||||
Theme loaded = Theme::fromJson(json);
|
Theme loaded = Theme::fromJson(json);
|
||||||
|
|
||||||
@@ -70,21 +81,20 @@ private slots:
|
|||||||
sparse["background"] = "#ff0000";
|
sparse["background"] = "#ff0000";
|
||||||
Theme t = Theme::fromJson(sparse);
|
Theme t = Theme::fromJson(sparse);
|
||||||
|
|
||||||
QCOMPARE(t.name, "Sparse");
|
QCOMPARE(t.name, QString("Sparse"));
|
||||||
QCOMPARE(t.background, QColor("#ff0000"));
|
QCOMPARE(t.background, QColor("#ff0000"));
|
||||||
// Missing fields fall back to reclassDark defaults
|
// Missing fields are default (invalid) QColor
|
||||||
Theme defaults = Theme::reclassDark();
|
QVERIFY(!t.text.isValid());
|
||||||
QCOMPARE(t.text, defaults.text);
|
QVERIFY(!t.syntaxKeyword.isValid());
|
||||||
QCOMPARE(t.syntaxKeyword, defaults.syntaxKeyword);
|
QVERIFY(!t.markerError.isValid());
|
||||||
QCOMPARE(t.markerError, defaults.markerError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void themeManagerHasBuiltIns() {
|
void themeManagerHasBuiltIns() {
|
||||||
auto& tm = ThemeManager::instance();
|
auto& tm = ThemeManager::instance();
|
||||||
auto all = tm.themes();
|
auto all = tm.themes();
|
||||||
QVERIFY(all.size() >= 2);
|
QVERIFY(all.size() >= 2);
|
||||||
QCOMPARE(all[0].name, "Reclass Dark");
|
QCOMPARE(all[0].name, QString("Reclass Dark"));
|
||||||
QCOMPARE(all[1].name, "Warm");
|
QCOMPARE(all[1].name, QString("Warm"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void themeManagerSwitch() {
|
void themeManagerSwitch() {
|
||||||
@@ -108,12 +118,12 @@ private slots:
|
|||||||
int initialCount = tm.themes().size();
|
int initialCount = tm.themes().size();
|
||||||
|
|
||||||
// Add
|
// Add
|
||||||
Theme custom = Theme::reclassDark();
|
Theme custom = tm.themes()[0];
|
||||||
custom.name = "Test Custom";
|
custom.name = "Test Custom";
|
||||||
custom.background = QColor("#ff0000");
|
custom.background = QColor("#ff0000");
|
||||||
tm.addTheme(custom);
|
tm.addTheme(custom);
|
||||||
QCOMPARE(tm.themes().size(), initialCount + 1);
|
QCOMPARE(tm.themes().size(), initialCount + 1);
|
||||||
QCOMPARE(tm.themes().last().name, "Test Custom");
|
QCOMPARE(tm.themes().last().name, QString("Test Custom"));
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
int idx = tm.themes().size() - 1;
|
int idx = tm.themes().size() - 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user