Theme system overhaul, UI polish, and VS2022 Dark theme

- Replaced hardcoded theme factories with JSON files + CMake build step
- Shared ThemeFieldMeta table for DRY serialization and editor UI
- Fixed live preview (auto-triggers on color change, no toggle button)
- Fixed duplicate theme entries when editing built-in themes
- Moved title bar from icon to bold "Reclass" text with View > Show Icon toggle
- MDI tabs: 24px height, unicode close button styled like TypeSelectorPopup
- Added VS2022 Dark theme with purple accent colors
- Status bar padding, removed monospace font overrides on tabs/statusbar
- Default startup opens Ball demo + Unnamed hex64 tabs
This commit is contained in:
IChooseYou
2026-02-13 16:23:12 -07:00
committed by sysadmin
parent 5a9a6b754f
commit a86912add1
16 changed files with 429 additions and 328 deletions

View File

@@ -85,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

View File

@@ -288,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();
@@ -410,6 +402,15 @@ 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
@@ -432,6 +433,7 @@ void MainWindow::createMenus() {
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();
@@ -441,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; }"
@@ -468,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;
@@ -656,6 +678,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
ctrl->refresh(); ctrl->refresh();
rebuildWorkspaceModel(); rebuildWorkspaceModel();
styleTabCloseButtons();
return sub; return sub;
} }
@@ -762,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() {
@@ -930,13 +987,16 @@ void MainWindow::applyTheme(const Theme& theme) {
// 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();
@@ -958,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();
} }
@@ -991,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
@@ -1001,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 {
@@ -1045,7 +1088,6 @@ void MainWindow::updateWindowTitle() {
title = "Reclass"; title = "Reclass";
} }
setWindowTitle(title); setWindowTitle(title);
m_titleBar->setTitle(title);
} }
// ── Rendered view setup ── // ── Rendered view setup ──

View File

@@ -107,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();

View File

@@ -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>

View 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"
}

View 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"
}

View 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"
}

View File

