Added options dialog

This commit is contained in:
Sen66
2026-02-15 03:24:12 +01:00
parent 0a8244dad4
commit 0ef9841f90
8 changed files with 460 additions and 3 deletions

View File

@@ -60,6 +60,8 @@ add_executable(Reclass
src/themes/themeeditor.h src/themes/themeeditor.h
src/themes/themeeditor.cpp src/themes/themeeditor.cpp
src/mainwindow.h src/mainwindow.h
src/optionsdialog.h
src/optionsdialog.cpp
src/titlebar.h src/titlebar.h
src/titlebar.cpp src/titlebar.cpp
src/mcp/mcp_bridge.h src/mcp/mcp_bridge.h

View File

@@ -43,6 +43,7 @@
#include <QDesktopServices> #include <QDesktopServices>
#include "themes/thememanager.h" #include "themes/thememanager.h"
#include "themes/themeeditor.h" #include "themes/themeeditor.h"
#include "optionsdialog.h"
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
@@ -301,6 +302,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createMenus(); createMenus();
createStatusBar(); createStatusBar();
// Restore menu bar title case setting (after menus are created)
{
bool titleCase = QSettings("Reclass", "Reclass").value("menuBarTitleCase", true).toBool();
m_titleBar->setMenuBarTitleCase(titleCase);
}
// MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu // MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu
@@ -310,9 +316,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
// Load plugins // Load plugins
m_pluginManager.LoadPlugins(); m_pluginManager.LoadPlugins();
// MCP bridge (on by default) // Start MCP bridge
m_mcp = new McpBridge(this, this); m_mcp = new McpBridge(this, this);
m_mcp->start(); if (QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool())
m_mcp->start();
connect(m_mdiArea, &QMdiArea::subWindowActivated, connect(m_mdiArea, &QMdiArea::subWindowActivated,
this, [this](QMdiSubWindow*) { this, [this](QMdiSubWindow*) {
@@ -350,7 +357,9 @@ void MainWindow::createMenus() {
file->addSeparator(); file->addSeparator();
file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp); file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp);
file->addSeparator(); file->addSeparator();
m_mcpAction = file->addAction("Stop &MCP Server", this, &MainWindow::toggleMcp); m_mcpAction = file->addAction(QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server", this, &MainWindow::toggleMcp);
file->addSeparator();
file->addAction(makeIcon(":/vsicons/settings-gear.svg"), "&Options...", this, &MainWindow::showOptionsDialog);
file->addSeparator(); file->addSeparator();
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close)); file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close));
@@ -1024,6 +1033,39 @@ void MainWindow::editTheme() {
} }
} }
// TODO: when adding more and more options, this func becomes very clunky. Fix
void MainWindow::showOptionsDialog() {
auto& tm = ThemeManager::instance();
OptionsResult current;
current.themeIndex = tm.currentIndex();
current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
OptionsDialog dlg(current, this);
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
auto r = dlg.result();
if (r.themeIndex != current.themeIndex)
tm.setCurrent(r.themeIndex);
if (r.fontName != current.fontName)
setEditorFont(r.fontName);
if (r.menuBarTitleCase != current.menuBarTitleCase) {
m_titleBar->setMenuBarTitleCase(r.menuBarTitleCase);
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
}
if (r.safeMode != current.safeMode)
QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode);
if (r.autoStartMcp != current.autoStartMcp)
QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp);
}
void MainWindow::setEditorFont(const QString& fontName) { void MainWindow::setEditorFont(const QString& fontName) {
QSettings settings("Reclass", "Reclass"); QSettings settings("Reclass", "Reclass");
settings.setValue("font", fontName); settings.setValue("font", fontName);

View File

@@ -49,6 +49,7 @@ private slots:
void exportCpp(); void exportCpp();
void showTypeAliasesDialog(); void showTypeAliasesDialog();
void editTheme(); void editTheme();
void showOptionsDialog();
public: public:
// Project Lifecycle API // Project Lifecycle API

327
src/optionsdialog.cpp Normal file
View File

@@ -0,0 +1,327 @@
#include "optionsdialog.h"
#include "themes/thememanager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QGroupBox>
#include <QLabel>
#include <QTreeWidgetItem>
#include <QGraphicsDropShadowEffect>
#include <QEvent>
#include <functional>
namespace rcx {
OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
: QDialog(parent)
{
setWindowTitle("Options");
setFixedSize(700, 450);
const auto& t = ThemeManager::instance().current();
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(8);
mainLayout->setContentsMargins(10, 10, 10, 10);
// -- Middle: left column (search + tree) | right column (pages) --
auto* middleLayout = new QHBoxLayout;
middleLayout->setSpacing(8);
// Left column: search bar + tree
auto* leftColumn = new QVBoxLayout;
leftColumn->setSpacing(4);
m_search = new QLineEdit;
m_search->setPlaceholderText("Search Options (Ctrl+E)");
m_search->setClearButtonEnabled(true);
connect(m_search, &QLineEdit::textChanged, this, &OptionsDialog::filterTree);
leftColumn->addWidget(m_search);
m_tree = new QTreeWidget;
m_tree->setHeaderHidden(true);
m_tree->setRootIsDecorated(true);
m_tree->setFixedWidth(200);
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
m_tree->expandAll();
m_tree->setCurrentItem(generalItem);
leftColumn->addWidget(m_tree, 1);
middleLayout->addLayout(leftColumn);
// Right column: stacked pages with group boxes
m_pages = new QStackedWidget;
// -- General page --
auto* generalPage = new QWidget;
auto* generalLayout = new QVBoxLayout(generalPage);
generalLayout->setContentsMargins(0, 0, 0, 0);
generalLayout->setSpacing(8);
// Visual Experience group box
auto* visualGroup = new QGroupBox("Visual Experience");
auto* visualLayout = new QFormLayout(visualGroup);
visualLayout->setSpacing(8);
visualLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_themeCombo = new QComboBox;
auto& tm = ThemeManager::instance();
for (const auto& theme : tm.themes())
m_themeCombo->addItem(theme.name);
m_themeCombo->setCurrentIndex(current.themeIndex);
m_themeCombo->setObjectName("themeCombo");
visualLayout->addRow("Color theme:", m_themeCombo);
m_fontCombo = new QComboBox;
m_fontCombo->addItem("JetBrains Mono");
m_fontCombo->addItem("Consolas");
m_fontCombo->setCurrentText(current.fontName);
m_fontCombo->setObjectName("fontCombo");
visualLayout->addRow("Editor Font:", m_fontCombo);
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
visualLayout->addRow(m_titleCaseCheck);
generalLayout->addWidget(visualGroup);
// Safe Mode group box
auto* safeModeGroup = new QGroupBox("Preview Features");
auto* safeModeLayout = new QVBoxLayout(safeModeGroup);
safeModeLayout->setSpacing(4);
m_safeModeCheck = new QCheckBox("Safe Mode");
m_safeModeCheck->setChecked(current.safeMode);
m_safeModeCheck->setStyleSheet(QStringLiteral(
"QCheckBox { font-weight: bold; }"));
safeModeLayout->addWidget(m_safeModeCheck);
auto* safeModeDesc = new QLabel(
"Enable to use the default OS icon for this application and "
"create the window with the name of the executable file.");
safeModeDesc->setWordWrap(true);
safeModeDesc->setContentsMargins(20, 0, 0, 0); // indent under checkbox
safeModeLayout->addWidget(safeModeDesc);
generalLayout->addWidget(safeModeGroup);
generalLayout->addStretch();
m_pages->addWidget(generalPage); // index 0
m_pageKeywords[generalItem] = collectPageKeywords(generalPage);
// -- AI Features page --
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
auto* aiPage = new QWidget;
auto* aiLayout = new QVBoxLayout(aiPage);
aiLayout->setContentsMargins(0, 0, 0, 0);
aiLayout->setSpacing(8);
auto* mcpGroup = new QGroupBox("MCP Server");
auto* mcpLayout = new QVBoxLayout(mcpGroup);
mcpLayout->setSpacing(4);
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
m_autoMcpCheck->setChecked(current.autoStartMcp);
m_autoMcpCheck->setStyleSheet(QStringLiteral(
"QCheckBox { font-weight: bold; }"));
mcpLayout->addWidget(m_autoMcpCheck);
auto* mcpDesc = new QLabel(
"Automatically start the MCP bridge server when the application launches, "
"allowing external AI tools to connect and interact with the editor.");
mcpDesc->setWordWrap(true);
mcpDesc->setContentsMargins(20, 0, 0, 0);
mcpLayout->addWidget(mcpDesc);
aiLayout->addWidget(mcpGroup);
aiLayout->addStretch();
m_pages->addWidget(aiPage); // index 1
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
middleLayout->addWidget(m_pages, 1);
mainLayout->addLayout(middleLayout, 1);
// Tree <-> page connection
m_itemPageIndex[generalItem] = 0;
m_itemPageIndex[aiItem] = 1;
connect(m_tree, &QTreeWidget::currentItemChanged, this,
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return;
auto it = m_itemPageIndex.find(item);
if (it != m_itemPageIndex.end())
m_pages->setCurrentIndex(it.value());
});
// -- Button box --
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttons);
// -- Styling --
// Combo boxes: set directly so the popup (top-level widget) inherits it
QString comboStyle = QStringLiteral(
"QComboBox {"
" background: %1; color: %2; border: 1px solid %3;"
" padding: 3px 8px; font-size: 12px;"
"}"
"QComboBox::drop-down {"
" border: none; border-left: 1px solid %3;"
" width: 20px;"
"}"
"QComboBox::down-arrow {"
" image: url(:/vsicons/chevron-down.svg);"
" width: 12px; height: 12px;"
"}"
"QComboBox QAbstractItemView {"
" background: %1; color: %2; border: 1px solid %3;"
" selection-background-color: %4;"
"}")
.arg(t.backgroundAlt.name(), t.text.name(),
t.border.name(), t.hover.name());
m_themeCombo->setStyleSheet(comboStyle);
m_fontCombo->setStyleSheet(comboStyle);
// Dialog-wide stylesheet for everything else
setStyleSheet(QStringLiteral(
"QDialog { background: %1; }"
"QLineEdit {"
" background: %2; color: %3; border: 1px solid %4;"
" padding: 4px 8px; font-size: 12px;"
"}"
"QTreeWidget {"
" background: %2; color: %3; border: 1px solid %4;"
" font-size: 12px; outline: none;"
"}"
"QTreeWidget::item { padding: 3px 0; outline: none; }"
"QTreeWidget::item:selected { background: %5; color: %3; }"
"QTreeWidget::item:hover { background: %6; }"
"QGroupBox {"
" color: %3; border: 1px solid %4;"
" margin-top: 8px; padding: 12px 8px 8px 8px;"
" font-size: 12px; font-weight: bold;"
"}"
"QGroupBox::title {"
" subcontrol-origin: margin;"
" left: 8px; padding: 0 4px;"
"}"
"QLabel { color: %3; font-size: 12px; }"
"QCheckBox { color: %3; font-size: 12px; spacing: 6px; }"
"QPushButton {"
" background: %2; color: %3; border: 1px solid %4;"
" padding: 5px 16px; min-width: 70px; font-size: 12px;"
" outline: none;"
"}"
"QPushButton:hover { background: %6; }"
"QPushButton:pressed { background: %1; }"
"QPushButton:focus { outline: none; }")
.arg(t.background.name(), // %1
t.backgroundAlt.name(), // %2
t.text.name(), // %3
t.border.name(), // %4
t.selection.name(), // %5
t.hover.name())); // %6
// Install hover shadow on interactive widgets (not buttons — they use stylesheet hover)
for (auto* w : {static_cast<QWidget*>(m_search),
static_cast<QWidget*>(m_themeCombo),
static_cast<QWidget*>(m_fontCombo),
static_cast<QWidget*>(m_titleCaseCheck),
static_cast<QWidget*>(m_safeModeCheck),
static_cast<QWidget*>(m_autoMcpCheck)})
w->installEventFilter(this);
m_shadowColor = t.text;
m_shadowColor.setAlpha(80);
}
bool OptionsDialog::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::Enter) {
auto* w = qobject_cast<QWidget*>(obj);
if (w && !w->graphicsEffect()) {
auto* shadow = new QGraphicsDropShadowEffect(w);
shadow->setBlurRadius(12);
shadow->setOffset(0, 0);
shadow->setColor(m_shadowColor);
w->setGraphicsEffect(shadow);
}
} else if (event->type() == QEvent::Leave) {
auto* w = qobject_cast<QWidget*>(obj);
if (w)
w->setGraphicsEffect(nullptr);
}
return QDialog::eventFilter(obj, event);
}
OptionsResult OptionsDialog::result() const {
OptionsResult r;
r.themeIndex = m_themeCombo->currentIndex();
r.fontName = m_fontCombo->currentText();
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
r.safeMode = m_safeModeCheck->isChecked();
r.autoStartMcp = m_autoMcpCheck->isChecked();
return r;
}
QStringList OptionsDialog::collectPageKeywords(QWidget* page) {
QStringList keywords;
for (auto* child : page->findChildren<QWidget*>()) {
if (auto* label = qobject_cast<QLabel*>(child))
keywords << label->text();
else if (auto* cb = qobject_cast<QCheckBox*>(child))
keywords << cb->text();
else if (auto* gb = qobject_cast<QGroupBox*>(child))
keywords << gb->title();
else if (auto* combo = qobject_cast<QComboBox*>(child)) {
for (int i = 0; i < combo->count(); ++i)
keywords << combo->itemText(i);
}
}
return keywords;
}
void OptionsDialog::filterTree(const QString& text) {
std::function<bool(QTreeWidgetItem*)> filter = [&](QTreeWidgetItem* item) -> bool {
bool anyChildVisible = false;
for (int i = 0; i < item->childCount(); ++i) {
if (filter(item->child(i)))
anyChildVisible = true;
}
bool selfMatch = item->text(0).contains(text, Qt::CaseInsensitive);
if (!selfMatch) {
for (const auto& kw : m_pageKeywords.value(item)) {
if (kw.contains(text, Qt::CaseInsensitive)) {
selfMatch = true;
break;
}
}
}
bool visible = selfMatch || anyChildVisible;
item->setHidden(!visible);
if (visible && item->childCount() > 0)
item->setExpanded(true);
return visible;
};
for (int i = 0; i < m_tree->topLevelItemCount(); ++i)
filter(m_tree->topLevelItem(i));
}
} // namespace rcx

