mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
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,122 +1,56 @@
|
||||
#include "theme.h"
|
||||
#include <type_traits>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Field table for DRY serialization ──
|
||||
// ── Shared field metadata (serialization + editor UI) ──
|
||||
|
||||
struct ColorField { const char* key; QColor Theme::*ptr; };
|
||||
|
||||
static const ColorField kFields[] = {
|
||||
{"background", &Theme::background},
|
||||
{"backgroundAlt", &Theme::backgroundAlt},
|
||||
{"surface", &Theme::surface},
|
||||
{"border", &Theme::border},
|
||||
{"borderFocused", &Theme::borderFocused},
|
||||
{"button", &Theme::button},
|
||||
{"text", &Theme::text},
|
||||
{"textDim", &Theme::textDim},
|
||||
{"textMuted", &Theme::textMuted},
|
||||
{"textFaint", &Theme::textFaint},
|
||||
{"hover", &Theme::hover},
|
||||
{"selected", &Theme::selected},
|
||||
{"selection", &Theme::selection},
|
||||
{"syntaxKeyword", &Theme::syntaxKeyword},
|
||||
{"syntaxNumber", &Theme::syntaxNumber},
|
||||
{"syntaxString", &Theme::syntaxString},
|
||||
{"syntaxComment", &Theme::syntaxComment},
|
||||
{"syntaxPreproc", &Theme::syntaxPreproc},
|
||||
{"syntaxType", &Theme::syntaxType},
|
||||
{"indHoverSpan", &Theme::indHoverSpan},
|
||||
{"indCmdPill", &Theme::indCmdPill},
|
||||
{"indDataChanged",&Theme::indDataChanged},
|
||||
{"indHintGreen", &Theme::indHintGreen},
|
||||
{"markerPtr", &Theme::markerPtr},
|
||||
{"markerCycle", &Theme::markerCycle},
|
||||
{"markerError", &Theme::markerError},
|
||||
const ThemeFieldMeta kThemeFields[] = {
|
||||
{"background", "Background", "Chrome", &Theme::background},
|
||||
{"backgroundAlt", "Background Alt", "Chrome", &Theme::backgroundAlt},
|
||||
{"surface", "Surface", "Chrome", &Theme::surface},
|
||||
{"border", "Border", "Chrome", &Theme::border},
|
||||
{"borderFocused", "Border Focused", "Chrome", &Theme::borderFocused},
|
||||
{"button", "Button", "Chrome", &Theme::button},
|
||||
{"text", "Text", "Text", &Theme::text},
|
||||
{"textDim", "Text Dim", "Text", &Theme::textDim},
|
||||
{"textMuted", "Text Muted", "Text", &Theme::textMuted},
|
||||
{"textFaint", "Text Faint", "Text", &Theme::textFaint},
|
||||
{"hover", "Hover", "Interactive", &Theme::hover},
|
||||
{"selected", "Selected", "Interactive", &Theme::selected},
|
||||
{"selection", "Selection", "Interactive", &Theme::selection},
|
||||
{"syntaxKeyword", "Keyword", "Syntax", &Theme::syntaxKeyword},
|
||||
{"syntaxNumber", "Number", "Syntax", &Theme::syntaxNumber},
|
||||
{"syntaxString", "String", "Syntax", &Theme::syntaxString},
|
||||
{"syntaxComment", "Comment", "Syntax", &Theme::syntaxComment},
|
||||
{"syntaxPreproc", "Preprocessor", "Syntax", &Theme::syntaxPreproc},
|
||||
{"syntaxType", "Type", "Syntax", &Theme::syntaxType},
|
||||
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
||||
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
||||
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
||||
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
||||
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
||||
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
||||
{"markerError", "Error", "Markers", &Theme::markerError},
|
||||
};
|
||||
const int kThemeFieldCount = static_cast<int>(std::extent_v<decltype(kThemeFields)>);
|
||||
|
||||
QJsonObject Theme::toJson() const {
|
||||
QJsonObject o;
|
||||
o["name"] = name;
|
||||
for (const auto& f : kFields)
|
||||
o[f.key] = (this->*f.ptr).name();
|
||||
for (int i = 0; i < kThemeFieldCount; i++)
|
||||
o[kThemeFields[i].key] = (this->*kThemeFields[i].ptr).name();
|
||||
return o;
|
||||
}
|
||||
|
||||
Theme Theme::fromJson(const QJsonObject& o) {
|
||||
Theme t = reclassDark();
|
||||
t.name = o["name"].toString(t.name);
|
||||
for (const auto& f : kFields) {
|
||||
if (o.contains(f.key))
|
||||
t.*f.ptr = QColor(o[f.key].toString());
|
||||
Theme t;
|
||||
t.name = o["name"].toString("Untitled");
|
||||
for (int i = 0; i < kThemeFieldCount; i++) {
|
||||
if (o.contains(kThemeFields[i].key))
|
||||
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||
}
|
||||
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
|
||||
|
||||
@@ -48,9 +48,18 @@ struct Theme {
|
||||
|
||||
QJsonObject toJson() const;
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QDialogButtonBox>
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <cstring>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -70,7 +71,7 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
||||
: QStringLiteral("File: %1").arg(path));
|
||||
mainLayout->addWidget(m_fileInfoLabel);
|
||||
|
||||
// ── Scrollable area for swatches + contrast ──
|
||||
// ── Scrollable area for swatches ──
|
||||
auto* scroll = new QScrollArea;
|
||||
scroll->setWidgetResizable(true);
|
||||
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->setSpacing(2);
|
||||
|
||||
// ── Color swatches ──
|
||||
struct FieldDef { const char* label; QColor Theme::*ptr; };
|
||||
// ── Color swatches (driven by kThemeFields) ──
|
||||
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) {
|
||||
scrollLayout->addWidget(makeSectionLabel(title));
|
||||
for (const auto& f : fields) {
|
||||
int idx = m_swatches.size();
|
||||
|
||||
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);
|
||||
// Section header on group change
|
||||
if (!currentGroup || std::strcmp(currentGroup, f.group) != 0) {
|
||||
scrollLayout->addWidget(makeSectionLabel(QString::fromLatin1(f.group)));
|
||||
currentGroup = f.group;
|
||||
}
|
||||
};
|
||||
|
||||
addGroup("Chrome", {
|
||||
{"Background", &Theme::background},
|
||||
{"Background Alt", &Theme::backgroundAlt},
|
||||
{"Surface", &Theme::surface},
|
||||
{"Border", &Theme::border},
|
||||
{"Border Focused", &Theme::borderFocused},
|
||||
{"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},
|
||||
});
|
||||
int idx = m_swatches.size();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
scrollLayout->addStretch();
|
||||
scroll->setWidget(scrollWidget);
|
||||
@@ -164,28 +130,21 @@ ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
|
||||
|
||||
// ── Bottom bar ──
|
||||
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();
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
|
||||
if (m_previewing) {
|
||||
ThemeManager::instance().revertPreview();
|
||||
m_previewing = false;
|
||||
}
|
||||
ThemeManager::instance().revertPreview();
|
||||
reject();
|
||||
});
|
||||
bottomRow->addWidget(buttons);
|
||||
mainLayout->addLayout(bottomRow);
|
||||
|
||||
// Initial update
|
||||
// Initial swatch update + start live preview
|
||||
for (int i = 0; i < m_swatches.size(); i++)
|
||||
updateSwatch(i);
|
||||
tm.previewTheme(m_theme);
|
||||
}
|
||||
|
||||
// ── 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++)
|
||||
updateSwatch(i);
|
||||
|
||||
if (m_previewing)
|
||||
tm.previewTheme(m_theme);
|
||||
tm.previewTheme(m_theme);
|
||||
}
|
||||
|
||||
// ── Swatch update ──
|
||||
@@ -231,19 +189,8 @@ void ThemeEditor::pickColor(int idx) {
|
||||
if (c.isValid()) {
|
||||
m_theme.*s.field = c;
|
||||
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
|
||||
|
||||
@@ -36,14 +36,10 @@ private:
|
||||
QComboBox* m_themeCombo = nullptr;
|
||||
QLineEdit* m_nameEdit = nullptr;
|
||||
QLabel* m_fileInfoLabel = nullptr;
|
||||
QPushButton* m_previewBtn = nullptr;
|
||||
bool m_previewing = false;
|
||||
|
||||
void loadTheme(int index);
|
||||
void rebuildSwatches(QVBoxLayout* swatchLayout);
|
||||
void updateSwatch(int idx);
|
||||
void pickColor(int idx);
|
||||
void togglePreview();
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardPaths>
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -13,18 +14,40 @@ ThemeManager& ThemeManager::instance() {
|
||||
}
|
||||
|
||||
ThemeManager::ThemeManager() {
|
||||
m_builtIn.append(Theme::reclassDark());
|
||||
m_builtIn.append(Theme::warm());
|
||||
loadBuiltInThemes();
|
||||
loadUserThemes();
|
||||
|
||||
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();
|
||||
for (int i = 0; i < all.size(); i++) {
|
||||
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> all = m_builtIn;
|
||||
all.append(m_user);
|
||||
@@ -37,7 +60,10 @@ const Theme& ThemeManager::current() const {
|
||||
int userIdx = m_currentIdx - m_builtIn.size();
|
||||
if (userIdx >= 0 && userIdx < m_user.size())
|
||||
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) {
|
||||
@@ -55,17 +81,20 @@ void ThemeManager::addTheme(const Theme& theme) {
|
||||
}
|
||||
|
||||
void ThemeManager::updateTheme(int index, const Theme& theme) {
|
||||
m_previewing = false; // commit any active preview
|
||||
|
||||
if (index < builtInCount()) {
|
||||
// Can't overwrite built-in; save as user theme instead
|
||||
m_user.append(theme);
|
||||
m_builtIn[index] = theme;
|
||||
m_currentIdx = index;
|
||||
} else {
|
||||
int ui = index - builtInCount();
|
||||
if (ui >= 0 && ui < m_user.size())
|
||||
m_user[ui] = theme;
|
||||
}
|
||||
saveUserThemes();
|
||||
if (index == m_currentIdx)
|
||||
emit themeChanged(current());
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
settings.setValue("theme", current().name);
|
||||
emit themeChanged(current());
|
||||
}
|
||||
|
||||
void ThemeManager::removeTheme(int index) {
|
||||
@@ -82,7 +111,9 @@ void ThemeManager::removeTheme(int index) {
|
||||
saveUserThemes();
|
||||
}
|
||||
|
||||
QString ThemeManager::themesDir() const {
|
||||
// ── User theme persistence ──
|
||||
|
||||
QString ThemeManager::userDir() const {
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
||||
+ "/themes";
|
||||
QDir().mkpath(dir);
|
||||
@@ -91,37 +122,69 @@ QString ThemeManager::themesDir() const {
|
||||
|
||||
void ThemeManager::loadUserThemes() {
|
||||
m_user.clear();
|
||||
QDir dir(themesDir());
|
||||
QDir dir(userDir());
|
||||
for (const QString& name : dir.entryList({"*.json"}, QDir::Files)) {
|
||||
QFile f(dir.filePath(name));
|
||||
if (!f.open(QIODevice::ReadOnly)) continue;
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||
if (jdoc.isObject())
|
||||
m_user.append(Theme::fromJson(jdoc.object()));
|
||||
if (!jdoc.isObject()) continue;
|
||||
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 {
|
||||
QString dir = themesDir();
|
||||
// Remove old files
|
||||
QString dir = userDir();
|
||||
QDir d(dir);
|
||||
for (const QString& name : d.entryList({"*.json"}, QDir::Files))
|
||||
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++) {
|
||||
QString filename = m_user[i].name.toLower().replace(' ', '_') + ".json";
|
||||
QFile f(dir + "/" + filename);
|
||||
if (!f.open(QIODevice::WriteOnly)) continue;
|
||||
f.write(QJsonDocument(m_user[i].toJson()).toJson(QJsonDocument::Indented));
|
||||
if (f.open(QIODevice::WriteOnly))
|
||||
f.write(QJsonDocument(m_user[i].toJson()).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (ui < 0 || ui >= m_user.size()) return {};
|
||||
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
|
||||
return themesDir() + "/" + filename;
|
||||
return userDir() + "/" + filename;
|
||||
}
|
||||
|
||||
void ThemeManager::previewTheme(const Theme& theme) {
|
||||
|
||||
@@ -31,14 +31,17 @@ signals:
|
||||
|
||||
private:
|
||||
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;
|
||||
int m_currentIdx = 0;
|
||||
|
||||
int builtInCount() const { return m_builtIn.size(); }
|
||||
QString themesDir() const;
|
||||
void loadBuiltInThemes();
|
||||
QString builtInDir() const;
|
||||
QString userDir() const;
|
||||
bool m_previewing = false;
|
||||
Theme m_savedTheme; // stashed current theme during preview
|
||||
Theme m_savedTheme;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
Reference in New Issue
Block a user