@@ -1,122 +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},
{"borderFocused", &Theme::borderFocused}, {"text", "Text", "Text", &Theme::text},
{"button", &Theme::button}, {"textDim", "Text Dim", "Text", &Theme::textDim},
{"text", &Theme::text}, {"textMuted", "Text Muted", "Text", &Theme::textMuted},
{"textDim", &Theme::textDim}, {"textFaint", "Text Faint", "Text", &Theme::textFaint},
{"textMuted", &Theme::textMuted}, {"hover", "Hover", "Interactive", &Theme::hover},
{"textFaint", &Theme::textFaint}, {"selected", "Selected", "Interactive", &Theme::selected},
{"hover", &Theme::hover}, {"selection", "Selection", "Interactive", &Theme::selection},
{"selected", &Theme::selected}, {"syntaxKeyword", "Keyword", "Syntax", &Theme::syntaxKeyword},
{"selection", &Theme::selection}, {"syntaxNumber", "Number", "Syntax", &Theme::syntaxNumber},
{"syntaxKeyword", &Theme::syntaxKeyword}, {"syntaxString", "String", "Syntax", &Theme::syntaxString},
{"syntaxNumber", &Theme::syntaxNumber}, {"syntaxComment", "Comment", "Syntax", &Theme::syntaxComment},
{"syntaxString", &Theme::syntaxString}, {"syntaxPreproc", "Preprocessor", "Syntax", &Theme::syntaxPreproc},
{"syntaxComment", &Theme::syntaxComment}, {"syntaxType", "Type", "Syntax", &Theme::syntaxType},
{"syntaxPreproc", &Theme::syntaxPreproc}, {"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
{"syntaxType", &Theme::syntaxType}, {"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
{"indHoverSpan", &Theme::indHoverSpan}, {"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
{"indCmdPill", &Theme::indCmdPill}, {"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
{"indDataChanged",&Theme::indDataChanged}, {"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
{"indHintGreen", &Theme::indHintGreen}, {"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
{"markerPtr", &Theme::markerPtr}, {"markerError", "Error", "Markers", &Theme::markerError},
{"markerCycle", &Theme::markerCycle},
{"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.borderFocused = QColor("#64e6b450"); // indHoverSpan at ~40% alpha
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.borderFocused = QColor("#64aa9565"); // indHoverSpan at ~40% alpha
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

View File

@@ -48,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

View File

@@ -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,84 +80,49 @@ 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];
auto addGroup = [&](const QString& title, std::initializer_list<FieldDef> fields) { // Section header on group change
scrollLayout->addWidget(makeSectionLabel(title)); if (!currentGroup || std::strcmp(currentGroup, f.group) != 0) {
for (const auto& f : fields) { scrollLayout->addWidget(makeSectionLabel(QString::fromLatin1(f.group)));
int idx = m_swatches.size(); currentGroup = f.group;
auto* row = new QHBoxLayout;
row->setSpacing(6);
row->setContentsMargins(8, 1, 0, 1);
auto* lbl = new QLabel(QString::fromLatin1(f.label));
lbl->setFixedWidth(120);
row->addWidget(lbl);
auto* swatchBtn = new QPushButton;
swatchBtn->setFixedSize(32, 18);
swatchBtn->setCursor(Qt::PointingHandCursor);
connect(swatchBtn, &QPushButton::clicked, this, [this, idx]() { pickColor(idx); });
row->addWidget(swatchBtn);
auto* hexLbl = new QLabel;
hexLbl->setFixedWidth(60);
hexLbl->setStyleSheet(QStringLiteral("color: #aaa; font-size: 10px;"));
row->addWidget(hexLbl);
row->addStretch();
SwatchEntry se;
se.label = f.label;
se.field = f.ptr;
se.swatchBtn = swatchBtn;
se.hexLabel = hexLbl;
m_swatches.append(se);
scrollLayout->addLayout(row);
} }
};
addGroup("Chrome", { int idx = m_swatches.size();
{"Background", &Theme::background},
{"Background Alt", &Theme::backgroundAlt}, auto* row = new QHBoxLayout;
{"Surface", &Theme::surface}, row->setSpacing(6);
{"Border", &Theme::border}, row->setContentsMargins(8, 1, 0, 1);
{"Border Focused", &Theme::borderFocused},
{"Button", &Theme::button}, auto* lbl = new QLabel(QString::fromLatin1(f.label));
}); lbl->setFixedWidth(120);
addGroup("Text", { row->addWidget(lbl);
{"Text", &Theme::text},
{"Text Dim", &Theme::textDim}, auto* swatchBtn = new QPushButton;
{"Text Muted", &Theme::textMuted}, swatchBtn->setFixedSize(32, 18);
{"Text Faint", &Theme::textFaint}, swatchBtn->setCursor(Qt::PointingHandCursor);
}); connect(swatchBtn, &QPushButton::clicked, this, [this, idx]() { pickColor(idx); });
addGroup("Interactive", { row->addWidget(swatchBtn);
{"Hover", &Theme::hover},
{"Selected", &Theme::selected}, auto* hexLbl = new QLabel;
{"Selection", &Theme::selection}, hexLbl->setFixedWidth(60);
}); hexLbl->setStyleSheet(QStringLiteral("color: #aaa; font-size: 10px;"));
addGroup("Syntax", { row->addWidget(hexLbl);
{"Keyword", &Theme::syntaxKeyword},
{"Number", &Theme::syntaxNumber}, row->addStretch();
{"String", &Theme::syntaxString},
{"Comment", &Theme::syntaxComment}, SwatchEntry se;
{"Preprocessor", &Theme::syntaxPreproc}, se.label = f.label;
{"Type", &Theme::syntaxType}, se.field = f.ptr;
}); se.swatchBtn = swatchBtn;
addGroup("Indicators", { se.hexLabel = hexLbl;
{"Hover Span", &Theme::indHoverSpan}, m_swatches.append(se);
{"Cmd Pill", &Theme::indCmdPill},
{"Data Changed", &Theme::indDataChanged}, scrollLayout->addLayout(row);
{"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);
@@ -164,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 ──
@@ -207,8 +166,7 @@ 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);
} }
// ── Swatch update ── // ── Swatch update ──
@@ -231,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

View File

@@ -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

View File

@@ -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];
return m_builtIn[0]; if (!m_builtIn.isEmpty())
return m_builtIn[0];
static const Theme empty;
return empty;
} }
void ThemeManager::setCurrent(int index) { void ThemeManager::setCurrent(int index) {
@@ -55,17 +81,20 @@ 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");
emit themeChanged(current()); settings.setValue("theme", current().name);
emit themeChanged(current());
} }
void ThemeManager::removeTheme(int index) { void ThemeManager::removeTheme(int index) {
@@ -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) {

View File

@@ -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

View File

@@ -1,4 +1,5 @@
#include "titlebar.h" #include "titlebar.h"
#include "themes/thememanager.h"
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QStyle> #include <QStyle>
@@ -8,7 +9,7 @@ namespace rcx {
TitleBarWidget::TitleBarWidget(QWidget* parent) TitleBarWidget::TitleBarWidget(QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_theme(Theme::reclassDark()) , m_theme(ThemeManager::instance().current())
{ {
setFixedHeight(32); setFixedHeight(32);
@@ -16,13 +17,11 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0); layout->setSpacing(0);
// App icon // App name
auto* iconLabel = new QLabel(this); m_appLabel = new QLabel(QStringLiteral("Reclass"), this);
iconLabel->setPixmap(QPixmap(":/icons/class.png").scaled(24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation)); m_appLabel->setContentsMargins(10, 0, 4, 0);
iconLabel->setFixedSize(32, 32); m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
iconLabel->setAlignment(Qt::AlignCenter); layout->addWidget(m_appLabel);
iconLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
layout->addWidget(iconLabel);
// Menu bar // Menu bar
m_menuBar = new QMenuBar(this); m_menuBar = new QMenuBar(this);
@@ -32,14 +31,6 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
layout->addStretch(); 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 // Chrome buttons
m_btnMin = makeChromeButton(":/vsicons/chrome-minimize.svg"); m_btnMin = makeChromeButton(":/vsicons/chrome-minimize.svg");
m_btnMax = makeChromeButton(":/vsicons/chrome-maximize.svg"); m_btnMax = makeChromeButton(":/vsicons/chrome-maximize.svg");
@@ -70,10 +61,6 @@ QToolButton* TitleBarWidget::makeChromeButton(const QString& iconPath) {
return btn; return btn;
} }
void TitleBarWidget::setTitle(const QString& title) {
m_titleLabel->setText(title);
}
void TitleBarWidget::applyTheme(const Theme& theme) { void TitleBarWidget::applyTheme(const Theme& theme) {
m_theme = theme; m_theme = theme;
@@ -83,9 +70,9 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
pal.setColor(QPalette::Window, theme.background); pal.setColor(QPalette::Window, theme.background);
setPalette(pal); setPalette(pal);
// Title text // App label
m_titleLabel->setStyleSheet( m_appLabel->setStyleSheet(
QStringLiteral("QLabel { color: %1; font-size: 12px; }") QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
.arg(theme.textDim.name())); .arg(theme.textDim.name()));
// Menu bar styling — transparent background, themed text // Menu bar styling — transparent background, themed text
@@ -113,6 +100,19 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
update(); update();
} }
void TitleBarWidget::setShowIcon(bool show) {
if (show) {
m_appLabel->setText(QString());
m_appLabel->setPixmap(QIcon(":/icons/class.png").pixmap(16, 16));
} 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() { void TitleBarWidget::updateMaximizeIcon() {
if (window()->isMaximized()) if (window()->isMaximized())
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg")); m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));

View File

@@ -14,8 +14,8 @@ public:
explicit TitleBarWidget(QWidget* parent = nullptr); explicit TitleBarWidget(QWidget* parent = nullptr);
QMenuBar* menuBar() const { return m_menuBar; } QMenuBar* menuBar() const { return m_menuBar; }
void setTitle(const QString& title);
void applyTheme(const Theme& theme); void applyTheme(const Theme& theme);
void setShowIcon(bool show);
void updateMaximizeIcon(); void updateMaximizeIcon();
@@ -25,8 +25,8 @@ protected:
void paintEvent(QPaintEvent* event) override; void paintEvent(QPaintEvent* event) override;
private: private:
QLabel* m_appLabel = nullptr;
QMenuBar* m_menuBar = nullptr; QMenuBar* m_menuBar = nullptr;
QLabel* m_titleLabel = nullptr;
QToolButton* m_btnMin = nullptr; QToolButton* m_btnMin = nullptr;
QToolButton* m_btnMax = nullptr; QToolButton* m_btnMax = nullptr;
QToolButton* m_btnClose = nullptr; QToolButton* m_btnClose = nullptr;

View File

@@ -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;