From 0ef9841f90506a46a925e677b83cdb8c8256e629 Mon Sep 17 00:00:00 2001 From: Sen66 Date: Sun, 15 Feb 2026 03:24:12 +0100 Subject: [PATCH] Added options dialog --- CMakeLists.txt | 2 + src/main.cpp | 48 ++++++- src/mainwindow.h | 1 + src/optionsdialog.cpp | 327 ++++++++++++++++++++++++++++++++++++++++++ src/optionsdialog.h | 53 +++++++ src/resources.qrc | 2 + src/titlebar.cpp | 27 ++++ src/titlebar.h | 3 + 8 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 src/optionsdialog.cpp create mode 100644 src/optionsdialog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 68752bf..428cfb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,8 @@ add_executable(Reclass src/themes/themeeditor.h src/themes/themeeditor.cpp src/mainwindow.h + src/optionsdialog.h + src/optionsdialog.cpp src/titlebar.h src/titlebar.cpp src/mcp/mcp_bridge.h diff --git a/src/main.cpp b/src/main.cpp index d3f3bcc..b16bf75 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,6 +43,7 @@ #include #include "themes/thememanager.h" #include "themes/themeeditor.h" +#include "optionsdialog.h" #ifdef _WIN32 #include @@ -301,6 +302,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { createMenus(); 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 @@ -310,9 +316,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { // Load plugins m_pluginManager.LoadPlugins(); - // MCP bridge (on by default) + // Start MCP bridge 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, this, [this](QMdiSubWindow*) { @@ -350,7 +357,9 @@ void MainWindow::createMenus() { file->addSeparator(); file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp); 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->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) { QSettings settings("Reclass", "Reclass"); settings.setValue("font", fontName); diff --git a/src/mainwindow.h b/src/mainwindow.h index 6ac1768..c886c19 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -49,6 +49,7 @@ private slots: void exportCpp(); void showTypeAliasesDialog(); void editTheme(); + void showOptionsDialog(); public: // Project Lifecycle API diff --git a/src/optionsdialog.cpp b/src/optionsdialog.cpp new file mode 100644 index 0000000..2a0aae1 --- /dev/null +++ b/src/optionsdialog.cpp @@ -0,0 +1,327 @@ +#include "optionsdialog.h" +#include "themes/thememanager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(m_search), + static_cast(m_themeCombo), + static_cast(m_fontCombo), + static_cast(m_titleCaseCheck), + static_cast(m_safeModeCheck), + static_cast(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(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(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()) { + if (auto* label = qobject_cast(child)) + keywords << label->text(); + else if (auto* cb = qobject_cast(child)) + keywords << cb->text(); + else if (auto* gb = qobject_cast(child)) + keywords << gb->title(); + else if (auto* combo = qobject_cast(child)) { + for (int i = 0; i < combo->count(); ++i) + keywords << combo->itemText(i); + } + } + return keywords; +} + +void OptionsDialog::filterTree(const QString& text) { + std::function 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 diff --git a/src/optionsdialog.h b/src/optionsdialog.h new file mode 100644 index 0000000..0bf04e6 --- /dev/null +++ b/src/optionsdialog.h @@ -0,0 +1,53 @@ +#pragma once +#include "themes/theme.h" +#include +#include +#include +#include +#include +#include +#include +#include + +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 m_pageKeywords; + // tree item → stacked widget page index + QHash m_itemPageIndex; +}; + +} // namespace rcx diff --git a/src/resources.qrc b/src/resources.qrc index 8ae6a26..32d6fb1 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -47,5 +47,7 @@ vsicons/list-selection.svg vsicons/symbol-numeric.svg vsicons/symbol-ruler.svg + vsicons/settings-gear.svg + vsicons/chevron-down.svg diff --git a/src/titlebar.cpp b/src/titlebar.cpp index 4c532c0..8f4b2b3 100644 --- a/src/titlebar.cpp +++ b/src/titlebar.cpp @@ -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() { if (window()->isMaximized()) m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg")); diff --git a/src/titlebar.h b/src/titlebar.h index 84f27b6..8cbc376 100644 --- a/src/titlebar.h +++ b/src/titlebar.h @@ -16,6 +16,8 @@ public: QMenuBar* menuBar() const { return m_menuBar; } void applyTheme(const Theme& theme); void setShowIcon(bool show); + void setMenuBarTitleCase(bool titleCase); + bool menuBarTitleCase() const { return m_titleCase; } void updateMaximizeIcon(); @@ -32,6 +34,7 @@ private: QToolButton* m_btnClose = nullptr; Theme m_theme; + bool m_titleCase = true; QToolButton* makeChromeButton(const QString& iconPath); void toggleMaximize();