mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Theme preview/revert, theme editor enhancements, build and deploy updates
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -30,7 +30,7 @@ endif()
|
|||||||
|
|
||||||
find_package(QScintilla REQUIRED)
|
find_package(QScintilla REQUIRED)
|
||||||
|
|
||||||
add_executable(ReclassX
|
add_executable(Reclass
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/editor.h
|
src/editor.h
|
||||||
src/editor.cpp
|
src/editor.cpp
|
||||||
@@ -64,9 +64,9 @@ add_executable(ReclassX
|
|||||||
src/mcp/mcp_bridge.cpp
|
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}::Widgets
|
||||||
${QT}::PrintSupport
|
${QT}::PrintSupport
|
||||||
${QT}::Svg
|
${QT}::Svg
|
||||||
@@ -76,7 +76,7 @@ target_link_libraries(ReclassX PRIVATE
|
|||||||
${_QT_WINEXTRAS}
|
${_QT_WINEXTRAS}
|
||||||
)
|
)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(ReclassX PRIVATE dbghelp dwmapi psapi)
|
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(rcx-mcp-stdio tools/rcx-mcp-stdio.cpp)
|
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)
|
include(deploy)
|
||||||
|
|
||||||
add_custom_target(screenshot ALL
|
add_custom_target(screenshot ALL
|
||||||
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||||
DEPENDS ReclassX deploy
|
DEPENDS Reclass deploy
|
||||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
COMMENT "Capturing UI screenshot with class open..."
|
COMMENT "Capturing UI screenshot with class open..."
|
||||||
)
|
)
|
||||||
@@ -114,7 +114,7 @@ message(STATUS \"Combined sources -> \${_out}\")
|
|||||||
|
|
||||||
add_custom_target(combined ALL
|
add_custom_target(combined ALL
|
||||||
COMMAND ${CMAKE_COMMAND} -P ${_combine_script}
|
COMMAND ${CMAKE_COMMAND} -P ${_combine_script}
|
||||||
DEPENDS ReclassX
|
DEPENDS Reclass
|
||||||
COMMENT "Combining all source files into h_cpp_combined.txt"
|
COMMENT "Combining all source files into h_cpp_combined.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
2. Quick Build (relies on powershell| for manual build skip to step 3)
|
||||||
|
|
||||||
git clone --recurse-submodules https://github.com/IChooseYou/ReclassX.git
|
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||||
cd ReclassX
|
cd Reclass
|
||||||
.\scripts\build_qscintilla.ps1
|
.\scripts\build_qscintilla.ps1
|
||||||
.\scripts\build.ps1
|
.\scripts\build.ps1
|
||||||
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
|
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
|
||||||
|
|||||||
@@ -69,14 +69,14 @@ endif()
|
|||||||
if(TARGET ${QT}::windeployqt)
|
if(TARGET ${QT}::windeployqt)
|
||||||
add_custom_target(deploy
|
add_custom_target(deploy
|
||||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||||
$<TARGET_FILE:ReclassX>
|
$<TARGET_FILE:Reclass>
|
||||||
$<TARGET_FILE:${QT}::windeployqt>
|
$<TARGET_FILE:${QT}::windeployqt>
|
||||||
DEPENDS ReclassX
|
DEPENDS Reclass
|
||||||
COMMENT "Deploying Qt runtime DLLs..."
|
COMMENT "Deploying Qt runtime DLLs..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Force re-deploy on rebuild
|
# Force re-deploy on rebuild
|
||||||
set_target_properties(deploy PROPERTIES
|
set_target_properties(deploy PROPERTIES
|
||||||
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:ReclassX>/.qt_deployed
|
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:Reclass>/.qt_deployed
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ProcessMemoryPlugin : public IProviderPlugin
|
|||||||
public:
|
public:
|
||||||
std::string Name() const override { return "Process Memory"; }
|
std::string Name() const override { return "Process Memory"; }
|
||||||
std::string Version() const override { return "1.0.0"; }
|
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"; }
|
std::string Description() const override { return "Read and write memory from local running processes"; }
|
||||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||||
QIcon Icon() const override;
|
QIcon Icon() const override;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# PowerShell script to build ReclassX
|
# PowerShell script to build Reclass
|
||||||
# Automatically detects Qt installation and configures build environment
|
# Automatically detects Qt installation and configures build environment
|
||||||
|
|
||||||
#Requires -Version 5.1
|
#Requires -Version 5.1
|
||||||
@@ -303,7 +303,7 @@ function Find-MinGWDirectory {
|
|||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Write-ColorOutput "`n========================================" Cyan
|
Write-ColorOutput "`n========================================" Cyan
|
||||||
Write-ColorOutput "ReclassX Build Script" Cyan
|
Write-ColorOutput "Reclass Build Script" Cyan
|
||||||
Write-ColorOutput "========================================`n" Cyan
|
Write-ColorOutput "========================================`n" Cyan
|
||||||
|
|
||||||
# Get script directory and project root
|
# Get script directory and project root
|
||||||
@@ -426,7 +426,7 @@ try {
|
|||||||
Write-ColorOutput "`nCMake configuration completed successfully.`n" Green
|
Write-ColorOutput "`nCMake configuration completed successfully.`n" Green
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
Write-ColorOutput "Building ReclassX..." Cyan
|
Write-ColorOutput "Building Reclass..." Cyan
|
||||||
|
|
||||||
$cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
|
$cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
|
||||||
if (-not $cores -or $cores -lt 1) {
|
if (-not $cores -or $cores -lt 1) {
|
||||||
@@ -445,8 +445,8 @@ try {
|
|||||||
# Find executable
|
# Find executable
|
||||||
Write-ColorOutput "Locating executable..." Cyan
|
Write-ColorOutput "Locating executable..." Cyan
|
||||||
$exePaths = @(
|
$exePaths = @(
|
||||||
(Join-Path $buildDir "ReclassX.exe"),
|
(Join-Path $buildDir "Reclass.exe"),
|
||||||
(Join-Path $buildDir "$BuildType\ReclassX.exe")
|
(Join-Path $buildDir "$BuildType\Reclass.exe")
|
||||||
)
|
)
|
||||||
|
|
||||||
$exePath = $null
|
$exePath = $null
|
||||||
@@ -477,7 +477,7 @@ try {
|
|||||||
|
|
||||||
# Count deployed files
|
# Count deployed files
|
||||||
$deployedFiles = Get-ChildItem -Path $exeDir -Recurse -File | Where-Object {
|
$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) {
|
if ($deployedFiles) {
|
||||||
Write-ColorOutput "Deployed $($deployedFiles.Count) Qt dependency files." Gray
|
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
|
Write-ColorOutput "Application may not run without Qt DLLs in PATH" Yellow
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-ColorOutput "WARNING: Could not locate ReclassX.exe" Yellow
|
Write-ColorOutput "WARNING: Could not locate Reclass.exe" Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
@@ -507,5 +507,5 @@ Write-ColorOutput "========================================`n" Cyan
|
|||||||
|
|
||||||
if ($exePath) {
|
if ($exePath) {
|
||||||
Write-ColorOutput "Run the application with:" White
|
Write-ColorOutput "Run the application with:" White
|
||||||
Write-ColorOutput " .\build\ReclassX.exe`n" Cyan
|
Write-ColorOutput " .\build\Reclass.exe`n" Cyan
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
# This script checks for Qt installation, prompts if missing, and builds QScintilla
|
||||||
|
|
||||||
#Requires -Version 5.1
|
#Requires -Version 5.1
|
||||||
@@ -272,7 +272,7 @@ function Find-MakeCommand {
|
|||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Write-ColorOutput "`n========================================" Cyan
|
Write-ColorOutput "`n========================================" Cyan
|
||||||
Write-ColorOutput "QScintilla Build Script for ReclassX" Cyan
|
Write-ColorOutput "QScintilla Build Script for Reclass" Cyan
|
||||||
Write-ColorOutput "========================================`n" Cyan
|
Write-ColorOutput "========================================`n" Cyan
|
||||||
|
|
||||||
# Get script directory and project root
|
# Get script directory and project root
|
||||||
@@ -423,7 +423,7 @@ try {
|
|||||||
Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green
|
Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green
|
||||||
Write-Host " Path: $($lib.Path)" -ForegroundColor Gray
|
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 {
|
} else {
|
||||||
Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow
|
Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow
|
||||||
Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow
|
Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow
|
||||||
|
|||||||
@@ -1622,7 +1622,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Font with zoom ──
|
// ── Font with zoom ──
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||||
QFont font(fontName, 12);
|
QFont font(fontName, 12);
|
||||||
font.setFixedPitch(true);
|
font.setFixedPitch(true);
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ enum Marker : int {
|
|||||||
M_HOVER = 6,
|
M_HOVER = 6,
|
||||||
M_SELECTED = 7,
|
M_SELECTED = 7,
|
||||||
M_CMD_ROW = 8,
|
M_CMD_ROW = 8,
|
||||||
|
M_ACCENT = 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Node ──
|
// ── Node ──
|
||||||
|
|||||||
@@ -201,8 +201,11 @@ void RcxEditor::setupMargins() {
|
|||||||
m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument()
|
m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument()
|
||||||
m_sci->setMarginSensitivity(0, true);
|
m_sci->setMarginSensitivity(0, true);
|
||||||
|
|
||||||
// Margin 1: hidden (fold chevrons moved to text column)
|
// Margin 1: 2px accent bar (selection indicator)
|
||||||
m_sci->setMarginWidth(1, 0);
|
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() {
|
void RcxEditor::setupFolding() {
|
||||||
@@ -252,6 +255,9 @@ void RcxEditor::setupMarkers() {
|
|||||||
|
|
||||||
// M_CMD_ROW (8): distinct background for CommandRow bar
|
// M_CMD_ROW (8): distinct background for CommandRow bar
|
||||||
m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW);
|
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() {
|
void RcxEditor::allocateMarginStyles() {
|
||||||
@@ -329,6 +335,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
|
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
|
||||||
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
|
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
|
||||||
m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW);
|
m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW);
|
||||||
|
m_sci->setMarkerBackgroundColor(theme.indHoverSpan, M_ACCENT);
|
||||||
|
|
||||||
// Margin extended styles
|
// Margin extended styles
|
||||||
if (m_marginStyleBase >= 0) {
|
if (m_marginStyleBase >= 0) {
|
||||||
@@ -493,6 +500,7 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
|||||||
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||||
m_currentSelIds = selIds;
|
m_currentSelIds = selIds;
|
||||||
m_sci->markerDeleteAll(M_SELECTED);
|
m_sci->markerDeleteAll(M_SELECTED);
|
||||||
|
m_sci->markerDeleteAll(M_ACCENT);
|
||||||
|
|
||||||
// Clear all editable indicators, then repaint for selected lines only
|
// Clear all editable indicators, then repaint for selected lines only
|
||||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||||
@@ -508,6 +516,7 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
|
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
|
||||||
if (selIds.contains(checkId)) {
|
if (selIds.contains(checkId)) {
|
||||||
m_sci->markerAdd(i, M_SELECTED);
|
m_sci->markerAdd(i, M_SELECTED);
|
||||||
|
m_sci->markerAdd(i, M_ACCENT);
|
||||||
if (!isFooter)
|
if (!isFooter)
|
||||||
paintEditableSpans(i);
|
paintEditableSpans(i);
|
||||||
}
|
}
|
||||||
@@ -1477,6 +1486,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
}
|
}
|
||||||
auto* lm = metaForLine(line);
|
auto* lm = metaForLine(line);
|
||||||
if (!lm) return false;
|
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
|
// Position popup at the type column start
|
||||||
ColumnSpan ts = typeSpan(*lm);
|
ColumnSpan ts = typeSpan(*lm);
|
||||||
long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0);
|
long typePos = posFromCol(m_sci, line, ts.valid ? ts.start : 0);
|
||||||
@@ -2234,6 +2246,10 @@ void RcxEditor::setGlobalFontName(const QString& fontName) {
|
|||||||
g_fontName = fontName;
|
g_fontName = fontName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString RcxEditor::globalFontName() {
|
||||||
|
return g_fontName;
|
||||||
|
}
|
||||||
|
|
||||||
QString RcxEditor::textWithMargins() const {
|
QString RcxEditor::textWithMargins() const {
|
||||||
int lineCount = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINECOUNT);
|
int lineCount = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINECOUNT);
|
||||||
QStringList lines;
|
QStringList lines;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public:
|
|||||||
void setCommandRowText(const QString& line);
|
void setCommandRowText(const QString& line);
|
||||||
void setEditorFont(const QString& fontName);
|
void setEditorFont(const QString& fontName);
|
||||||
static void setGlobalFontName(const QString& fontName);
|
static void setGlobalFontName(const QString& fontName);
|
||||||
|
static QString globalFontName();
|
||||||
void applyTheme(const Theme& theme);
|
void applyTheme(const Theme& theme);
|
||||||
|
|
||||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
namespace rcx { class Provider; }
|
namespace rcx { class Provider; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin interface for ReclassX
|
* Plugin interface for Reclass
|
||||||
*
|
*
|
||||||
* Plugins are loaded from the "Plugins" folder as shared libraries.
|
* Plugins are loaded from the "Plugins" folder as shared libraries.
|
||||||
* Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
* Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||||
@@ -133,4 +133,4 @@ public:
|
|||||||
// Plugin factory function signature
|
// Plugin factory function signature
|
||||||
typedef IPlugin* (*CreatePluginFunc)();
|
typedef IPlugin* (*CreatePluginFunc)();
|
||||||
|
|
||||||
#define IPLUGIN_IID "com.reclassx.IPlugin/1.0"
|
#define IPLUGIN_IID "com.reclass.IPlugin/1.0"
|
||||||
|
|||||||
22
src/main.cpp
22
src/main.cpp
@@ -334,7 +334,7 @@ void MainWindow::createMenus() {
|
|||||||
actJetBrains->setCheckable(true);
|
actJetBrains->setCheckable(true);
|
||||||
actJetBrains->setActionGroup(fontGroup);
|
actJetBrains->setActionGroup(fontGroup);
|
||||||
// Load saved preference
|
// Load saved preference
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString savedFont = settings.value("font", "JetBrains Mono").toString();
|
QString savedFont = settings.value("font", "JetBrains Mono").toString();
|
||||||
if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true);
|
if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true);
|
||||||
else actConsolas->setChecked(true);
|
else actConsolas->setChecked(true);
|
||||||
@@ -392,7 +392,7 @@ void MainWindow::createStatusBar() {
|
|||||||
statusBar()->setAutoFillBackground(true);
|
statusBar()->setAutoFillBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||||
QFont f(fontName, 12);
|
QFont f(fontName, 12);
|
||||||
f.setFixedPitch(true);
|
f.setFixedPitch(true);
|
||||||
@@ -400,7 +400,7 @@ void MainWindow::createStatusBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
|
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||||
QFont tabFont(fontName, 12);
|
QFont tabFont(fontName, 12);
|
||||||
tabFont.setFixedPitch(true);
|
tabFont.setFixedPitch(true);
|
||||||
@@ -889,7 +889,7 @@ void MainWindow::editTheme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setEditorFont(const QString& fontName) {
|
void MainWindow::setEditorFont(const QString& fontName) {
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
settings.setValue("font", fontName);
|
settings.setValue("font", fontName);
|
||||||
QFont f(fontName, 12);
|
QFont f(fontName, 12);
|
||||||
f.setFixedPitch(true);
|
f.setFixedPitch(true);
|
||||||
@@ -959,7 +959,7 @@ void MainWindow::updateWindowTitle() {
|
|||||||
// ── Rendered view setup ──
|
// ── Rendered view setup ──
|
||||||
|
|
||||||
void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||||
QFont f(fontName, 12);
|
QFont f(fontName, 12);
|
||||||
f.setFixedPitch(true);
|
f.setFixedPitch(true);
|
||||||
@@ -1204,7 +1204,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
QString filePath = path;
|
QString filePath = path;
|
||||||
if (filePath.isEmpty()) {
|
if (filePath.isEmpty()) {
|
||||||
filePath = QFileDialog::getOpenFileName(this,
|
filePath = QFileDialog::getOpenFileName(this,
|
||||||
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
|
"Open Definition", {}, "Reclass (*.rcx);;JSON (*.json);;All (*)");
|
||||||
if (filePath.isEmpty()) return nullptr;
|
if (filePath.isEmpty()) return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,7 +1226,7 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
|
|||||||
|
|
||||||
if (saveAs || tab.doc->filePath.isEmpty()) {
|
if (saveAs || tab.doc->filePath.isEmpty()) {
|
||||||
QString path = QFileDialog::getSaveFileName(this,
|
QString path = QFileDialog::getSaveFileName(this,
|
||||||
"Save Definition", {}, "ReclassX (*.rcx);;JSON (*.json)");
|
"Save Definition", {}, "Reclass (*.rcx);;JSON (*.json)");
|
||||||
if (path.isEmpty()) return false;
|
if (path.isEmpty()) return false;
|
||||||
tab.doc->save(path);
|
tab.doc->save(path);
|
||||||
} else {
|
} else {
|
||||||
@@ -1259,7 +1259,7 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
|
|
||||||
// Match editor font
|
// Match editor font
|
||||||
{
|
{
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||||
QFont f(fontName, 12);
|
QFont f(fontName, 12);
|
||||||
f.setFixedPitch(true);
|
f.setFixedPitch(true);
|
||||||
@@ -1442,8 +1442,8 @@ int main(int argc, char* argv[]) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
DarkApp app(argc, argv);
|
DarkApp app(argc, argv);
|
||||||
app.setApplicationName("ReclassX");
|
app.setApplicationName("Reclass");
|
||||||
app.setOrganizationName("ReclassX");
|
app.setOrganizationName("Reclass");
|
||||||
app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing
|
app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing
|
||||||
|
|
||||||
// Load embedded fonts
|
// Load embedded fonts
|
||||||
@@ -1452,7 +1452,7 @@ int main(int argc, char* argv[]) {
|
|||||||
qWarning("Failed to load embedded JetBrains Mono font");
|
qWarning("Failed to load embedded JetBrains Mono font");
|
||||||
// Apply saved font preference before creating any editors
|
// Apply saved font preference before creating any editors
|
||||||
{
|
{
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString savedFont = settings.value("font", "JetBrains Mono").toString();
|
QString savedFont = settings.value("font", "JetBrains Mono").toString();
|
||||||
rcx::RcxEditor::setGlobalFontName(savedFont);
|
rcx::RcxEditor::setGlobalFontName(savedFont);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&
|
|||||||
{"protocolVersion", "2024-11-05"},
|
{"protocolVersion", "2024-11-05"},
|
||||||
{"capabilities", caps},
|
{"capabilities", caps},
|
||||||
{"serverInfo", QJsonObject{
|
{"serverInfo", QJsonObject{
|
||||||
{"name", "reclassx-mcp"},
|
{"name", "reclass-mcp"},
|
||||||
{"version", "1.0.0"}
|
{"version", "1.0.0"}
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,95 +1,442 @@
|
|||||||
#include "themeeditor.h"
|
#include "themeeditor.h"
|
||||||
#include <QFormLayout>
|
#include "thememanager.h"
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QScrollArea>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
#include <QLineEdit>
|
#include <QComboBox>
|
||||||
#include <QLabel>
|
#include <cmath>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
ThemeEditor::ThemeEditor(const Theme& theme, QWidget* parent)
|
// ── Color utilities ──
|
||||||
: QDialog(parent), m_theme(theme)
|
|
||||||
|
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");
|
auto& tm = ThemeManager::instance();
|
||||||
setMinimumWidth(320);
|
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* mainLayout = new QVBoxLayout(this);
|
||||||
auto* nameEdit = new QLineEdit(m_theme.name);
|
mainLayout->setSpacing(6);
|
||||||
connect(nameEdit, &QLineEdit::textChanged, this, [this](const QString& t) {
|
|
||||||
|
// ── Theme selector combo ──
|
||||||
|
{
|
||||||
|
auto* row = new QHBoxLayout;
|
||||||
|
row->addWidget(new QLabel(QStringLiteral("Theme:")));
|
||||||
|
m_themeCombo = new QComboBox;
|
||||||
|
for (const auto& t : all)
|
||||||
|
m_themeCombo->addItem(t.name);
|
||||||
|
m_themeCombo->setCurrentIndex(themeIndex);
|
||||||
|
connect(m_themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
|
this, [this](int idx) { loadTheme(idx); });
|
||||||
|
row->addWidget(m_themeCombo, 1);
|
||||||
|
mainLayout->addLayout(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Name field ──
|
||||||
|
{
|
||||||
|
auto* row = new QHBoxLayout;
|
||||||
|
row->addWidget(new QLabel(QStringLiteral("Name:")));
|
||||||
|
m_nameEdit = new QLineEdit(m_theme.name);
|
||||||
|
connect(m_nameEdit, &QLineEdit::textChanged, this, [this](const QString& t) {
|
||||||
m_theme.name = t;
|
m_theme.name = t;
|
||||||
});
|
});
|
||||||
form->addRow("Name", nameEdit);
|
row->addWidget(m_nameEdit, 1);
|
||||||
|
mainLayout->addLayout(row);
|
||||||
|
}
|
||||||
|
|
||||||
// Color swatches
|
// ── 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; };
|
struct FieldDef { const char* label; QColor Theme::*ptr; };
|
||||||
const FieldDef fields[] = {
|
|
||||||
|
auto addGroup = [&](const QString& title, std::initializer_list<FieldDef> fields) {
|
||||||
|
scrollLayout->addWidget(makeSectionLabel(title));
|
||||||
|
for (const auto& f : fields) {
|
||||||
|
int idx = m_swatches.size();
|
||||||
|
|
||||||
|
auto* row = new QHBoxLayout;
|
||||||
|
row->setSpacing(6);
|
||||||
|
row->setContentsMargins(8, 1, 0, 1);
|
||||||
|
|
||||||
|
auto* lbl = new QLabel(QString::fromLatin1(f.label));
|
||||||
|
lbl->setFixedWidth(120);
|
||||||
|
row->addWidget(lbl);
|
||||||
|
|
||||||
|
auto* swatchBtn = new QPushButton;
|
||||||
|
swatchBtn->setFixedSize(32, 18);
|
||||||
|
swatchBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
connect(swatchBtn, &QPushButton::clicked, this, [this, idx]() { pickColor(idx); });
|
||||||
|
row->addWidget(swatchBtn);
|
||||||
|
|
||||||
|
auto* hexLbl = new QLabel;
|
||||||
|
hexLbl->setFixedWidth(60);
|
||||||
|
hexLbl->setStyleSheet(QStringLiteral("color: #aaa; font-size: 10px;"));
|
||||||
|
row->addWidget(hexLbl);
|
||||||
|
|
||||||
|
row->addStretch();
|
||||||
|
|
||||||
|
SwatchEntry se;
|
||||||
|
se.label = f.label;
|
||||||
|
se.field = f.ptr;
|
||||||
|
se.swatchBtn = swatchBtn;
|
||||||
|
se.hexLabel = hexLbl;
|
||||||
|
m_swatches.append(se);
|
||||||
|
|
||||||
|
scrollLayout->addLayout(row);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addGroup("Chrome", {
|
||||||
{"Background", &Theme::background},
|
{"Background", &Theme::background},
|
||||||
{"Background Alt", &Theme::backgroundAlt},
|
{"Background Alt", &Theme::backgroundAlt},
|
||||||
{"Surface", &Theme::surface},
|
{"Surface", &Theme::surface},
|
||||||
{"Border", &Theme::border},
|
{"Border", &Theme::border},
|
||||||
{"Button", &Theme::button},
|
{"Button", &Theme::button},
|
||||||
|
});
|
||||||
|
addGroup("Text", {
|
||||||
{"Text", &Theme::text},
|
{"Text", &Theme::text},
|
||||||
{"Text Dim", &Theme::textDim},
|
{"Text Dim", &Theme::textDim},
|
||||||
{"Text Muted", &Theme::textMuted},
|
{"Text Muted", &Theme::textMuted},
|
||||||
{"Text Faint", &Theme::textFaint},
|
{"Text Faint", &Theme::textFaint},
|
||||||
|
});
|
||||||
|
addGroup("Interactive", {
|
||||||
{"Hover", &Theme::hover},
|
{"Hover", &Theme::hover},
|
||||||
{"Selected", &Theme::selected},
|
{"Selected", &Theme::selected},
|
||||||
{"Selection", &Theme::selection},
|
{"Selection", &Theme::selection},
|
||||||
|
});
|
||||||
|
addGroup("Syntax", {
|
||||||
{"Keyword", &Theme::syntaxKeyword},
|
{"Keyword", &Theme::syntaxKeyword},
|
||||||
{"Number", &Theme::syntaxNumber},
|
{"Number", &Theme::syntaxNumber},
|
||||||
{"String", &Theme::syntaxString},
|
{"String", &Theme::syntaxString},
|
||||||
{"Comment", &Theme::syntaxComment},
|
{"Comment", &Theme::syntaxComment},
|
||||||
{"Preprocessor", &Theme::syntaxPreproc},
|
{"Preprocessor", &Theme::syntaxPreproc},
|
||||||
{"Type", &Theme::syntaxType},
|
{"Type", &Theme::syntaxType},
|
||||||
|
});
|
||||||
|
addGroup("Indicators", {
|
||||||
{"Hover Span", &Theme::indHoverSpan},
|
{"Hover Span", &Theme::indHoverSpan},
|
||||||
{"Cmd Pill", &Theme::indCmdPill},
|
{"Cmd Pill", &Theme::indCmdPill},
|
||||||
{"Data Changed", &Theme::indDataChanged},
|
{"Data Changed", &Theme::indDataChanged},
|
||||||
{"Hint Green", &Theme::indHintGreen},
|
{"Hint Green", &Theme::indHintGreen},
|
||||||
{"Pointer Marker", &Theme::markerPtr},
|
});
|
||||||
{"Cycle Marker", &Theme::markerCycle},
|
addGroup("Markers", {
|
||||||
{"Error Marker", &Theme::markerError},
|
{"Pointer", &Theme::markerPtr},
|
||||||
|
{"Cycle", &Theme::markerCycle},
|
||||||
|
{"Error", &Theme::markerError},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 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 (const auto& f : fields) {
|
for (int pi = 0; pi < (int)(sizeof(pairs) / sizeof(pairs[0])); pi++) {
|
||||||
auto* btn = new QPushButton;
|
const auto& p = pairs[pi];
|
||||||
btn->setFixedSize(60, 24);
|
int idx = m_contrastPairs.size();
|
||||||
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;
|
auto* row = new QHBoxLayout;
|
||||||
connect(btn, &QPushButton::clicked, this, [this, idx]() {
|
row->setSpacing(4);
|
||||||
pickColor(m_swatches[idx]);
|
row->setContentsMargins(8, 1, 0, 1);
|
||||||
});
|
|
||||||
form->addRow(f.label, btn);
|
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);
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
connect(buttons, &QDialogButtonBox::rejected, this, &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);
|
// Initial update
|
||||||
layout->addLayout(form);
|
for (int i = 0; i < m_swatches.size(); i++)
|
||||||
layout->addWidget(buttons);
|
updateSwatch(i);
|
||||||
|
updateAllContrast();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeEditor::updateSwatch(SwatchEntry& entry) {
|
// ── Load a different theme into the editor ──
|
||||||
QColor c = m_theme.*entry.field;
|
|
||||||
entry.button->setStyleSheet(QStringLiteral(
|
void ThemeEditor::loadTheme(int index) {
|
||||||
"QPushButton { background: %1; border: 1px solid #555; border-radius: 3px; }")
|
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()));
|
.arg(c.name()));
|
||||||
entry.button->setToolTip(c.name());
|
s.hexLabel->setText(c.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeEditor::pickColor(SwatchEntry& entry) {
|
// ── Contrast update ──
|
||||||
QColor c = QColorDialog::getColor(m_theme.*entry.field, this, entry.label);
|
|
||||||
if (c.isValid()) {
|
void ThemeEditor::updateAllContrast() {
|
||||||
m_theme.*entry.field = c;
|
for (int i = 0; i < m_contrastPairs.size(); i++) {
|
||||||
updateSwatch(entry);
|
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
|
} // namespace rcx
|
||||||
|
|||||||
@@ -3,27 +3,61 @@
|
|||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
|
||||||
|
class QScrollArea;
|
||||||
|
class QVBoxLayout;
|
||||||
|
class QComboBox;
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
class ThemeEditor : public QDialog {
|
class ThemeEditor : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ThemeEditor(const Theme& theme, QWidget* parent = nullptr);
|
explicit ThemeEditor(int themeIndex, QWidget* parent = nullptr);
|
||||||
Theme result() const { return m_theme; }
|
Theme result() const { return m_theme; }
|
||||||
|
int selectedIndex() const { return m_themeIndex; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Theme m_theme;
|
Theme m_theme;
|
||||||
|
int m_themeIndex;
|
||||||
|
|
||||||
|
// ── Swatch row (compact: label + swatch + hex) ──
|
||||||
struct SwatchEntry {
|
struct SwatchEntry {
|
||||||
const char* label;
|
const char* label;
|
||||||
QColor Theme::*field;
|
QColor Theme::*field;
|
||||||
QPushButton* button;
|
QPushButton* swatchBtn = nullptr;
|
||||||
|
QLabel* hexLabel = nullptr;
|
||||||
};
|
};
|
||||||
QVector<SwatchEntry> m_swatches;
|
QVector<SwatchEntry> m_swatches;
|
||||||
|
|
||||||
void updateSwatch(SwatchEntry& entry);
|
// ── Contrast pair row ──
|
||||||
void pickColor(SwatchEntry& entry);
|
struct ContrastEntry {
|
||||||
|
const char* fgLabel;
|
||||||
|
const char* bgLabel;
|
||||||
|
QColor Theme::*fgField;
|
||||||
|
QColor Theme::*bgField;
|
||||||
|
QLabel* ratioLabel = nullptr;
|
||||||
|
QLabel* levelLabel = nullptr;
|
||||||
|
QPushButton* fixBtn = nullptr;
|
||||||
|
};
|
||||||
|
QVector<ContrastEntry> m_contrastPairs;
|
||||||
|
|
||||||
|
// ── UI ──
|
||||||
|
QComboBox* m_themeCombo = nullptr;
|
||||||
|
QLineEdit* m_nameEdit = nullptr;
|
||||||
|
QLabel* m_fileInfoLabel = nullptr;
|
||||||
|
QPushButton* m_previewBtn = nullptr;
|
||||||
|
bool m_previewing = false;
|
||||||
|
|
||||||
|
void loadTheme(int index);
|
||||||
|
void rebuildSwatches(QVBoxLayout* swatchLayout);
|
||||||
|
void updateSwatch(int idx);
|
||||||
|
void updateAllContrast();
|
||||||
|
void pickColor(int idx);
|
||||||
|
void autoFixContrast(int idx);
|
||||||
|
void togglePreview();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ ThemeManager::ThemeManager() {
|
|||||||
m_builtIn.append(Theme::warm());
|
m_builtIn.append(Theme::warm());
|
||||||
loadUserThemes();
|
loadUserThemes();
|
||||||
|
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
QString saved = settings.value("theme", m_builtIn[0].name).toString();
|
QString saved = settings.value("theme", m_builtIn[0].name).toString();
|
||||||
auto all = themes();
|
auto all = themes();
|
||||||
for (int i = 0; i < all.size(); i++) {
|
for (int i = 0; i < all.size(); i++) {
|
||||||
@@ -44,7 +44,7 @@ void ThemeManager::setCurrent(int index) {
|
|||||||
auto all = themes();
|
auto all = themes();
|
||||||
if (index < 0 || index >= all.size()) return;
|
if (index < 0 || index >= all.size()) return;
|
||||||
m_currentIdx = index;
|
m_currentIdx = index;
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("Reclass", "Reclass");
|
||||||
settings.setValue("theme", all[index].name);
|
settings.setValue("theme", all[index].name);
|
||||||
emit themeChanged(current());
|
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
|
} // namespace rcx
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public:
|
|||||||
void loadUserThemes();
|
void loadUserThemes();
|
||||||
void saveUserThemes() const;
|
void saveUserThemes() const;
|
||||||
|
|
||||||
|
QString themeFilePath(int index) const;
|
||||||
|
void previewTheme(const Theme& theme);
|
||||||
|
void revertPreview();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void themeChanged(const rcx::Theme& theme);
|
void themeChanged(const rcx::Theme& theme);
|
||||||
|
|
||||||
@@ -33,6 +37,8 @@ private:
|
|||||||
|
|
||||||
int builtInCount() const { return m_builtIn.size(); }
|
int builtInCount() const { return m_builtIn.size(); }
|
||||||
QString themesDir() const;
|
QString themesDir() const;
|
||||||
|
bool m_previewing = false;
|
||||||
|
Theme m_savedTheme; // stashed current theme during preview
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// rcx-mcp-stdio: Bridges stdin/stdout to QLocalSocket for MCP transport.
|
// rcx-mcp-stdio: Bridges stdin/stdout to QLocalSocket for MCP transport.
|
||||||
// Claude Desktop spawns this process; it connects to the rcx-mcp named pipe
|
// 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)
|
// stdin (from Claude) → QLocalSocket → McpBridge (in Reclass)
|
||||||
// stdout (to Claude) ← QLocalSocket ← McpBridge (in ReclassX)
|
// stdout (to Claude) ← QLocalSocket ← McpBridge (in Reclass)
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QLocalSocket>
|
#include <QLocalSocket>
|
||||||
@@ -29,7 +29,7 @@ int main(int argc, char* argv[]) {
|
|||||||
auto* socket = new QLocalSocket(&app);
|
auto* socket = new QLocalSocket(&app);
|
||||||
QByteArray readBuf;
|
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, [&]() {
|
QObject::connect(socket, &QLocalSocket::readyRead, [&]() {
|
||||||
readBuf.append(socket->readAll());
|
readBuf.append(socket->readAll());
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
Reference in New Issue
Block a user