diff --git a/CMakeLists.txt b/CMakeLists.txt index 92336a4..5443f7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(ReclassX VERSION 0.1 LANGUAGES CXX) +project(Reclass VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -30,7 +30,7 @@ endif() find_package(QScintilla REQUIRED) -add_executable(ReclassX +add_executable(Reclass src/main.cpp src/editor.h src/editor.cpp @@ -64,9 +64,9 @@ add_executable(ReclassX src/mcp/mcp_bridge.cpp ) -target_include_directories(ReclassX PRIVATE src) +target_include_directories(Reclass PRIVATE src) -target_link_libraries(ReclassX PRIVATE +target_link_libraries(Reclass PRIVATE ${QT}::Widgets ${QT}::PrintSupport ${QT}::Svg @@ -76,7 +76,7 @@ target_link_libraries(ReclassX PRIVATE ${_QT_WINEXTRAS} ) if(WIN32) - target_link_libraries(ReclassX PRIVATE dbghelp dwmapi psapi) + target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi) endif() add_executable(rcx-mcp-stdio tools/rcx-mcp-stdio.cpp) @@ -85,8 +85,8 @@ target_link_libraries(rcx-mcp-stdio PRIVATE ${QT}::Core ${QT}::Network) include(deploy) add_custom_target(screenshot ALL - COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png - DEPENDS ReclassX deploy + COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png + DEPENDS Reclass deploy WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Capturing UI screenshot with class open..." ) @@ -114,7 +114,7 @@ message(STATUS \"Combined sources -> \${_out}\") add_custom_target(combined ALL COMMAND ${CMAKE_COMMAND} -P ${_combine_script} - DEPENDS ReclassX + DEPENDS Reclass COMMENT "Combining all source files into h_cpp_combined.txt" ) diff --git a/README.md b/README.md index 3897d01..e89cf6d 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ This tool helps you inspect raw bytes and interpret them as types (structs, arra 2. Quick Build (relies on powershell| for manual build skip to step 3) - git clone --recurse-submodules https://github.com/IChooseYou/ReclassX.git - cd ReclassX + git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git + cd Reclass .\scripts\build_qscintilla.ps1 .\scripts\build.ps1 ^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/) diff --git a/cmake/deploy.cmake b/cmake/deploy.cmake index 7dad1c0..1ff8dc6 100644 --- a/cmake/deploy.cmake +++ b/cmake/deploy.cmake @@ -69,14 +69,14 @@ endif() if(TARGET ${QT}::windeployqt) add_custom_target(deploy COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake - $ + $ $ - DEPENDS ReclassX + DEPENDS Reclass COMMENT "Deploying Qt runtime DLLs..." ) # Force re-deploy on rebuild set_target_properties(deploy PROPERTIES - ADDITIONAL_CLEAN_FILES $/.qt_deployed + ADDITIONAL_CLEAN_FILES $/.qt_deployed ) endif() diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h index 8c2ab1e..5a5fa74 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -64,7 +64,7 @@ class ProcessMemoryPlugin : public IProviderPlugin public: std::string Name() const override { return "Process Memory"; } std::string Version() const override { return "1.0.0"; } - std::string Author() const override { return "ReclassX"; } + std::string Author() const override { return "Reclass"; } std::string Description() const override { return "Read and write memory from local running processes"; } k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } QIcon Icon() const override; diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 8c2b955..cd6872b 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,4 +1,4 @@ -# PowerShell script to build ReclassX +# PowerShell script to build Reclass # Automatically detects Qt installation and configures build environment #Requires -Version 5.1 @@ -303,7 +303,7 @@ function Find-MinGWDirectory { # ────────────────────────────────────────────────────────────────────────────── Write-ColorOutput "`n========================================" Cyan -Write-ColorOutput "ReclassX Build Script" Cyan +Write-ColorOutput "Reclass Build Script" Cyan Write-ColorOutput "========================================`n" Cyan # Get script directory and project root @@ -426,7 +426,7 @@ try { Write-ColorOutput "`nCMake configuration completed successfully.`n" Green # Build - Write-ColorOutput "Building ReclassX..." Cyan + Write-ColorOutput "Building Reclass..." Cyan $cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors if (-not $cores -or $cores -lt 1) { @@ -445,8 +445,8 @@ try { # Find executable Write-ColorOutput "Locating executable..." Cyan $exePaths = @( - (Join-Path $buildDir "ReclassX.exe"), - (Join-Path $buildDir "$BuildType\ReclassX.exe") + (Join-Path $buildDir "Reclass.exe"), + (Join-Path $buildDir "$BuildType\Reclass.exe") ) $exePath = $null @@ -477,7 +477,7 @@ try { # Count deployed files $deployedFiles = Get-ChildItem -Path $exeDir -Recurse -File | Where-Object { - $_.Name -ne "ReclassX.exe" -and $_.Extension -match '\.(dll|qm)$' + $_.Name -ne "Reclass.exe" -and $_.Extension -match '\.(dll|qm)$' } if ($deployedFiles) { Write-ColorOutput "Deployed $($deployedFiles.Count) Qt dependency files." Gray @@ -491,7 +491,7 @@ try { Write-ColorOutput "Application may not run without Qt DLLs in PATH" Yellow } } else { - Write-ColorOutput "WARNING: Could not locate ReclassX.exe" Yellow + Write-ColorOutput "WARNING: Could not locate Reclass.exe" Yellow } } catch { @@ -507,5 +507,5 @@ Write-ColorOutput "========================================`n" Cyan if ($exePath) { Write-ColorOutput "Run the application with:" White - Write-ColorOutput " .\build\ReclassX.exe`n" Cyan + Write-ColorOutput " .\build\Reclass.exe`n" Cyan } diff --git a/scripts/build_qscintilla.ps1 b/scripts/build_qscintilla.ps1 index 89fe0d4..38dc4e3 100644 --- a/scripts/build_qscintilla.ps1 +++ b/scripts/build_qscintilla.ps1 @@ -1,4 +1,4 @@ -# PowerShell script to build QScintilla static library for ReclassX +# PowerShell script to build QScintilla static library for Reclass # This script checks for Qt installation, prompts if missing, and builds QScintilla #Requires -Version 5.1 @@ -272,7 +272,7 @@ function Find-MakeCommand { # ────────────────────────────────────────────────────────────────────────────── Write-ColorOutput "`n========================================" Cyan -Write-ColorOutput "QScintilla Build Script for ReclassX" Cyan +Write-ColorOutput "QScintilla Build Script for Reclass" Cyan Write-ColorOutput "========================================`n" Cyan # Get script directory and project root @@ -423,7 +423,7 @@ try { Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green Write-Host " Path: $($lib.Path)" -ForegroundColor Gray } - Write-ColorOutput "`nYou can now build ReclassX with CMake." Green + Write-ColorOutput "`nYou can now build Reclass with CMake." Green } else { Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow diff --git a/src/controller.cpp b/src/controller.cpp index 092a92c..a9c0e85 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -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); diff --git a/src/core.h b/src/core.h index 5ecd74d..059526e 100644 --- a/src/core.h +++ b/src/core.h @@ -163,6 +163,7 @@ enum Marker : int { M_HOVER = 6, M_SELECTED = 7, M_CMD_ROW = 8, + M_ACCENT = 9, }; // ── Node ── diff --git a/src/editor.cpp b/src/editor.cpp index c68ec7d..9974268 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -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& meta) { void RcxEditor::applySelectionOverlay(const QSet& 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& 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; diff --git a/src/editor.h b/src/editor.h index 8450f98..6d8a1ac 100644 --- a/src/editor.h +++ b/src/editor.h @@ -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 diff --git a/src/iplugin.h b/src/iplugin.h index fca3b20..3dac1aa 100644 --- a/src/iplugin.h +++ b/src/iplugin.h @@ -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" diff --git a/src/main.cpp b/src/main.cpp index b7a986f..eb58f2a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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); } diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index 4b91a56..a13c7ad 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -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"} }} }; diff --git a/src/themes/themeeditor.cpp b/src/themes/themeeditor.cpp index 3637e1e..4b0ce52 100644 --- a/src/themes/themeeditor.cpp +++ b/src/themes/themeeditor.cpp @@ -1,95 +1,442 @@ #include "themeeditor.h" -#include +#include "thememanager.h" +#include +#include +#include #include #include -#include -#include +#include +#include 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::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 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 diff --git a/src/themes/themeeditor.h b/src/themes/themeeditor.h index 98f2ca9..b60d243 100644 --- a/src/themes/themeeditor.h +++ b/src/themes/themeeditor.h @@ -3,27 +3,61 @@ #include #include #include +#include +#include + +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 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 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 diff --git a/src/themes/thememanager.cpp b/src/themes/thememanager.cpp index c493eba..bdf2c78 100644 --- a/src/themes/thememanager.cpp +++ b/src/themes/thememanager.cpp @@ -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 diff --git a/src/themes/thememanager.h b/src/themes/thememanager.h index 3050fa6..b7f0891 100644 --- a/src/themes/thememanager.h +++ b/src/themes/thememanager.h @@ -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 diff --git a/tools/rcx-mcp-stdio.cpp b/tools/rcx-mcp-stdio.cpp index 85993e2..26d1c52 100644 --- a/tools/rcx-mcp-stdio.cpp +++ b/tools/rcx-mcp-stdio.cpp @@ -1,9 +1,9 @@ // rcx-mcp-stdio: Bridges stdin/stdout to QLocalSocket for MCP transport. // Claude Desktop spawns this process; it connects to the rcx-mcp named pipe -// inside the running ReclassX application. +// inside the running Reclass application. // -// stdin (from Claude) → QLocalSocket → McpBridge (in ReclassX) -// stdout (to Claude) ← QLocalSocket ← McpBridge (in ReclassX) +// stdin (from Claude) → QLocalSocket → McpBridge (in Reclass) +// stdout (to Claude) ← QLocalSocket ← McpBridge (in Reclass) #include #include @@ -29,7 +29,7 @@ int main(int argc, char* argv[]) { auto* socket = new QLocalSocket(&app); QByteArray readBuf; - // Socket → stdout: forward lines from ReclassX to Claude Desktop + // Socket → stdout: forward lines from Reclass to Claude Desktop QObject::connect(socket, &QLocalSocket::readyRead, [&]() { readBuf.append(socket->readAll()); while (true) {