Theme preview/revert, theme editor enhancements, build and deploy updates

This commit is contained in:
IChooseYou
2026-02-12 12:37:09 -07:00
committed by sysadmin
parent e73b783cda
commit 4b1d3e9d3f
18 changed files with 548 additions and 120 deletions

View File

@@ -1622,7 +1622,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
}
// ── Font with zoom ──
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont font(fontName, 12);
font.setFixedPitch(true);

View File

@@ -163,6 +163,7 @@ enum Marker : int {
M_HOVER = 6,
M_SELECTED = 7,
M_CMD_ROW = 8,
M_ACCENT = 9,
};
// ── Node ──

View File

@@ -201,8 +201,11 @@ void RcxEditor::setupMargins() {
m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument()
m_sci->setMarginSensitivity(0, true);
// Margin 1: hidden (fold chevrons moved to text column)
m_sci->setMarginWidth(1, 0);
// Margin 1: 2px accent bar (selection indicator)
m_sci->setMarginType(1, QsciScintilla::SymbolMargin);
m_sci->setMarginWidth(1, 2);
m_sci->setMarginSensitivity(1, false);
m_sci->setMarginMarkerMask(1, 1 << M_ACCENT);
}
void RcxEditor::setupFolding() {
@@ -252,6 +255,9 @@ void RcxEditor::setupMarkers() {
// M_CMD_ROW (8): distinct background for CommandRow bar
m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW);
// M_ACCENT (9): 2px accent bar in margin 1 (selection indicator)
m_sci->markerDefine(QsciScintilla::FullRectangle, M_ACCENT);
}
void RcxEditor::allocateMarginStyles() {
@@ -329,6 +335,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW);
m_sci->setMarkerBackgroundColor(theme.indHoverSpan, M_ACCENT);
// Margin extended styles
if (m_marginStyleBase >= 0) {
@@ -493,6 +500,7 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_currentSelIds = selIds;
m_sci->markerDeleteAll(M_SELECTED);
m_sci->markerDeleteAll(M_ACCENT);
// Clear all editable indicators, then repaint for selected lines only
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
@@ -508,6 +516,7 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
if (selIds.contains(checkId)) {
m_sci->markerAdd(i, M_SELECTED);
m_sci->markerAdd(i, M_ACCENT);
if (!isFooter)
paintEditableSpans(i);
}
@@ -1477,6 +1486,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
}
auto* lm = metaForLine(line);
if (!lm) return false;
// Reject lines that don't support type editing
if (lm->nodeIdx < 0) return false; // CommandRow etc.
if (lm->lineKind == LineKind::Footer) return false;
// Position popup at the type column start
ColumnSpan ts = typeSpan(*lm);
long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0);
@@ -2234,6 +2246,10 @@ void RcxEditor::setGlobalFontName(const QString& fontName) {
g_fontName = fontName;
}
QString RcxEditor::globalFontName() {
return g_fontName;
}
QString RcxEditor::textWithMargins() const {
int lineCount = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINECOUNT);
QStringList lines;

View File

@@ -48,6 +48,7 @@ public:
void setCommandRowText(const QString& line);
void setEditorFont(const QString& fontName);
static void setGlobalFontName(const QString& fontName);
static QString globalFontName();
void applyTheme(const Theme& theme);
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring

View File