53
src/optionsdialog.h Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include "themes/theme.h"
#include <QDialog>
#include <QLineEdit>
#include <QTreeWidget>
#include <QStackedWidget>
#include <QComboBox>
#include <QCheckBox>
#include <QHash>
#include <QColor>
namespace rcx {
struct OptionsResult {
int themeIndex = 0;
QString fontName;
bool menuBarTitleCase = true;
bool safeMode = false;
bool autoStartMcp = false;
};
class OptionsDialog : public QDialog {
Q_OBJECT
public:
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
OptionsResult result() const;
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private:
void filterTree(const QString& text);
static QStringList collectPageKeywords(QWidget* page);
QLineEdit* m_search = nullptr;
QTreeWidget* m_tree = nullptr;
QStackedWidget* m_pages = nullptr;
QComboBox* m_themeCombo = nullptr;
QComboBox* m_fontCombo = nullptr;
QCheckBox* m_titleCaseCheck = nullptr;
QCheckBox* m_safeModeCheck = nullptr;
QCheckBox* m_autoMcpCheck = nullptr;
QColor m_shadowColor;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
// tree item → stacked widget page index
QHash<QTreeWidgetItem*, int> m_itemPageIndex;
};
} // namespace rcx

View File

@@ -47,5 +47,7 @@
<file alias="selection.svg">vsicons/list-selection.svg</file> <file alias="selection.svg">vsicons/list-selection.svg</file>
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file> <file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file> <file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file>
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -114,6 +114,33 @@ void TitleBarWidget::setShowIcon(bool show) {
} }
} }
void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
m_titleCase = titleCase;
for (QAction* action : m_menuBar->actions()) {
QString text = action->text();
QString clean = text;
clean.remove('&');
if (titleCase) {
QString result;
bool capitalizeNext = true;
for (int i = 0; i < clean.length(); ++i) {
QChar ch = clean[i];
if (ch.isLetter()) {
result += capitalizeNext ? ch.toUpper() : ch.toLower();
capitalizeNext = false;
} else {
result += ch;
if (ch.isSpace()) capitalizeNext = true;
}
}
action->setText("&" + result);
} else {
action->setText("&" + clean.toUpper());
}
}
}
void TitleBarWidget::updateMaximizeIcon() { void TitleBarWidget::updateMaximizeIcon() {
if (window()->isMaximized()) if (window()->isMaximized())
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg")); m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));

View File

@@ -16,6 +16,8 @@ public:
QMenuBar* menuBar() const { return m_menuBar; } QMenuBar* menuBar() const { return m_menuBar; }
void applyTheme(const Theme& theme); void applyTheme(const Theme& theme);
void setShowIcon(bool show); void setShowIcon(bool show);
void setMenuBarTitleCase(bool titleCase);
bool menuBarTitleCase() const { return m_titleCase; }
void updateMaximizeIcon(); void updateMaximizeIcon();
@@ -32,6 +34,7 @@ private:
QToolButton* m_btnClose = nullptr; QToolButton* m_btnClose = nullptr;
Theme m_theme; Theme m_theme;
bool m_titleCase = true;
QToolButton* makeChromeButton(const QString& iconPath); QToolButton* makeChromeButton(const QString& iconPath);
void toggleMaximize(); void toggleMaximize();