mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add theme system with Reclass Dark and Warm built-in themes
Replaces ~40 hardcoded color values with 27 semantic color roles. Adds ThemeManager singleton, theme editor dialog, View > Theme menu, JSON persistence for user themes, and fixes inline edit selection color from blue #264f78 to #2b2b2b.
This commit is contained in:
@@ -35,6 +35,12 @@ add_executable(ReclassX
|
||||
src/pluginmanager.h
|
||||
src/typeselectorpopup.h
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.h
|
||||
src/themes/theme.cpp
|
||||
src/themes/thememanager.h
|
||||
src/themes/thememanager.cpp
|
||||
src/themes/themeeditor.h
|
||||
src/themes/themeeditor.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ReclassX PRIVATE src)
|
||||
@@ -112,7 +118,8 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_compose PRIVATE Qt6::Core Qt6::Test)
|
||||
add_test(NAME test_compose COMMAND test_compose)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp)
|
||||
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_editor PRIVATE src)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
@@ -140,7 +147,8 @@ if(BUILD_TESTING)
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp)
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_controller PRIVATE src)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
@@ -153,7 +161,8 @@ if(BUILD_TESTING)
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp)
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_validation PRIVATE src)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
@@ -172,7 +181,8 @@ if(BUILD_TESTING)
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp)
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_context_menu PRIVATE src)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
@@ -193,7 +203,8 @@ if(BUILD_TESTING)
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp)
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_new_features PRIVATE src)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
@@ -206,11 +217,18 @@ if(BUILD_TESTING)
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp)
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_type_selector PRIVATE src)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_theme tests/test_theme.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_theme PRIVATE src)
|
||||
target_link_libraries(test_theme PRIVATE Qt6::Widgets Qt6::Test)
|
||||
add_test(NAME test_theme COMMAND test_theme)
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
|
||||
166
src/editor.cpp
166
src/editor.cpp
@@ -14,15 +14,10 @@
|
||||
#include <QCursor>
|
||||
#include <QMenu>
|
||||
#include <QApplication>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Theme constants ──
|
||||
static const QColor kBgText("#1e1e1e");
|
||||
static const QColor kBgMargin("#1e1e1e"); // matches regular editor background
|
||||
static const QColor kFgMargin("#858585");
|
||||
static const QColor kFgMarginDim("#505050");
|
||||
|
||||
static constexpr int IND_EDITABLE = 8;
|
||||
static constexpr int IND_HEX_DIM = 9;
|
||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||
@@ -54,6 +49,10 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
setupMarkers();
|
||||
allocateMarginStyles();
|
||||
|
||||
applyTheme(ThemeManager::instance().current());
|
||||
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
||||
this, &RcxEditor::applyTheme);
|
||||
|
||||
m_sci->installEventFilter(this);
|
||||
m_sci->viewport()->installEventFilter(this);
|
||||
m_sci->viewport()->setMouseTracking(true);
|
||||
@@ -122,15 +121,9 @@ void RcxEditor::setupScintilla() {
|
||||
// Arrow cursor by default — not the I-beam (this is a structured viewer, not a text editor)
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
|
||||
m_sci->setPaper(kBgText);
|
||||
m_sci->setColor(QColor("#d4d4d4"));
|
||||
|
||||
m_sci->setTabWidth(2);
|
||||
m_sci->setIndentationsUseTabs(false);
|
||||
|
||||
// Caret color for dark theme
|
||||
m_sci->setCaretForegroundColor(QColor("#d4d4d4"));
|
||||
|
||||
// Line spacing for readability
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
|
||||
@@ -147,51 +140,37 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
||||
|
||||
// Hex/Padding node dim indicator — overrides text color to gray
|
||||
// Hex/Padding node dim indicator — overrides text color
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_HEX_DIM, QColor("#505050"));
|
||||
|
||||
// Base address indicator — default text color to override lexer green on command row
|
||||
// Base address indicator — text color override on command row
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_BASE_ADDR, QColor("#d4d4d4"));
|
||||
|
||||
// Hover span indicator — muted teal text (distinct from blue keywords)
|
||||
// Hover span indicator — link-like text
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_HOVER_SPAN, QColor("#E6B450"));
|
||||
|
||||
// Command-row pill background (shadcn-ish chip)
|
||||
// Command-row pill background
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_CMD_PILL, 8 /*INDIC_STRAIGHTBOX*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_CMD_PILL, QColor("#2a2a2a"));
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA,
|
||||
IND_CMD_PILL, (long)100);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||
IND_CMD_PILL, (long)1);
|
||||
|
||||
// Data-changed indicator — muted green text (derived from number green #b5cea8)
|
||||
// Data-changed indicator
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_DATA_CHANGED, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_DATA_CHANGED, QColor("#8fbc7a"));
|
||||
|
||||
// Root class name — teal (VS Code type color)
|
||||
// Root class name — type color
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_CLASS_NAME, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_CLASS_NAME, QColor("#4EC9B0"));
|
||||
|
||||
// Green text for hint/comment annotations
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_HINT_GREEN, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_HINT_GREEN, QColor("#5a8248"));
|
||||
|
||||
}
|
||||
|
||||
@@ -199,29 +178,8 @@ void RcxEditor::setupLexer() {
|
||||
m_lexer = new QsciLexerCPP(m_sci);
|
||||
QFont font = editorFont();
|
||||
m_lexer->setFont(font);
|
||||
|
||||
// Dark theme colors
|
||||
m_lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
|
||||
m_lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2);
|
||||
m_lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number);
|
||||
m_lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString);
|
||||
m_lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString);
|
||||
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment);
|
||||
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine);
|
||||
m_lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc);
|
||||
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default);
|
||||
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier);
|
||||
m_lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor);
|
||||
m_lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator);
|
||||
|
||||
// Dark background for all styles
|
||||
for (int i = 0; i <= 127; i++) {
|
||||
m_lexer->setPaper(kBgText, i);
|
||||
for (int i = 0; i <= 127; i++)
|
||||
m_lexer->setFont(font, i);
|
||||
}
|
||||
|
||||
// Custom / user-defined types → teal (VS Code #4EC9B0)
|
||||
m_lexer->setColor(QColor("#4EC9B0"), QsciLexerCPP::GlobalClass);
|
||||
|
||||
m_sci->setLexer(m_lexer);
|
||||
m_sci->setBraceMatching(QsciScintilla::NoBraceMatch); // Disable - this is a structured viewer
|
||||
@@ -245,8 +203,6 @@ void RcxEditor::setupMargins() {
|
||||
// Margin 0: Offset text
|
||||
m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified);
|
||||
m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument()
|
||||
m_sci->setMarginsBackgroundColor(kBgMargin);
|
||||
m_sci->setMarginsForegroundColor(kFgMarginDim);
|
||||
m_sci->setMarginSensitivity(0, true);
|
||||
|
||||
// Margin 1: hidden (fold chevrons moved to text column)
|
||||
@@ -256,7 +212,6 @@ void RcxEditor::setupMargins() {
|
||||
void RcxEditor::setupFolding() {
|
||||
// Hide fold margin (fold indicators are text-based now)
|
||||
m_sci->setMarginWidth(2, 0);
|
||||
m_sci->setFoldMarginColors(kBgMargin, kBgMargin);
|
||||
|
||||
// Fold indicators are now text in the line content (kFoldCol prefix),
|
||||
// so no Scintilla markers needed for fold state.
|
||||
@@ -281,37 +236,26 @@ void RcxEditor::setupMarkers() {
|
||||
// M_PAD (1): padding line (metadata only, no visual)
|
||||
m_sci->markerDefine(QsciScintilla::Invisible, M_PAD);
|
||||
|
||||
// M_PTR0 (2): right triangle (red)
|
||||
// M_PTR0 (2): right triangle
|
||||
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
|
||||
m_sci->setMarkerBackgroundColor(QColor("#f44747"), M_PTR0);
|
||||
m_sci->setMarkerForegroundColor(QColor("#f44747"), M_PTR0);
|
||||
|
||||
// M_CYCLE (3): arrows (orange)
|
||||
// M_CYCLE (3): arrows
|
||||
m_sci->markerDefine(QsciScintilla::ThreeRightArrows, M_CYCLE);
|
||||
m_sci->setMarkerBackgroundColor(QColor("#e5a00d"), M_CYCLE);
|
||||
m_sci->setMarkerForegroundColor(QColor("#e5a00d"), M_CYCLE);
|
||||
|
||||
// M_ERR (4): background (dark red - brightened for visibility)
|
||||
// M_ERR (4): background
|
||||
m_sci->markerDefine(QsciScintilla::Background, M_ERR);
|
||||
m_sci->setMarkerBackgroundColor(QColor("#7a2e2e"), M_ERR);
|
||||
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
|
||||
|
||||
// M_STRUCT_BG (5): struct header/footer (matches regular bg, may remove later)
|
||||
// M_STRUCT_BG (5): struct header/footer
|
||||
m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG);
|
||||
m_sci->setMarkerBackgroundColor(QColor("#1e1e1e"), M_STRUCT_BG);
|
||||
m_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG);
|
||||
|
||||
// M_HOVER (6): full-row hover highlight
|
||||
m_sci->markerDefine(QsciScintilla::Background, M_HOVER);
|
||||
m_sci->setMarkerBackgroundColor(QColor(43, 43, 43), M_HOVER);
|
||||
|
||||
// M_SELECTED (7): full-row selection highlight (higher = wins over hover)
|
||||
// M_SELECTED (7): full-row selection highlight
|
||||
m_sci->markerDefine(QsciScintilla::Background, M_SELECTED);
|
||||
m_sci->setMarkerBackgroundColor(QColor(35, 35, 35), M_SELECTED);
|
||||
|
||||
// M_CMD_ROW (8): distinct background for CommandRow bar
|
||||
m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW);
|
||||
m_sci->setMarkerBackgroundColor(QColor("#1e1e1e"), M_CMD_ROW);
|
||||
}
|
||||
|
||||
void RcxEditor::allocateMarginStyles() {
|
||||
@@ -322,21 +266,87 @@ void RcxEditor::allocateMarginStyles() {
|
||||
m_marginStyleBase = (int)base;
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base);
|
||||
|
||||
const long bgrMargin = 0x1e1e1e; // BGR for #1e1e1e (matches editor bg)
|
||||
QByteArray fontName = editorFont().family().toUtf8();
|
||||
int fontSize = editorFont().pointSize();
|
||||
|
||||
// Margin styles (dim gray text)
|
||||
for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) {
|
||||
unsigned long abs = (unsigned long)(base + s);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, abs, (long)0x505050);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK, abs, bgrMargin);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFONT,
|
||||
(uintptr_t)abs, fontName.constData());
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETSIZE, abs, (long)fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyTheme(const Theme& theme) {
|
||||
// Paper and text
|
||||
m_sci->setPaper(theme.background);
|
||||
m_sci->setColor(theme.text);
|
||||
m_sci->setCaretForegroundColor(theme.text);
|
||||
|
||||
// Indicator colors
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_HEX_DIM, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_BASE_ADDR, theme.text);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_HOVER_SPAN, theme.indHoverSpan);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_CMD_PILL, theme.indCmdPill);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_DATA_CHANGED, theme.indDataChanged);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_CLASS_NAME, theme.syntaxType);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_HINT_GREEN, theme.indHintGreen);
|
||||
|
||||
// Lexer colors
|
||||
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
||||
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::KeywordSet2);
|
||||
m_lexer->setColor(theme.syntaxNumber, QsciLexerCPP::Number);
|
||||
m_lexer->setColor(theme.syntaxString, QsciLexerCPP::DoubleQuotedString);
|
||||
m_lexer->setColor(theme.syntaxString, QsciLexerCPP::SingleQuotedString);
|
||||
m_lexer->setColor(theme.syntaxComment, QsciLexerCPP::Comment);
|
||||
m_lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentLine);
|
||||
m_lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentDoc);
|
||||
m_lexer->setColor(theme.text, QsciLexerCPP::Default);
|
||||
m_lexer->setColor(theme.text, QsciLexerCPP::Identifier);
|
||||
m_lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor);
|
||||
m_lexer->setColor(theme.text, QsciLexerCPP::Operator);
|
||||
m_lexer->setColor(theme.syntaxType, QsciLexerCPP::GlobalClass);
|
||||
for (int i = 0; i <= 127; i++)
|
||||
m_lexer->setPaper(theme.background, i);
|
||||
|
||||
// Margins
|
||||
m_sci->setMarginsBackgroundColor(theme.background);
|
||||
m_sci->setMarginsForegroundColor(theme.textFaint);
|
||||
m_sci->setFoldMarginColors(theme.background, theme.background);
|
||||
|
||||
// Markers
|
||||
m_sci->setMarkerBackgroundColor(theme.markerPtr, M_PTR0);
|
||||
m_sci->setMarkerForegroundColor(theme.markerPtr, M_PTR0);
|
||||
m_sci->setMarkerBackgroundColor(theme.markerCycle, M_CYCLE);
|
||||
m_sci->setMarkerForegroundColor(theme.markerCycle, M_CYCLE);
|
||||
m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR);
|
||||
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_STRUCT_BG);
|
||||
m_sci->setMarkerForegroundColor(theme.text, M_STRUCT_BG);
|
||||
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
|
||||
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW);
|
||||
|
||||
// Margin extended styles
|
||||
if (m_marginStyleBase >= 0) {
|
||||
long base = m_marginStyleBase;
|
||||
for (int s = 0; s <= 1; s++) {
|
||||
unsigned long abs = (unsigned long)(base + s);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
|
||||
abs, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
|
||||
abs, theme.background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
// Silently deactivate inline edit (no signal — refresh is already happening)
|
||||
if (m_editState.active)
|
||||
@@ -1561,7 +1571,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||
if (!isPicker)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
|
||||
QColor("#264f78"));
|
||||
ThemeManager::instance().current().selection);
|
||||
|
||||
// Use correct UTF-8 position conversion (not lineStart + col!)
|
||||
m_editState.posStart = posFromCol(m_sci, line, norm.start);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
@@ -47,6 +48,7 @@ public:
|
||||
void setCommandRowText(const QString& line);
|
||||
void setEditorFont(const QString& fontName);
|
||||
static void setGlobalFontName(const QString& fontName);
|
||||
void applyTheme(const Theme& theme);
|
||||
|
||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||
QString textWithMargins() const;
|
||||
|
||||
273
src/main.cpp
273
src/main.cpp
@@ -41,6 +41,8 @@
|
||||
#include <Qsci/qscilexercpp.h>
|
||||
#include <QProxyStyle>
|
||||
#include <QDesktopServices>
|
||||
#include "themes/thememanager.h"
|
||||
#include "themes/themeeditor.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -128,6 +130,44 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.background);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||
pal.setColor(QPalette::Mid, theme.border);
|
||||
pal.setColor(QPalette::Dark, theme.background);
|
||||
pal.setColor(QPalette::Light, theme.textFaint);
|
||||
qApp->setPalette(pal);
|
||||
|
||||
qApp->setStyleSheet(QStringLiteral(
|
||||
"QMenu {"
|
||||
" background-color: %1;"
|
||||
" color: %2;"
|
||||
" border: 1px solid %3;"
|
||||
" padding: 4px 6px;"
|
||||
"}"
|
||||
"QMenu::item { padding: 4px 24px; }"
|
||||
"QMenu::item:selected { background-color: %4; }"
|
||||
"QMenu::separator { height: 1px; background: %3; margin: 4px 8px; }"
|
||||
"QMenu::item:disabled { color: %5; }"
|
||||
"QToolTip {"
|
||||
" background-color: %1;"
|
||||
" color: %2;"
|
||||
" border: 1px solid %3;"
|
||||
"}")
|
||||
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name(),
|
||||
theme.hover.name(), theme.textMuted.name()));
|
||||
}
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
@@ -158,6 +198,7 @@ private slots:
|
||||
void setEditorFont(const QString& fontName);
|
||||
void exportCpp();
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
|
||||
public:
|
||||
// Project Lifecycle API
|
||||
@@ -208,6 +249,7 @@ private:
|
||||
void setupRenderedSci(QsciScintilla* sci);
|
||||
|
||||
SplitPane createSplitPane(TabState& tab);
|
||||
void applyTheme(const Theme& theme);
|
||||
void applyTabWidgetStyle(QTabWidget* tw);
|
||||
SplitPane* findPaneByTabWidget(QTabWidget* tw);
|
||||
SplitPane* findActiveSplitPane();
|
||||
@@ -229,22 +271,17 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
m_mdiArea->setViewMode(QMdiArea::TabbedView);
|
||||
m_mdiArea->setTabsClosable(true);
|
||||
m_mdiArea->setTabsMovable(true);
|
||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||
"QTabBar::tab {"
|
||||
" background: #1e1e1e;"
|
||||
" color: #585858;"
|
||||
" padding: 6px 16px;"
|
||||
" border: none;"
|
||||
"}"
|
||||
"QTabBar::tab:selected {"
|
||||
" color: #d4d4d4;"
|
||||
" background: #252526;"
|
||||
"}"
|
||||
"QTabBar::tab:hover {"
|
||||
" color: #d4d4d4;"
|
||||
" background: #2b2b2b;"
|
||||
"}"
|
||||
));
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 6px 16px; border: none;"
|
||||
"}"
|
||||
"QTabBar::tab:selected { color: %3; background: %4; }"
|
||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||
.arg(t.background.name(), t.textMuted.name(), t.text.name(),
|
||||
t.backgroundAlt.name(), t.hover.name()));
|
||||
}
|
||||
setCentralWidget(m_mdiArea);
|
||||
|
||||
createWorkspaceDock();
|
||||
@@ -257,6 +294,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
menuBar()->setStyle(new MenuBarStyle(menuBar()->style()));
|
||||
}
|
||||
|
||||
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
||||
this, &MainWindow::applyTheme);
|
||||
|
||||
// Load plugins
|
||||
m_pluginManager.LoadPlugins();
|
||||
|
||||
@@ -327,6 +367,24 @@ void MainWindow::createMenus() {
|
||||
connect(actConsolas, &QAction::triggered, this, [this]() { setEditorFont("Consolas"); });
|
||||
connect(actJetBrains, &QAction::triggered, this, [this]() { setEditorFont("JetBrains Mono"); });
|
||||
|
||||
// Theme submenu
|
||||
auto* themeMenu = view->addMenu("&Theme");
|
||||
auto* themeGroup = new QActionGroup(this);
|
||||
themeGroup->setExclusive(true);
|
||||
auto& tm = ThemeManager::instance();
|
||||
auto allThemes = tm.themes();
|
||||
for (int i = 0; i < allThemes.size(); i++) {
|
||||
auto* act = themeMenu->addAction(allThemes[i].name);
|
||||
act->setCheckable(true);
|
||||
act->setActionGroup(themeGroup);
|
||||
if (i == tm.currentIndex()) act->setChecked(true);
|
||||
connect(act, &QAction::triggered, this, [i]() {
|
||||
ThemeManager::instance().setCurrent(i);
|
||||
});
|
||||
}
|
||||
themeMenu->addSeparator();
|
||||
themeMenu->addAction("Edit Theme...", this, &MainWindow::editTheme);
|
||||
|
||||
view->addSeparator();
|
||||
view->addAction(m_workspaceDock->toggleViewAction());
|
||||
|
||||
@@ -351,7 +409,12 @@ void MainWindow::createStatusBar() {
|
||||
m_statusLabel = new QLabel("Ready");
|
||||
m_statusLabel->setContentsMargins(10, 0, 0, 0);
|
||||
statusBar()->addWidget(m_statusLabel, 1);
|
||||
statusBar()->setStyleSheet("QStatusBar { background: #252526; color: #858585; }");
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
statusBar()->setStyleSheet(QStringLiteral(
|
||||
"QStatusBar { background: %1; color: %2; }")
|
||||
.arg(t.backgroundAlt.name(), t.textDim.name()));
|
||||
}
|
||||
|
||||
QSettings settings("ReclassX", "ReclassX");
|
||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||
@@ -366,23 +429,16 @@ void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
|
||||
QFont tabFont(fontName, 12);
|
||||
tabFont.setFixedPitch(true);
|
||||
tw->tabBar()->setFont(tabFont);
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
tw->setStyleSheet(QStringLiteral(
|
||||
"QTabWidget::pane { border: none; }"
|
||||
"QTabBar::tab {"
|
||||
" background: #1e1e1e;"
|
||||
" color: #585858;"
|
||||
" padding: 4px 12px;"
|
||||
" border: none;"
|
||||
" min-width: 60px;"
|
||||
" background: %1; color: %2; padding: 4px 12px; border: none; min-width: 60px;"
|
||||
"}"
|
||||
"QTabBar::tab:selected {"
|
||||
" color: #d4d4d4;"
|
||||
"}"
|
||||
"QTabBar::tab:hover {"
|
||||
" color: #d4d4d4;"
|
||||
" background: #2b2b2b;"
|
||||
"}"
|
||||
));
|
||||
"QTabBar::tab:selected { color: %3; }"
|
||||
"QTabBar::tab:hover { color: %3; background: %4; }")
|
||||
.arg(t.background.name(), t.textMuted.name(),
|
||||
t.text.name(), t.hover.name()));
|
||||
tw->tabBar()->setExpanding(false);
|
||||
}
|
||||
|
||||
@@ -754,28 +810,74 @@ void MainWindow::about() {
|
||||
lay->setSpacing(12);
|
||||
|
||||
auto* buildLabel = new QLabel(
|
||||
QStringLiteral("<span style='color:#858585;font-size:11px;'>"
|
||||
"Build " __DATE__ " " __TIME__ "</span>"));
|
||||
QStringLiteral("<span style='color:%1;font-size:11px;'>"
|
||||
"Build " __DATE__ " " __TIME__ "</span>")
|
||||
.arg(ThemeManager::instance().current().textDim.name()));
|
||||
buildLabel->setAlignment(Qt::AlignCenter);
|
||||
lay->addWidget(buildLabel);
|
||||
|
||||
auto* ghBtn = new QPushButton("GitHub");
|
||||
ghBtn->setCursor(Qt::PointingHandCursor);
|
||||
ghBtn->setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #2a2a2a; color: #d4d4d4; border: 1px solid #3f3f3f;"
|
||||
" border-radius: 4px; padding: 5px 16px; font-size: 12px;"
|
||||
"}"
|
||||
"QPushButton:hover { background: #333333; border-color: #505050; }");
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
ghBtn->setStyleSheet(QStringLiteral(
|
||||
"QPushButton {"
|
||||
" background: %1; color: %2; border: 1px solid %3;"
|
||||
" border-radius: 4px; padding: 5px 16px; font-size: 12px;"
|
||||
"}"
|
||||
"QPushButton:hover { background: %4; border-color: %5; }")
|
||||
.arg(t.indCmdPill.name(), t.text.name(), t.border.name(),
|
||||
t.button.name(), t.textFaint.name()));
|
||||
}
|
||||
connect(ghBtn, &QPushButton::clicked, this, []() {
|
||||
QDesktopServices::openUrl(QUrl("https://github.com/IChooseYou/Reclass"));
|
||||
});
|
||||
lay->addWidget(ghBtn, 0, Qt::AlignCenter);
|
||||
|
||||
dlg.setStyleSheet("QDialog { background: #1e1e1e; }");
|
||||
dlg.setStyleSheet(QStringLiteral("QDialog { background: %1; }")
|
||||
.arg(ThemeManager::instance().current().background.name()));
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
void MainWindow::applyTheme(const Theme& theme) {
|
||||
applyGlobalTheme(theme);
|
||||
|
||||
// MDI area tabs
|
||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 6px 16px; border: none;"
|
||||
"}"
|
||||
"QTabBar::tab:selected { color: %3; background: %4; }"
|
||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
||||
theme.backgroundAlt.name(), theme.hover.name()));
|
||||
|
||||
// Status bar
|
||||
statusBar()->setStyleSheet(QStringLiteral(
|
||||
"QStatusBar { background: %1; color: %2; }")
|
||||
.arg(theme.backgroundAlt.name(), theme.textDim.name()));
|
||||
|
||||
// Split pane tab widgets
|
||||
for (auto& state : m_tabs) {
|
||||
for (auto& pane : state.panes) {
|
||||
if (pane.tabWidget) applyTabWidgetStyle(pane.tabWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::editTheme() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
Theme edited = tm.current();
|
||||
ThemeEditor dlg(edited, this);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
edited = dlg.result();
|
||||
int idx = tm.currentIndex();
|
||||
if (idx < tm.themes().size() && idx >= 0) {
|
||||
tm.updateTheme(idx, edited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setEditorFont(const QString& fontName) {
|
||||
QSettings settings("ReclassX", "ReclassX");
|
||||
settings.setValue("font", fontName);
|
||||
@@ -852,8 +954,9 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
// Line number margin
|
||||
sci->setMarginType(0, QsciScintilla::NumberMargin);
|
||||
sci->setMarginWidth(0, "00000");
|
||||
sci->setMarginsBackgroundColor(QColor("#252526"));
|
||||
sci->setMarginsForegroundColor(QColor("#858585"));
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
sci->setMarginsBackgroundColor(theme.backgroundAlt);
|
||||
sci->setMarginsForegroundColor(theme.textDim);
|
||||
sci->setMarginsFont(f);
|
||||
|
||||
// Hide other margins
|
||||
@@ -864,33 +967,33 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
// because setLexer() resets caret line, selection, and paper colors.
|
||||
auto* lexer = new QsciLexerCPP(sci);
|
||||
lexer->setFont(f);
|
||||
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
|
||||
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2);
|
||||
lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number);
|
||||
lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString);
|
||||
lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString);
|
||||
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment);
|
||||
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine);
|
||||
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc);
|
||||
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default);
|
||||
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier);
|
||||
lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor);
|
||||
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator);
|
||||
lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
||||
lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::KeywordSet2);
|
||||
lexer->setColor(theme.syntaxNumber, QsciLexerCPP::Number);
|
||||
lexer->setColor(theme.syntaxString, QsciLexerCPP::DoubleQuotedString);
|
||||
lexer->setColor(theme.syntaxString, QsciLexerCPP::SingleQuotedString);
|
||||
lexer->setColor(theme.syntaxComment, QsciLexerCPP::Comment);
|
||||
lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentLine);
|
||||
lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentDoc);
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Default);
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Identifier);
|
||||
lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor);
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Operator);
|
||||
for (int i = 0; i <= 127; i++) {
|
||||
lexer->setPaper(QColor("#1e1e1e"), i);
|
||||
lexer->setPaper(theme.background, i);
|
||||
lexer->setFont(f, i);
|
||||
}
|
||||
sci->setLexer(lexer);
|
||||
sci->setBraceMatching(QsciScintilla::NoBraceMatch);
|
||||
|
||||
// Colors applied AFTER setLexer() — the lexer resets these on attach
|
||||
sci->setPaper(QColor("#1e1e1e"));
|
||||
sci->setColor(QColor("#d4d4d4"));
|
||||
sci->setCaretForegroundColor(QColor("#d4d4d4"));
|
||||
sci->setPaper(theme.background);
|
||||
sci->setColor(theme.text);
|
||||
sci->setCaretForegroundColor(theme.text);
|
||||
sci->setCaretLineVisible(true);
|
||||
sci->setCaretLineBackgroundColor(QColor(43, 43, 43)); // Match Reclass M_HOVER
|
||||
sci->setSelectionBackgroundColor(QColor("#264f78")); // Match Reclass edit selection
|
||||
sci->setSelectionForegroundColor(QColor("#d4d4d4"));
|
||||
sci->setCaretLineBackgroundColor(theme.hover);
|
||||
sci->setSelectionBackgroundColor(theme.selection);
|
||||
sci->setSelectionForegroundColor(theme.text);
|
||||
}
|
||||
|
||||
// ── View mode / generator switching ──
|
||||
@@ -1333,54 +1436,8 @@ int main(int argc, char* argv[]) {
|
||||
rcx::RcxEditor::setGlobalFontName(savedFont);
|
||||
}
|
||||
|
||||
// Global dark palette
|
||||
QPalette darkPalette;
|
||||
darkPalette.setColor(QPalette::Window, QColor("#1e1e1e"));
|
||||
darkPalette.setColor(QPalette::WindowText, QColor("#d4d4d4"));
|
||||
darkPalette.setColor(QPalette::Base, QColor("#252526"));
|
||||
darkPalette.setColor(QPalette::AlternateBase, QColor("#2a2d2e"));
|
||||
darkPalette.setColor(QPalette::Text, QColor("#d4d4d4"));
|
||||
darkPalette.setColor(QPalette::Button, QColor("#333333"));
|
||||
darkPalette.setColor(QPalette::ButtonText, QColor("#d4d4d4"));
|
||||
darkPalette.setColor(QPalette::Highlight, QColor("#2b2b2b"));
|
||||
darkPalette.setColor(QPalette::HighlightedText, QColor("#d4d4d4"));
|
||||
darkPalette.setColor(QPalette::ToolTipBase, QColor("#252526"));
|
||||
darkPalette.setColor(QPalette::ToolTipText, QColor("#d4d4d4"));
|
||||
darkPalette.setColor(QPalette::Mid, QColor("#3c3c3c"));
|
||||
darkPalette.setColor(QPalette::Dark, QColor("#1e1e1e"));
|
||||
darkPalette.setColor(QPalette::Light, QColor("#505050"));
|
||||
app.setPalette(darkPalette);
|
||||
|
||||
// ── Global widget styling ──
|
||||
// QMenu: grey hover, amber accent border (replaces Fusion outline artifact)
|
||||
// QToolTip: dark theme
|
||||
app.setStyleSheet(QStringLiteral(
|
||||
"QMenu {"
|
||||
" background-color: #252526;"
|
||||
" color: #d4d4d4;"
|
||||
" border: 1px solid #3c3c3c;"
|
||||
" padding: 4px 6px;"
|
||||
"}"
|
||||
"QMenu::item {"
|
||||
" padding: 4px 24px;"
|
||||
"}"
|
||||
"QMenu::item:selected {"
|
||||
" background-color: #2b2b2b;"
|
||||
"}"
|
||||
"QMenu::separator {"
|
||||
" height: 1px;"
|
||||
" background: #3c3c3c;"
|
||||
" margin: 4px 8px;"
|
||||
"}"
|
||||
"QMenu::item:disabled {"
|
||||
" color: #585858;"
|
||||
"}"
|
||||
"QToolTip {"
|
||||
" background-color: #252526;"
|
||||
" color: #d4d4d4;"
|
||||
" border: 1px solid #3c3c3c;"
|
||||
"}"
|
||||
));
|
||||
// Global theme
|
||||
applyGlobalTheme(rcx::ThemeManager::instance().current());
|
||||
|
||||
rcx::MainWindow window;
|
||||
|
||||
|
||||
119
src/themes/theme.cpp
Normal file
119
src/themes/theme.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "theme.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Field table for DRY serialization ──
|
||||
|
||||
struct ColorField { const char* key; QColor Theme::*ptr; };
|
||||
|
||||
static const ColorField kFields[] = {
|
||||
{"background", &Theme::background},
|
||||
{"backgroundAlt", &Theme::backgroundAlt},
|
||||
{"surface", &Theme::surface},
|
||||
{"border", &Theme::border},
|
||||
{"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},
|
||||
};
|
||||
|
||||
QJsonObject Theme::toJson() const {
|
||||
QJsonObject o;
|
||||
o["name"] = name;
|
||||
for (const auto& f : kFields)
|
||||
o[f.key] = (this->*f.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());
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// ── Built-in themes ──
|
||||
|
||||
Theme Theme::reclassDark() {
|
||||
Theme t;
|
||||
t.name = "Reclass Dark";
|
||||
t.background = QColor("#1e1e1e");
|
||||
t.backgroundAlt = QColor("#252526");
|
||||
t.surface = QColor("#2a2d2e");
|
||||
t.border = QColor("#3c3c3c");
|
||||
t.button = QColor("#333333");
|
||||
t.text = QColor("#d4d4d4");
|
||||
t.textDim = QColor("#858585");
|
||||
t.textMuted = QColor("#585858");
|
||||
t.textFaint = QColor("#505050");
|
||||
t.hover = QColor("#2b2b2b");
|
||||
t.selected = QColor("#232323");
|
||||
t.selection = QColor("#2b2b2b");
|
||||
t.syntaxKeyword = QColor("#569cd6");
|
||||
t.syntaxNumber = QColor("#b5cea8");
|
||||
t.syntaxString = QColor("#ce9178");
|
||||
t.syntaxComment = QColor("#6a9955");
|
||||
t.syntaxPreproc = QColor("#c586c0");
|
||||
t.syntaxType = QColor("#4EC9B0");
|
||||
t.indHoverSpan = QColor("#E6B450");
|
||||
t.indCmdPill = QColor("#2a2a2a");
|
||||
t.indDataChanged= QColor("#8fbc7a");
|
||||
t.indHintGreen = QColor("#5a8248");
|
||||
t.markerPtr = QColor("#f44747");
|
||||
t.markerCycle = QColor("#e5a00d");
|
||||
t.markerError = QColor("#7a2e2e");
|
||||
return t;
|
||||
}
|
||||
|
||||
Theme Theme::warm() {
|
||||
Theme t;
|
||||
t.name = "Warm";
|
||||
t.background = QColor("#212121");
|
||||
t.backgroundAlt = QColor("#2a2a2a");
|
||||
t.surface = QColor("#2a2a2a");
|
||||
t.border = QColor("#373737");
|
||||
t.button = QColor("#373737");
|
||||
t.text = QColor("#AAA99F");
|
||||
t.textDim = QColor("#7a7a6e");
|
||||
t.textMuted = QColor("#555550");
|
||||
t.textFaint = QColor("#464646");
|
||||
t.hover = QColor("#373737");
|
||||
t.selected = QColor("#2d2d2d");
|
||||
t.selection = QColor("#21213A");
|
||||
t.syntaxKeyword = QColor("#AA9565");
|
||||
t.syntaxNumber = QColor("#AAA98C");
|
||||
t.syntaxString = QColor("#6B3B21");
|
||||
t.syntaxComment = QColor("#464646");
|
||||
t.syntaxPreproc = QColor("#AA9565");
|
||||
t.syntaxType = QColor("#6B959F");
|
||||
t.indHoverSpan = QColor("#AA9565");
|
||||
t.indCmdPill = QColor("#2a2a2a");
|
||||
t.indDataChanged= QColor("#6B959F");
|
||||
t.indHintGreen = QColor("#464646");
|
||||
t.markerPtr = QColor("#6B3B21");
|
||||
t.markerCycle = QColor("#AA9565");
|
||||
t.markerError = QColor("#3C2121");
|
||||
return t;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
55
src/themes/theme.h
Normal file
55
src/themes/theme.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct Theme {
|
||||
QString name;
|
||||
|
||||
// ── Chrome ──
|
||||
QColor background; // editor bg, margin bg, window
|
||||
QColor backgroundAlt; // panels, tab selected, tooltips
|
||||
QColor surface; // alternateBase
|
||||
QColor border; // separators, menu borders
|
||||
QColor button; // button bg
|
||||
|
||||
// ── Text ──
|
||||
QColor text; // primary text, caret, identifiers
|
||||
QColor textDim; // margin fg, status bar
|
||||
QColor textMuted; // inactive tab, disabled menu
|
||||
QColor textFaint; // margin dim, hex dim
|
||||
|
||||
// ── Interactive ──
|
||||
QColor hover; // row hover, tab hover, menu hover
|
||||
QColor selected; // row selection highlight
|
||||
QColor selection; // text selection background
|
||||
|
||||
// ── Syntax ──
|
||||
QColor syntaxKeyword;
|
||||
QColor syntaxNumber;
|
||||
QColor syntaxString;
|
||||
QColor syntaxComment;
|
||||
QColor syntaxPreproc;
|
||||
QColor syntaxType; // custom types / GlobalClass
|
||||
|
||||
// ── Indicators ──
|
||||
QColor indHoverSpan; // hover link text
|
||||
QColor indCmdPill; // command row pill bg
|
||||
QColor indDataChanged; // changed data values
|
||||
QColor indHintGreen; // comment/hint text
|
||||
|
||||
// ── Markers ──
|
||||
QColor markerPtr; // null pointer
|
||||
QColor markerCycle; // cycle detection
|
||||
QColor markerError; // error row bg
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static Theme fromJson(const QJsonObject& obj);
|
||||
|
||||
static Theme reclassDark();
|
||||
static Theme warm();
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
95
src/themes/themeeditor.cpp
Normal file
95
src/themes/themeeditor.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "themeeditor.h"
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QColorDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
ThemeEditor::ThemeEditor(const Theme& theme, QWidget* parent)
|
||||
: QDialog(parent), m_theme(theme)
|
||||
{
|
||||
setWindowTitle("Edit Theme");
|
||||
setMinimumWidth(320);
|
||||
|
||||
auto* form = new QFormLayout;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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},
|
||||
};
|
||||
|
||||
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());
|
||||
|
||||
int idx = m_swatches.size() - 1;
|
||||
connect(btn, &QPushButton::clicked, this, [this, idx]() {
|
||||
pickColor(m_swatches[idx]);
|
||||
});
|
||||
form->addRow(f.label, btn);
|
||||
}
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(buttons);
|
||||
}
|
||||
|
||||
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; }")
|
||||
.arg(c.name()));
|
||||
entry.button->setToolTip(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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
29
src/themes/themeeditor.h
Normal file
29
src/themes/themeeditor.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "theme.h"
|
||||
#include <QDialog>
|
||||
#include <QVector>
|
||||
#include <QPushButton>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class ThemeEditor : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ThemeEditor(const Theme& theme, QWidget* parent = nullptr);
|
||||
Theme result() const { return m_theme; }
|
||||
|
||||
private:
|
||||
Theme m_theme;
|
||||
|
||||
struct SwatchEntry {
|
||||
const char* label;
|
||||
QColor Theme::*field;
|
||||
QPushButton* button;
|
||||
};
|
||||
QVector<SwatchEntry> m_swatches;
|
||||
|
||||
void updateSwatch(SwatchEntry& entry);
|
||||
void pickColor(SwatchEntry& entry);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
119
src/themes/thememanager.cpp
Normal file
119
src/themes/thememanager.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "thememanager.h"
|
||||
#include <QSettings>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
ThemeManager& ThemeManager::instance() {
|
||||
static ThemeManager s;
|
||||
return s;
|
||||
}
|
||||
|
||||
ThemeManager::ThemeManager() {
|
||||
m_builtIn.append(Theme::reclassDark());
|
||||
m_builtIn.append(Theme::warm());
|
||||
loadUserThemes();
|
||||
|
||||
QSettings settings("ReclassX", "ReclassX");
|
||||
QString saved = settings.value("theme", m_builtIn[0].name).toString();
|
||||
auto all = themes();
|
||||
for (int i = 0; i < all.size(); i++) {
|
||||
if (all[i].name == saved) { m_currentIdx = i; break; }
|
||||
}
|
||||
}
|
||||
|
||||
QVector<Theme> ThemeManager::themes() const {
|
||||
QVector<Theme> all = m_builtIn;
|
||||
all.append(m_user);
|
||||
return all;
|
||||
}
|
||||
|
||||
const Theme& ThemeManager::current() const {
|
||||
if (m_currentIdx < m_builtIn.size())
|
||||
return m_builtIn[m_currentIdx];
|
||||
int userIdx = m_currentIdx - m_builtIn.size();
|
||||
if (userIdx >= 0 && userIdx < m_user.size())
|
||||
return m_user[userIdx];
|
||||
return m_builtIn[0];
|
||||
}
|
||||
|
||||
void ThemeManager::setCurrent(int index) {
|
||||
auto all = themes();
|
||||
if (index < 0 || index >= all.size()) return;
|
||||
m_currentIdx = index;
|
||||
QSettings settings("ReclassX", "ReclassX");
|
||||
settings.setValue("theme", all[index].name);
|
||||
emit themeChanged(current());
|
||||
}
|
||||
|
||||
void ThemeManager::addTheme(const Theme& theme) {
|
||||
m_user.append(theme);
|
||||
saveUserThemes();
|
||||
}
|
||||
|
||||
void ThemeManager::updateTheme(int index, const Theme& theme) {
|
||||
if (index < builtInCount()) {
|
||||
// Can't overwrite built-in; save as user theme instead
|
||||
m_user.append(theme);
|
||||
} else {
|
||||
int ui = index - builtInCount();
|
||||
if (ui >= 0 && ui < m_user.size())
|
||||
m_user[ui] = theme;
|
||||
}
|
||||
saveUserThemes();
|
||||
if (index == m_currentIdx)
|
||||
emit themeChanged(current());
|
||||
}
|
||||
|
||||
void ThemeManager::removeTheme(int index) {
|
||||
if (index < builtInCount()) return;
|
||||
int ui = index - builtInCount();
|
||||
if (ui < 0 || ui >= m_user.size()) return;
|
||||
m_user.remove(ui);
|
||||
if (m_currentIdx == index) {
|
||||
m_currentIdx = 0;
|
||||
emit themeChanged(current());
|
||||
} else if (m_currentIdx > index) {
|
||||
m_currentIdx--;
|
||||
}
|
||||
saveUserThemes();
|
||||
}
|
||||
|
||||
QString ThemeManager::themesDir() const {
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
||||
+ "/themes";
|
||||
QDir().mkpath(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
void ThemeManager::loadUserThemes() {
|
||||
m_user.clear();
|
||||
QDir dir(themesDir());
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
void ThemeManager::saveUserThemes() const {
|
||||
QString dir = themesDir();
|
||||
// Remove old files
|
||||
QDir d(dir);
|
||||
for (const QString& name : d.entryList({"*.json"}, QDir::Files))
|
||||
d.remove(name);
|
||||
// Write current 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));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
38
src/themes/thememanager.h
Normal file
38
src/themes/thememanager.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "theme.h"
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class ThemeManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static ThemeManager& instance();
|
||||
|
||||
QVector<Theme> themes() const;
|
||||
int currentIndex() const { return m_currentIdx; }
|
||||
const Theme& current() const;
|
||||
|
||||
void setCurrent(int index);
|
||||
void addTheme(const Theme& theme);
|
||||
void updateTheme(int index, const Theme& theme);
|
||||
void removeTheme(int index);
|
||||
|
||||
void loadUserThemes();
|
||||
void saveUserThemes() const;
|
||||
|
||||
signals:
|
||||
void themeChanged(const rcx::Theme& theme);
|
||||
|
||||
private:
|
||||
ThemeManager();
|
||||
QVector<Theme> m_builtIn;
|
||||
QVector<Theme> m_user;
|
||||
int m_currentIdx = 0;
|
||||
|
||||
int builtInCount() const { return m_builtIn.size(); }
|
||||
QString themesDir() const;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QIcon>
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -34,11 +35,12 @@ public:
|
||||
const QModelIndex& index) const override {
|
||||
painter->save();
|
||||
|
||||
// Background: editor design language greys
|
||||
// Background: themed colors
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
if (option.state & QStyle::State_Selected)
|
||||
painter->fillRect(option.rect, QColor("#232323")); // M_SELECTED
|
||||
painter->fillRect(option.rect, t.selected);
|
||||
else if (option.state & QStyle::State_MouseOver)
|
||||
painter->fillRect(option.rect, QColor("#2b2b2b")); // M_HOVER
|
||||
painter->fillRect(option.rect, t.hover);
|
||||
|
||||
int x = option.rect.x();
|
||||
int y = option.rect.y();
|
||||
@@ -48,7 +50,7 @@ public:
|
||||
int row = index.row();
|
||||
if (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
&& (*m_filtered)[row].id == m_currentId) {
|
||||
painter->setPen(QColor("#4ec9b0"));
|
||||
painter->setPen(t.syntaxType);
|
||||
QFont checkFont = m_font;
|
||||
painter->setFont(checkFont);
|
||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
||||
@@ -93,17 +95,18 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
|
||||
// Dark palette (no CSS)
|
||||
// Theme palette
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, QColor("#252526"));
|
||||
pal.setColor(QPalette::WindowText, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Base, QColor("#1e1e1e"));
|
||||
pal.setColor(QPalette::AlternateBase, QColor("#2a2d2e"));
|
||||
pal.setColor(QPalette::Text, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Button, QColor("#333333"));
|
||||
pal.setColor(QPalette::ButtonText, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Highlight, QColor("#2b2b2b"));
|
||||
pal.setColor(QPalette::HighlightedText, QColor("#d4d4d4"));
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.background);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
setPalette(pal);
|
||||
setAutoFillBackground(true);
|
||||
|
||||
@@ -130,7 +133,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
|
||||
m_escLabel = new QLabel(QStringLiteral("Esc"));
|
||||
QPalette dimPal = pal;
|
||||
dimPal.setColor(QPalette::WindowText, QColor("#858585"));
|
||||
dimPal.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_escLabel->setPalette(dimPal);
|
||||
row->addWidget(m_escLabel);
|
||||
|
||||
@@ -159,7 +162,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
QPalette sepPal = pal;
|
||||
sepPal.setColor(QPalette::WindowText, QColor("#3c3c3c"));
|
||||
sepPal.setColor(QPalette::WindowText, theme.border);
|
||||
sep->setPalette(sepPal);
|
||||
sep->setFixedHeight(1);
|
||||
layout->addWidget(sep);
|
||||
|
||||
132
tests/test_theme.cpp
Normal file
132
tests/test_theme.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QtTest/QSignalSpy>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include "themes/theme.h"
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestTheme : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void builtInThemes() {
|
||||
Theme dark = Theme::reclassDark();
|
||||
QCOMPARE(dark.name, "Reclass Dark");
|
||||
QVERIFY(dark.background.isValid());
|
||||
QVERIFY(dark.text.isValid());
|
||||
QVERIFY(dark.syntaxKeyword.isValid());
|
||||
QVERIFY(dark.markerError.isValid());
|
||||
|
||||
Theme warm = Theme::warm();
|
||||
QCOMPARE(warm.name, "Warm");
|
||||
QVERIFY(warm.background.isValid());
|
||||
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 selectionColorFixed() {
|
||||
Theme dark = Theme::reclassDark();
|
||||
QCOMPARE(dark.selection, QColor("#2b2b2b"));
|
||||
QVERIFY(dark.selection != QColor("#264f78"));
|
||||
}
|
||||
|
||||
void jsonRoundTrip() {
|
||||
Theme orig = Theme::reclassDark();
|
||||
QJsonObject json = orig.toJson();
|
||||
Theme loaded = Theme::fromJson(json);
|
||||
|
||||
QCOMPARE(loaded.name, orig.name);
|
||||
QCOMPARE(loaded.background, orig.background);
|
||||
QCOMPARE(loaded.text, orig.text);
|
||||
QCOMPARE(loaded.selection, orig.selection);
|
||||
QCOMPARE(loaded.syntaxKeyword, orig.syntaxKeyword);
|
||||
QCOMPARE(loaded.syntaxNumber, orig.syntaxNumber);
|
||||
QCOMPARE(loaded.syntaxString, orig.syntaxString);
|
||||
QCOMPARE(loaded.syntaxComment, orig.syntaxComment);
|
||||
QCOMPARE(loaded.syntaxType, orig.syntaxType);
|
||||
QCOMPARE(loaded.markerPtr, orig.markerPtr);
|
||||
QCOMPARE(loaded.markerError, orig.markerError);
|
||||
QCOMPARE(loaded.indHoverSpan, orig.indHoverSpan);
|
||||
}
|
||||
|
||||
void jsonRoundTripWarm() {
|
||||
Theme orig = Theme::warm();
|
||||
QJsonObject json = orig.toJson();
|
||||
Theme loaded = Theme::fromJson(json);
|
||||
|
||||
QCOMPARE(loaded.name, orig.name);
|
||||
QCOMPARE(loaded.background, orig.background);
|
||||
QCOMPARE(loaded.selection, orig.selection);
|
||||
QCOMPARE(loaded.syntaxKeyword, orig.syntaxKeyword);
|
||||
}
|
||||
|
||||
void fromJsonMissingFields() {
|
||||
QJsonObject sparse;
|
||||
sparse["name"] = "Sparse";
|
||||
sparse["background"] = "#ff0000";
|
||||
Theme t = Theme::fromJson(sparse);
|
||||
|
||||
QCOMPARE(t.name, "Sparse");
|
||||
QCOMPARE(t.background, QColor("#ff0000"));
|
||||
// Missing fields fall back to reclassDark defaults
|
||||
Theme defaults = Theme::reclassDark();
|
||||
QCOMPARE(t.text, defaults.text);
|
||||
QCOMPARE(t.syntaxKeyword, defaults.syntaxKeyword);
|
||||
QCOMPARE(t.markerError, defaults.markerError);
|
||||
}
|
||||
|
||||
void themeManagerHasBuiltIns() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
auto all = tm.themes();
|
||||
QVERIFY(all.size() >= 2);
|
||||
QCOMPARE(all[0].name, "Reclass Dark");
|
||||
QCOMPARE(all[1].name, "Warm");
|
||||
}
|
||||
|
||||
void themeManagerSwitch() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
QSignalSpy spy(&tm, &ThemeManager::themeChanged);
|
||||
|
||||
int startIdx = tm.currentIndex();
|
||||
int target = (startIdx == 0) ? 1 : 0;
|
||||
tm.setCurrent(target);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(tm.currentIndex(), target);
|
||||
QCOMPARE(tm.current().name, tm.themes()[target].name);
|
||||
|
||||
// Restore
|
||||
tm.setCurrent(startIdx);
|
||||
}
|
||||
|
||||
void themeManagerCRUD() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
int initialCount = tm.themes().size();
|
||||
|
||||
// Add
|
||||
Theme custom = Theme::reclassDark();
|
||||
custom.name = "Test Custom";
|
||||
custom.background = QColor("#ff0000");
|
||||
tm.addTheme(custom);
|
||||
QCOMPARE(tm.themes().size(), initialCount + 1);
|
||||
QCOMPARE(tm.themes().last().name, "Test Custom");
|
||||
|
||||
// Update
|
||||
int idx = tm.themes().size() - 1;
|
||||
Theme updated = custom;
|
||||
updated.background = QColor("#00ff00");
|
||||
tm.updateTheme(idx, updated);
|
||||
QCOMPARE(tm.themes()[idx].background, QColor("#00ff00"));
|
||||
|
||||
// Remove
|
||||
tm.removeTheme(idx);
|
||||
QCOMPARE(tm.themes().size(), initialCount);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTheme)
|
||||
#include "test_theme.moc"
|
||||
Reference in New Issue
Block a user