@@ -14,7 +14,7 @@
namespace rcx { class Provider; }
/**
* Plugin interface for ReclassX
* Plugin interface for Reclass
*
* Plugins are loaded from the "Plugins" folder as shared libraries.
* Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
@@ -133,4 +133,4 @@ public:
// Plugin factory function signature
typedef IPlugin* (*CreatePluginFunc)();
#define IPLUGIN_IID "com.reclassx.IPlugin/1.0"
#define IPLUGIN_IID "com.reclass.IPlugin/1.0"

View File

@@ -334,7 +334,7 @@ void MainWindow::createMenus() {
actJetBrains->setCheckable(true);
actJetBrains->setActionGroup(fontGroup);
// Load saved preference
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString savedFont = settings.value("font", "JetBrains Mono").toString();
if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true);
else actConsolas->setChecked(true);
@@ -392,7 +392,7 @@ void MainWindow::createStatusBar() {
statusBar()->setAutoFillBackground(true);
}
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
@@ -400,7 +400,7 @@ void MainWindow::createStatusBar() {
}
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont tabFont(fontName, 12);
tabFont.setFixedPitch(true);
@@ -889,7 +889,7 @@ void MainWindow::editTheme() {
}
void MainWindow::setEditorFont(const QString& fontName) {
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
settings.setValue("font", fontName);
QFont f(fontName, 12);
f.setFixedPitch(true);
@@ -959,7 +959,7 @@ void MainWindow::updateWindowTitle() {
// ── Rendered view setup ──
void MainWindow::setupRenderedSci(QsciScintilla* sci) {
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
@@ -1204,7 +1204,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
QString filePath = path;
if (filePath.isEmpty()) {
filePath = QFileDialog::getOpenFileName(this,
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
"Open Definition", {}, "Reclass (*.rcx);;JSON (*.json);;All (*)");
if (filePath.isEmpty()) return nullptr;
}
@@ -1226,7 +1226,7 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
if (saveAs || tab.doc->filePath.isEmpty()) {
QString path = QFileDialog::getSaveFileName(this,
"Save Definition", {}, "ReclassX (*.rcx);;JSON (*.json)");
"Save Definition", {}, "Reclass (*.rcx);;JSON (*.json)");
if (path.isEmpty()) return false;
tab.doc->save(path);
} else {
@@ -1259,7 +1259,7 @@ void MainWindow::createWorkspaceDock() {
// Match editor font
{
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
@@ -1442,8 +1442,8 @@ int main(int argc, char* argv[]) {
#endif
DarkApp app(argc, argv);
app.setApplicationName("ReclassX");
app.setOrganizationName("ReclassX");
app.setApplicationName("Reclass");
app.setOrganizationName("Reclass");
app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing
// Load embedded fonts
@@ -1452,7 +1452,7 @@ int main(int argc, char* argv[]) {
qWarning("Failed to load embedded JetBrains Mono font");
// Apply saved font preference before creating any editors
{
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString savedFont = settings.value("font", "JetBrains Mono").toString();
rcx::RcxEditor::setGlobalFontName(savedFont);
}

View File

@@ -194,7 +194,7 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&
{"protocolVersion", "2024-11-05"},
{"capabilities", caps},
{"serverInfo", QJsonObject{
{"name", "reclassx-mcp"},
{"name", "reclass-mcp"},
{"version", "1.0.0"}
}}
};

View File

@@ -1,95 +1,442 @@
#include "themeeditor.h"
#include <QFormLayout>
#include "thememanager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QDialogButtonBox>
#include <QColorDialog>
#include <QLineEdit>
#include <QLabel>
#include <QComboBox>
#include <cmath>
namespace rcx {
ThemeEditor::ThemeEditor(const Theme& theme, QWidget* parent)
: QDialog(parent), m_theme(theme)
// ── Color utilities ──
namespace {
double srgbLinear(double c) {
return (c <= 0.03928) ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4);
}
double relativeLuminance(const QColor& c) {
return 0.2126 * srgbLinear(c.redF())
+ 0.7152 * srgbLinear(c.greenF())
+ 0.0722 * srgbLinear(c.blueF());
}
double contrastRatio(const QColor& fg, const QColor& bg) {
double l1 = relativeLuminance(fg);
double l2 = relativeLuminance(bg);
if (l1 < l2) std::swap(l1, l2);
return (l1 + 0.05) / (l2 + 0.05);
}
QString wcagLevel(double ratio) {
if (ratio >= 7.0) return QStringLiteral("AAA");
if (ratio >= 4.5) return QStringLiteral("AA");
return QStringLiteral("FAIL");
}
// Compute the minimum fg lightness (HSL L) to reach targetRatio against bg
QColor autoFixFg(const QColor& fg, const QColor& bg, double targetRatio) {
double lBg = relativeLuminance(bg);
// Determine if fg should be lighter or darker than bg
bool fgLighter = relativeLuminance(fg) >= relativeLuminance(bg);
double targetLum;
if (fgLighter)
targetLum = targetRatio * (lBg + 0.05) - 0.05;
else
targetLum = (lBg + 0.05) / targetRatio - 0.05;
targetLum = qBound(0.0, targetLum, 1.0);
// Binary search for HSL lightness that yields the target luminance
int h, s, l, a;
fg.getHsl(&h, &s, &l, &a);
int lo = fgLighter ? l : 0;
int hi = fgLighter ? 255 : l;
for (int iter = 0; iter < 20; iter++) {
int mid = (lo + hi) / 2;
QColor test;
test.setHsl(h, s, mid, a);
double testLum = relativeLuminance(test);
if (fgLighter) {
if (testLum < targetLum) lo = mid + 1;
else hi = mid;
} else {
if (testLum > targetLum) hi = mid - 1;
else lo = mid;
}
}
QColor result;
result.setHsl(h, s, fgLighter ? hi : lo, a);
return result;
}
} // anon
// ── Section header label ──
static QLabel* makeSectionLabel(const QString& text) {
auto* lbl = new QLabel(text);
lbl->setStyleSheet(QStringLiteral(
"font-weight: bold; font-size: 11px; color: #888;"
"padding: 6px 0 2px 0; border-bottom: 1px solid #444;"));
return lbl;
}
// ── Constructor ──
ThemeEditor::ThemeEditor(int themeIndex, QWidget* parent)
: QDialog(parent), m_themeIndex(themeIndex)
{
setWindowTitle("Edit Theme");
setMinimumWidth(320);
auto& tm = ThemeManager::instance();
auto all = tm.themes();
m_theme = (themeIndex >= 0 && themeIndex < all.size()) ? all[themeIndex] : tm.current();
auto* form = new QFormLayout;
setWindowTitle(QStringLiteral("Theme Editor"));
setMinimumSize(420, 480);
resize(440, 640);
// Name field
auto* nameEdit = new QLineEdit(m_theme.name);
connect(nameEdit, &QLineEdit::textChanged, this, [this](const QString& t) {
m_theme.name = t;
});
form->addRow("Name", nameEdit);
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(6);
// Color swatches
// ── Theme selector combo ──
{
auto* row = new QHBoxLayout;
row->addWidget(new QLabel(QStringLiteral("Theme:")));
m_themeCombo = new QComboBox;
for (const auto& t : all)
m_themeCombo->addItem(t.name);
m_themeCombo->setCurrentIndex(themeIndex);
connect(m_themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this](int idx) { loadTheme(idx); });
row->addWidget(m_themeCombo, 1);
mainLayout->addLayout(row);
}
// ── Name field ──
{
auto* row = new QHBoxLayout;
row->addWidget(new QLabel(QStringLiteral("Name:")));
m_nameEdit = new QLineEdit(m_theme.name);
connect(m_nameEdit, &QLineEdit::textChanged, this, [this](const QString& t) {
m_theme.name = t;
});
row->addWidget(m_nameEdit, 1);
mainLayout->addLayout(row);
}
// ── File info ──
m_fileInfoLabel = new QLabel;
m_fileInfoLabel->setStyleSheet(QStringLiteral("color: #666; font-size: 10px; padding: 0 0 4px 0;"));
QString path = tm.themeFilePath(themeIndex);
m_fileInfoLabel->setText(path.isEmpty()
? QStringLiteral("Built-in theme (edits save as user copy)")
: QStringLiteral("File: %1").arg(path));
mainLayout->addWidget(m_fileInfoLabel);
// ── Scrollable area for swatches + contrast ──
auto* scroll = new QScrollArea;
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto* scrollWidget = new QWidget;
auto* scrollLayout = new QVBoxLayout(scrollWidget);
scrollLayout->setContentsMargins(0, 0, 6, 0); // right margin for scrollbar
scrollLayout->setSpacing(2);
// ── Color swatches ──
struct FieldDef { const char* label; QColor Theme::*ptr; };
const FieldDef fields[] = {
{"Background", &Theme::background},
{"Background Alt", &Theme::backgroundAlt},
{"Surface", &Theme::surface},
{"Border", &Theme::border},
{"Button", &Theme::button},
{"Text", &Theme::text},
{"Text Dim", &Theme::textDim},
{"Text Muted", &Theme::textMuted},
{"Text Faint", &Theme::textFaint},
{"Hover", &Theme::hover},
{"Selected", &Theme::selected},
{"Selection", &Theme::selection},
{"Keyword", &Theme::syntaxKeyword},
{"Number", &Theme::syntaxNumber},
{"String", &Theme::syntaxString},
{"Comment", &Theme::syntaxComment},
{"Preprocessor", &Theme::syntaxPreproc},
{"Type", &Theme::syntaxType},
{"Hover Span", &Theme::indHoverSpan},
{"Cmd Pill", &Theme::indCmdPill},
{"Data Changed", &Theme::indDataChanged},
{"Hint Green", &Theme::indHintGreen},
{"Pointer Marker", &Theme::markerPtr},
{"Cycle Marker", &Theme::markerCycle},
{"Error Marker", &Theme::markerError},
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);
}
};
for (const auto& f : fields) {
auto* btn = new QPushButton;
btn->setFixedSize(60, 24);
btn->setCursor(Qt::PointingHandCursor);
SwatchEntry entry{f.label, f.ptr, btn};
m_swatches.append(entry);
updateSwatch(m_swatches.last());
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},
});
int idx = m_swatches.size() - 1;
connect(btn, &QPushButton::clicked, this, [this, idx]() {
pickColor(m_swatches[idx]);
});
form->addRow(f.label, btn);
// ── Contrast pairs ──
scrollLayout->addWidget(makeSectionLabel(QStringLiteral("Contrast")));
struct PairDef {
const char* fgLabel; const char* bgLabel;
QColor Theme::*fg; QColor Theme::*bg;
};
const PairDef pairs[] = {
{"text", "background", &Theme::text, &Theme::background},
{"textDim", "background", &Theme::textDim, &Theme::background},
{"textMuted", "background", &Theme::textMuted, &Theme::background},
{"textFaint", "background", &Theme::textFaint, &Theme::background},
{"text", "backgroundAlt", &Theme::text, &Theme::backgroundAlt},
{"text", "surface", &Theme::text, &Theme::surface},
{"keyword", "background", &Theme::syntaxKeyword, &Theme::background},
{"type", "background", &Theme::syntaxType, &Theme::background},
{"number", "background", &Theme::syntaxNumber, &Theme::background},
{"string", "background", &Theme::syntaxString, &Theme::background},
{"comment", "background", &Theme::syntaxComment, &Theme::background},
{"preproc", "background", &Theme::syntaxPreproc, &Theme::background},
{"hoverSpan", "background", &Theme::indHoverSpan, &Theme::background},
{"hintGreen", "background", &Theme::indHintGreen, &Theme::background},
};
for (int pi = 0; pi < (int)(sizeof(pairs) / sizeof(pairs[0])); pi++) {
const auto& p = pairs[pi];
int idx = m_contrastPairs.size();
auto* row = new QHBoxLayout;
row->setSpacing(4);
row->setContentsMargins(8, 1, 0, 1);
auto* pairLabel = new QLabel(QStringLiteral("%1 / %2")
.arg(QString::fromLatin1(p.fgLabel), QString::fromLatin1(p.bgLabel)));
pairLabel->setFixedWidth(150);
pairLabel->setStyleSheet(QStringLiteral("font-size: 10px;"));
row->addWidget(pairLabel);
auto* ratioLbl = new QLabel;
ratioLbl->setFixedWidth(44);
ratioLbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
ratioLbl->setStyleSheet(QStringLiteral("font-size: 10px;"));
row->addWidget(ratioLbl);
auto* levelLbl = new QLabel;
levelLbl->setFixedWidth(34);
levelLbl->setAlignment(Qt::AlignCenter);
row->addWidget(levelLbl);
auto* fixBtn = new QPushButton(QStringLiteral("Fix"));
fixBtn->setFixedSize(36, 18);
fixBtn->setCursor(Qt::PointingHandCursor);
fixBtn->setStyleSheet(QStringLiteral(
"QPushButton { font-size: 9px; padding: 0; border: 1px solid #555; border-radius: 2px; }"
"QPushButton:hover { background: #444; }"));
fixBtn->hide();
connect(fixBtn, &QPushButton::clicked, this, [this, idx]() { autoFixContrast(idx); });
row->addWidget(fixBtn);
row->addStretch();
ContrastEntry ce;
ce.fgLabel = p.fgLabel;
ce.bgLabel = p.bgLabel;
ce.fgField = p.fg;
ce.bgField = p.bg;
ce.ratioLabel = ratioLbl;
ce.levelLabel = levelLbl;
ce.fixBtn = fixBtn;
m_contrastPairs.append(ce);
scrollLayout->addLayout(row);
}
scrollLayout->addStretch();
scroll->setWidget(scrollWidget);
mainLayout->addWidget(scroll, 1);
// ── 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, &QDialog::reject);
connect(buttons, &QDialogButtonBox::rejected, this, [this]() {
if (m_previewing) {
ThemeManager::instance().revertPreview();
m_previewing = false;
}
reject();
});
bottomRow->addWidget(buttons);
mainLayout->addLayout(bottomRow);
auto* layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(buttons);
// Initial update
for (int i = 0; i < m_swatches.size(); i++)
updateSwatch(i);
updateAllContrast();
}
void ThemeEditor::updateSwatch(SwatchEntry& entry) {
QColor c = m_theme.*entry.field;
entry.button->setStyleSheet(QStringLiteral(
"QPushButton { background: %1; border: 1px solid #555; border-radius: 3px; }")
// ── Load a different theme into the editor ──
void ThemeEditor::loadTheme(int index) {
auto& tm = ThemeManager::instance();
auto all = tm.themes();
if (index < 0 || index >= all.size()) return;
m_themeIndex = index;
m_theme = all[index];
m_nameEdit->setText(m_theme.name);
QString path = tm.themeFilePath(index);
m_fileInfoLabel->setText(path.isEmpty()
? QStringLiteral("Built-in theme (edits save as user copy)")
: QStringLiteral("File: %1").arg(path));
for (int i = 0; i < m_swatches.size(); i++)
updateSwatch(i);
updateAllContrast();
if (m_previewing)
tm.previewTheme(m_theme);
}
// ── Swatch update ──
void ThemeEditor::updateSwatch(int idx) {
auto& s = m_swatches[idx];
QColor c = m_theme.*s.field;
s.swatchBtn->setStyleSheet(QStringLiteral(
"QPushButton { background: %1; border: 1px solid #555; border-radius: 2px; }")
.arg(c.name()));
entry.button->setToolTip(c.name());
s.hexLabel->setText(c.name());
}
void ThemeEditor::pickColor(SwatchEntry& entry) {
QColor c = QColorDialog::getColor(m_theme.*entry.field, this, entry.label);
if (c.isValid()) {
m_theme.*entry.field = c;
updateSwatch(entry);
// ── Contrast update ──
void ThemeEditor::updateAllContrast() {
for (int i = 0; i < m_contrastPairs.size(); i++) {
auto& cp = m_contrastPairs[i];
QColor fg = m_theme.*cp.fgField;
QColor bg = m_theme.*cp.bgField;
double ratio = contrastRatio(fg, bg);
QString level = wcagLevel(ratio);
cp.ratioLabel->setText(QStringLiteral("%1:1").arg(ratio, 0, 'f', 1));
cp.levelLabel->setText(level);
if (level == "AAA")
cp.levelLabel->setStyleSheet(QStringLiteral("color: #4ec94e; font-weight: bold; font-size: 10px;"));
else if (level == "AA")
cp.levelLabel->setStyleSheet(QStringLiteral("color: #c9c94e; font-weight: bold; font-size: 10px;"));
else
cp.levelLabel->setStyleSheet(QStringLiteral("color: #c94e4e; font-weight: bold; font-size: 10px;"));
cp.fixBtn->setVisible(level == "FAIL");
}
}
// ── Color picker ──
void ThemeEditor::pickColor(int idx) {
auto& s = m_swatches[idx];
QColor c = QColorDialog::getColor(m_theme.*s.field, this, QString::fromLatin1(s.label));
if (c.isValid()) {
m_theme.*s.field = c;
updateSwatch(idx);
updateAllContrast();
if (m_previewing)
ThemeManager::instance().previewTheme(m_theme);
}
}
// ── Auto-fix contrast ──
void ThemeEditor::autoFixContrast(int idx) {
auto& cp = m_contrastPairs[idx];
QColor fg = m_theme.*cp.fgField;
QColor bg = m_theme.*cp.bgField;
QColor fixed = autoFixFg(fg, bg, 4.6); // slightly above 4.5 for margin
m_theme.*cp.fgField = fixed;
// Update the swatch that owns this fg color
for (int i = 0; i < m_swatches.size(); i++) {
if (m_swatches[i].field == cp.fgField) {
updateSwatch(i);
break;
}
}
updateAllContrast();
if (m_previewing)
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

View File

@@ -3,27 +3,61 @@
#include <QDialog>
#include <QVector>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
class QScrollArea;
class QVBoxLayout;
class QComboBox;
namespace rcx {
class ThemeEditor : public QDialog {
Q_OBJECT
public:
explicit ThemeEditor(const Theme& theme, QWidget* parent = nullptr);
explicit ThemeEditor(int themeIndex, QWidget* parent = nullptr);
Theme result() const { return m_theme; }
int selectedIndex() const { return m_themeIndex; }
private:
Theme m_theme;
int m_themeIndex;
// ── Swatch row (compact: label + swatch + hex) ──
struct SwatchEntry {
const char* label;
const char* label;
QColor Theme::*field;
QPushButton* button;
QPushButton* swatchBtn = nullptr;
QLabel* hexLabel = nullptr;
};
QVector<SwatchEntry> m_swatches;
void updateSwatch(SwatchEntry& entry);
void pickColor(SwatchEntry& entry);
// ── Contrast pair row ──
struct ContrastEntry {
const char* fgLabel;
const char* bgLabel;
QColor Theme::*fgField;
QColor Theme::*bgField;
QLabel* ratioLabel = nullptr;
QLabel* levelLabel = nullptr;
QPushButton* fixBtn = nullptr;
};
QVector<ContrastEntry> m_contrastPairs;
// ── UI ──
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 updateAllContrast();
void pickColor(int idx);
void autoFixContrast(int idx);
void togglePreview();
};
} // namespace rcx

View File

@@ -17,7 +17,7 @@ ThemeManager::ThemeManager() {
m_builtIn.append(Theme::warm());
loadUserThemes();
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
QString saved = settings.value("theme", m_builtIn[0].name).toString();
auto all = themes();
for (int i = 0; i < all.size(); i++) {
@@ -44,7 +44,7 @@ void ThemeManager::setCurrent(int index) {
auto all = themes();
if (index < 0 || index >= all.size()) return;
m_currentIdx = index;
QSettings settings("ReclassX", "ReclassX");
QSettings settings("Reclass", "Reclass");
settings.setValue("theme", all[index].name);
emit themeChanged(current());
}
@@ -116,4 +116,27 @@ void ThemeManager::saveUserThemes() const {
}
}
QString ThemeManager::themeFilePath(int index) const {
if (index < builtInCount()) return {};
int ui = index - builtInCount();
if (ui < 0 || ui >= m_user.size()) return {};
QString filename = m_user[ui].name.toLower().replace(' ', '_') + ".json";
return themesDir() + "/" + filename;
}
void ThemeManager::previewTheme(const Theme& theme) {
if (!m_previewing) {
m_savedTheme = current();
m_previewing = true;
}
emit themeChanged(theme);
}
void ThemeManager::revertPreview() {
if (m_previewing) {
m_previewing = false;
emit themeChanged(m_savedTheme);
}
}
} // namespace rcx

View File

@@ -22,6 +22,10 @@ public:
void loadUserThemes();
void saveUserThemes() const;
QString themeFilePath(int index) const;
void previewTheme(const Theme& theme);
void revertPreview();
signals:
void themeChanged(const rcx::Theme& theme);
@@ -33,6 +37,8 @@ private:
int builtInCount() const { return m_builtIn.size(); }
QString themesDir() const;
bool m_previewing = false;
Theme m_savedTheme; // stashed current theme during preview
};
} // namespace rcx