diff --git a/CMakeLists.txt b/CMakeLists.txt index 9665c5a..3eb0fac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -261,6 +261,12 @@ if(BUILD_TESTING) target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test) add_test(NAME test_theme COMMAND test_theme) + add_executable(test_options_dialog tests/test_options_dialog.cpp + src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp) + target_include_directories(test_options_dialog PRIVATE src) + target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test) + add_test(NAME test_options_dialog COMMAND test_options_dialog) + if(WIN32) add_executable(test_windbg_provider tests/test_windbg_provider.cpp plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp) diff --git a/src/main.cpp b/src/main.cpp index 536c27a..f464e31 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -220,8 +220,8 @@ static void applyGlobalTheme(const rcx::Theme& theme) { pal.setColor(QPalette::Text, theme.text); pal.setColor(QPalette::Button, theme.button); pal.setColor(QPalette::ButtonText, theme.text); - pal.setColor(QPalette::Highlight, theme.hover); - pal.setColor(QPalette::HighlightedText, theme.indHoverSpan); + pal.setColor(QPalette::Highlight, theme.selection); + pal.setColor(QPalette::HighlightedText, theme.text); pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt); pal.setColor(QPalette::ToolTipText, theme.text); pal.setColor(QPalette::Mid, theme.border); @@ -313,8 +313,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { // Restore menu bar title case setting (after menus are created) { - bool titleCase = QSettings("Reclass", "Reclass").value("menuBarTitleCase", true).toBool(); - m_titleBar->setMenuBarTitleCase(titleCase); + QSettings s("Reclass", "Reclass"); + m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", true).toBool()); + if (s.value("showIcon", false).toBool()) + m_titleBar->setShowIcon(true); } // MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu @@ -422,15 +424,6 @@ void MainWindow::createMenus() { themeMenu->addAction("Edit Theme...", this, &MainWindow::editTheme); view->addSeparator(); - auto* actShowIcon = view->addAction("Show &Icon"); - actShowIcon->setCheckable(true); - actShowIcon->setChecked(settings.value("showIcon", false).toBool()); - if (actShowIcon->isChecked()) m_titleBar->setShowIcon(true); - connect(actShowIcon, &QAction::toggled, this, [this](bool checked) { - m_titleBar->setShowIcon(checked); - QSettings s("Reclass", "Reclass"); - s.setValue("showIcon", checked); - }); view->addAction(m_workspaceDock->toggleViewAction()); // Node @@ -1048,6 +1041,7 @@ void MainWindow::showOptionsDialog() { current.themeIndex = tm.currentIndex(); current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString(); current.menuBarTitleCase = m_titleBar->menuBarTitleCase(); + current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool(); current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool(); current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool(); @@ -1067,6 +1061,11 @@ void MainWindow::showOptionsDialog() { QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase); } + if (r.showIcon != current.showIcon) { + m_titleBar->setShowIcon(r.showIcon); + QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon); + } + if (r.safeMode != current.safeMode) QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode); diff --git a/src/optionsdialog.cpp b/src/optionsdialog.cpp index 2a0aae1..5de7e03 100644 --- a/src/optionsdialog.cpp +++ b/src/optionsdialog.cpp @@ -8,8 +8,6 @@ #include #include #include -#include -#include #include namespace rcx { @@ -20,8 +18,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* 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); @@ -87,6 +83,10 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent) m_titleCaseCheck->setChecked(current.menuBarTitleCase); visualLayout->addRow(m_titleCaseCheck); + m_showIconCheck = new QCheckBox("Show icon in title bar"); + m_showIconCheck->setChecked(current.showIcon); + visualLayout->addRow(m_showIconCheck); + generalLayout->addWidget(visualGroup); // Safe Mode group box @@ -96,8 +96,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent) 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( @@ -127,8 +125,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent) 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( @@ -144,6 +140,18 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent) m_pages->addWidget(aiPage); // index 1 m_pageKeywords[aiItem] = collectPageKeywords(aiPage); + // -- Generator page -- + auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"}); + + auto* generatorPage = new QWidget; + auto* generatorLayout = new QVBoxLayout(generatorPage); + generatorLayout->setContentsMargins(0, 0, 0, 0); + generatorLayout->setSpacing(8); + generatorLayout->addStretch(); + + m_pages->addWidget(generatorPage); // index 2 + m_pageKeywords[generatorItem] = collectPageKeywords(generatorPage); + middleLayout->addWidget(m_pages, 1); mainLayout->addLayout(middleLayout, 1); @@ -151,6 +159,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent) // Tree <-> page connection m_itemPageIndex[generalItem] = 0; m_itemPageIndex[aiItem] = 1; + m_itemPageIndex[generatorItem] = 2; connect(m_tree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem* item, QTreeWidgetItem*) { if (!item) return; @@ -165,106 +174,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent) 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 { @@ -272,6 +181,7 @@ OptionsResult OptionsDialog::result() const { r.themeIndex = m_themeCombo->currentIndex(); r.fontName = m_fontCombo->currentText(); r.menuBarTitleCase = m_titleCaseCheck->isChecked(); + r.showIcon = m_showIconCheck->isChecked(); r.safeMode = m_safeModeCheck->isChecked(); r.autoStartMcp = m_autoMcpCheck->isChecked(); return r; diff --git a/src/optionsdialog.h b/src/optionsdialog.h index 0bf04e6..94b466f 100644 --- a/src/optionsdialog.h +++ b/src/optionsdialog.h @@ -1,5 +1,4 @@ #pragma once -#include "themes/theme.h" #include #include #include @@ -7,7 +6,6 @@ #include #include #include -#include namespace rcx { @@ -15,6 +13,7 @@ struct OptionsResult { int themeIndex = 0; QString fontName; bool menuBarTitleCase = true; + bool showIcon = false; bool safeMode = false; bool autoStartMcp = false; }; @@ -26,9 +25,6 @@ public: OptionsResult result() const; -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - private: void filterTree(const QString& text); static QStringList collectPageKeywords(QWidget* page); @@ -39,11 +35,10 @@ private: QComboBox* m_themeCombo = nullptr; QComboBox* m_fontCombo = nullptr; QCheckBox* m_titleCaseCheck = nullptr; + QCheckBox* m_showIconCheck = 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 diff --git a/src/titlebar.cpp b/src/titlebar.cpp index 8f4b2b3..4c7f410 100644 --- a/src/titlebar.cpp +++ b/src/titlebar.cpp @@ -122,6 +122,8 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) { clean.remove('&'); if (titleCase) { + action->setText("&" + clean.toUpper()); + } else { QString result; bool capitalizeNext = true; for (int i = 0; i < clean.length(); ++i) { @@ -135,8 +137,6 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) { } } action->setText("&" + result); - } else { - action->setText("&" + clean.toUpper()); } } } diff --git a/tests/test_options_dialog.cpp b/tests/test_options_dialog.cpp new file mode 100644 index 0000000..de82ac5 --- /dev/null +++ b/tests/test_options_dialog.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "optionsdialog.h" +#include "themes/thememanager.h" + +using namespace rcx; + +// Helper: apply the global palette the same way main.cpp does +static void applyGlobalTheme(const Theme& theme) { + QPalette pal; + pal.setColor(QPalette::Window, theme.background); + pal.setColor(QPalette::WindowText, theme.text); + pal.setColor(QPalette::Base, theme.background); + pal.setColor(QPalette::AlternateBase, theme.surface); + pal.setColor(QPalette::Text, theme.text); + pal.setColor(QPalette::Button, theme.button); + pal.setColor(QPalette::ButtonText, theme.text); + pal.setColor(QPalette::Highlight, theme.selection); + pal.setColor(QPalette::HighlightedText, theme.text); + pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt); + pal.setColor(QPalette::ToolTipText, theme.text); + pal.setColor(QPalette::Mid, theme.border); + pal.setColor(QPalette::Dark, theme.background); + pal.setColor(QPalette::Light, theme.textFaint); + pal.setColor(QPalette::Link, theme.indHoverSpan); + + pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted); + pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted); + pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted); + pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted); + pal.setColor(QPalette::Disabled, QPalette::Light, theme.background); + + qApp->setPalette(pal); + qApp->setStyleSheet(QString()); +} + +class TestOptionsDialog : public QObject { + Q_OBJECT +private slots: + + void initTestCase() { + // Apply theme palette so dialog inherits real colors + auto& tm = ThemeManager::instance(); + applyGlobalTheme(tm.current()); + } + + void dialogCreatesAllWidgets() { + OptionsResult defaults; + defaults.themeIndex = 0; + defaults.fontName = "JetBrains Mono"; + defaults.menuBarTitleCase = true; + defaults.safeMode = false; + defaults.autoStartMcp = false; + + OptionsDialog dlg(defaults); + + // Core widgets exist + auto* tree = dlg.findChild(); + QVERIFY(tree); + auto* pages = dlg.findChild(); + QVERIFY(pages); + QCOMPARE(pages->count(), 3); + + auto* themeCombo = dlg.findChild("themeCombo"); + QVERIFY(themeCombo); + QVERIFY(themeCombo->count() >= 3); + + auto* fontCombo = dlg.findChild("fontCombo"); + QVERIFY(fontCombo); + QCOMPARE(fontCombo->count(), 2); + + auto* showIconCheck = dlg.findChild(); + QVERIFY(showIconCheck); + + auto* buttons = dlg.findChild(); + QVERIFY(buttons); + QVERIFY(buttons->button(QDialogButtonBox::Ok)); + QVERIFY(buttons->button(QDialogButtonBox::Cancel)); + } + + void resultReflectsInput() { + OptionsResult input; + input.themeIndex = 1; + input.fontName = "Consolas"; + input.menuBarTitleCase = false; + input.safeMode = true; + input.autoStartMcp = true; + + OptionsDialog dlg(input); + auto r = dlg.result(); + + QCOMPARE(r.themeIndex, 1); + QCOMPARE(r.fontName, QString("Consolas")); + QCOMPARE(r.menuBarTitleCase, false); + QCOMPARE(r.safeMode, true); + QCOMPARE(r.autoStartMcp, true); + } + + void noStyleSheetOnDialog() { + OptionsResult defaults; + OptionsDialog dlg(defaults); + + // Dialog itself must have no stylesheet override + QVERIFY(dlg.styleSheet().isEmpty()); + + // Combo boxes must have no stylesheet override + auto* themeCombo = dlg.findChild("themeCombo"); + QVERIFY(themeCombo->styleSheet().isEmpty()); + auto* fontCombo = dlg.findChild("fontCombo"); + QVERIFY(fontCombo->styleSheet().isEmpty()); + + // No child widget should have a stylesheet set + for (auto* child : dlg.findChildren()) { + QVERIFY2(child->styleSheet().isEmpty(), + qPrintable(QString("Widget %1 (%2) has unexpected stylesheet: %3") + .arg(child->objectName(), + child->metaObject()->className(), + child->styleSheet()))); + } + } + + void highlightColorDiffersFromBackground() { + // Verify the palette Highlight is distinguishable from Window background + // This is the root cause of broken hover: if they're the same, hover is invisible + auto& tm = ThemeManager::instance(); + for (int i = 0; i < tm.themes().size(); ++i) { + const auto& theme = tm.themes()[i]; + // selection must differ from background + QVERIFY2(theme.selection != theme.background, + qPrintable(QString("Theme '%1': selection == background (%2)") + .arg(theme.name, theme.background.name()))); + } + } + + void paletteHighlightIsSelection() { + // After applying theme, QPalette::Highlight must be theme.selection (not theme.hover) + auto& tm = ThemeManager::instance(); + const auto& theme = tm.current(); + applyGlobalTheme(theme); + + QPalette pal = qApp->palette(); + QCOMPARE(pal.color(QPalette::Highlight), theme.selection); + } + + void treePageSwitching() { + OptionsResult defaults; + OptionsDialog dlg(defaults); + + auto* tree = dlg.findChild(); + auto* pages = dlg.findChild(); + QVERIFY(tree && pages); + + // General is selected by default -> page 0 + QCOMPARE(pages->currentIndex(), 0); + + // Find "AI Features" item and select it + auto* envItem = tree->topLevelItem(0); + QVERIFY(envItem); + QTreeWidgetItem* aiItem = nullptr; + for (int i = 0; i < envItem->childCount(); ++i) { + if (envItem->child(i)->text(0) == "AI Features") { + aiItem = envItem->child(i); + break; + } + } + QVERIFY(aiItem); + tree->setCurrentItem(aiItem); + QCOMPARE(pages->currentIndex(), 1); + + // Switch back to General + QTreeWidgetItem* generalItem = nullptr; + for (int i = 0; i < envItem->childCount(); ++i) { + if (envItem->child(i)->text(0) == "General") { + generalItem = envItem->child(i); + break; + } + } + QVERIFY(generalItem); + tree->setCurrentItem(generalItem); + QCOMPARE(pages->currentIndex(), 0); + } + + void searchFilterHidesItems() { + OptionsResult defaults; + OptionsDialog dlg(defaults); + + auto* search = dlg.findChild(); + auto* tree = dlg.findChild(); + QVERIFY(search && tree); + + auto* envItem = tree->topLevelItem(0); + QVERIFY(envItem); + + // All children visible initially + for (int i = 0; i < envItem->childCount(); ++i) + QVERIFY(!envItem->child(i)->isHidden()); + + // Search for "MCP" - should hide General, show AI Features + search->setText("MCP"); + QTreeWidgetItem* generalItem = nullptr; + QTreeWidgetItem* aiItem = nullptr; + for (int i = 0; i < envItem->childCount(); ++i) { + auto* child = envItem->child(i); + if (child->text(0) == "General") generalItem = child; + if (child->text(0) == "AI Features") aiItem = child; + } + QVERIFY(generalItem && aiItem); + QVERIFY(generalItem->isHidden()); + QVERIFY(!aiItem->isHidden()); + + // Clear search - all visible again + search->setText(""); + QVERIFY(!generalItem->isHidden()); + QVERIFY(!aiItem->isHidden()); + } + + void dialogInheritsPalette() { + auto& tm = ThemeManager::instance(); + const auto& theme = tm.current(); + applyGlobalTheme(theme); + + OptionsResult defaults; + OptionsDialog dlg(defaults); + dlg.show(); + QTest::qWaitForWindowExposed(&dlg); + + // Dialog's effective palette should match the app palette + QPalette dlgPal = dlg.palette(); + QPalette appPal = qApp->palette(); + + QCOMPARE(dlgPal.color(QPalette::Window), appPal.color(QPalette::Window)); + QCOMPARE(dlgPal.color(QPalette::WindowText), appPal.color(QPalette::WindowText)); + QCOMPARE(dlgPal.color(QPalette::Highlight), appPal.color(QPalette::Highlight)); + QCOMPARE(dlgPal.color(QPalette::Button), appPal.color(QPalette::Button)); + QCOMPARE(dlgPal.color(QPalette::ButtonText), appPal.color(QPalette::ButtonText)); + + // Highlight must be visible against background + QVERIFY(dlgPal.color(QPalette::Highlight) != dlgPal.color(QPalette::Window)); + } +}; + +QTEST_MAIN(TestOptionsDialog) +#include "test_options_dialog.moc"