From c3b839a9053ef81d12f7211389f373dfba0a7d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 11 Oct 2026 11:40:20 +0200 Subject: [PATCH 1/6] [Client] Settings: extract SettingsView from DlgSettings Move the search bar, tab bar, stacked pages and search results out of DlgSettings into a reusable SettingsView widget. The dialog becomes a thin frame (title, OK button, close validation) delegating to the view. No visual or behavior change; tr() contexts preserved as DlgSettings. --- cockatrice/CMakeLists.txt | 1 + .../widgets/dialogs/dlg_settings.cpp | 462 +--------------- .../interface/widgets/dialogs/dlg_settings.h | 86 +-- .../widgets/dialogs/settings_view.cpp | 498 ++++++++++++++++++ .../interface/widgets/dialogs/settings_view.h | 98 ++++ 5 files changed, 626 insertions(+), 519 deletions(-) create mode 100644 cockatrice/src/interface/widgets/dialogs/settings_view.cpp create mode 100644 cockatrice/src/interface/widgets/dialogs/settings_view.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 32b81cf61..396ef7c34 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -59,6 +59,7 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_update.cpp src/interface/widgets/dialogs/dlg_view_log.cpp src/interface/widgets/dialogs/override_printing_warning.cpp + src/interface/widgets/dialogs/settings_view.cpp src/interface/widgets/dialogs/tip_of_the_day.cpp src/interface/widgets/deck_share/deck_share_utils.cpp src/interface/widgets/deck_share/shared_deck_preview_widget.cpp diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index d41c948eb..9b20372db 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -1,205 +1,41 @@ /** * @file dlg_settings.cpp - * @brief Implementation of the main settings dialog + * @brief Implementation of the main settings dialog (thin frame around SettingsView) * @ingroup Dialogs */ #include "dlg_settings.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "../main.h" #include "../settings_page/abstract_settings_page.h" -#include "../settings_page/appearance_settings_page.h" -#include "../settings_page/deck_editor_settings_page.h" -#include "../settings_page/general_settings_page.h" -#include "../settings_page/messages_settings_page.h" -#include "../settings_page/settings_search_delegate.h" -#include "../settings_page/settings_search_model.h" -#include "../settings_page/shortcut_settings_page.h" -#include "../settings_page/sound_settings_page.h" -#include "../settings_page/storage_settings_page.h" -#include "../settings_page/user_interface_settings_page.h" #include "libcockatrice/card/database/card_database.h" #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_manager.h" -#include -#include #include -#include -#include #include -#include -#include #include -#include -#include -#include -#include -#include #include #include -#include -#include -#include -#include -#include #include #include -#include -#include -#include -#include -#include #include -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include #include #include #include #include -#include -class QModelIndex; - -/** - * @brief Wraps a widget in a scroll area for long settings pages - * @param widget The widget to wrap - * @return The scroll area containing the widget - */ -static QScrollArea *makeScrollable(QWidget *widget) -{ - widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); - - auto *scrollArea = new QScrollArea; - scrollArea->setWidgetResizable(true); - scrollArea->setContentsMargins(0, 0, 0, 0); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea->horizontalScrollBar()->setEnabled(false); - scrollArea->setWidget(widget); - return scrollArea; -} - -/** - * @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order - */ -static QStringList pageIconResources() -{ - return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"), - QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"), - QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"), - QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")}; -} - -DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false) +DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) { auto rec = QGuiApplication::primaryScreen()->availableGeometry(); setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height())); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); - setupUi(); - - connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); - retranslateUi(); - - searchEdit->setFocus(); - - adjustSize(); -} - -void DlgSettings::setupUi() -{ - // Search bar - searchEdit = new QLineEdit; - searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - searchEdit->installEventFilter(this); - connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); - - auto *searchLayout = new QHBoxLayout; - searchLayout->addWidget(searchEdit); - - // Tab bar (built in setupTabBar) - setupTabBar(); - - // Pages stacked widget - pagesWidget = new QStackedWidget; - - auto *generalPage = new GeneralSettingsPage; - auto *appearancePage = new AppearanceSettingsPage; - auto *userInterfacePage = new UserInterfaceSettingsPage; - auto *deckEditorPage = new DeckEditorSettingsPage; - auto *storagePage = new StorageSettingsPage; - auto *messagesPage = new MessagesSettingsPage; - auto *soundPage = new SoundSettingsPage; - auto *shortcutsPage = new ShortcutSettingsPage; - - pages.append(generalPage); - pages.append(appearancePage); - pages.append(userInterfacePage); - pages.append(deckEditorPage); - pages.append(storagePage); - pages.append(messagesPage); - pages.append(soundPage); - pages.append(shortcutsPage); - - pagesWidget->addWidget(makeScrollable(generalPage)); - pagesWidget->addWidget(makeScrollable(appearancePage)); - pagesWidget->addWidget(makeScrollable(userInterfacePage)); - pagesWidget->addWidget(makeScrollable(deckEditorPage)); - pagesWidget->addWidget(makeScrollable(storagePage)); - pagesWidget->addWidget(makeScrollable(messagesPage)); - pagesWidget->addWidget(soundPage); - pagesWidget->addWidget(shortcutsPage); - - Q_ASSERT(pages.size() == NumPages); - - // Search results view (hidden by default) - searchResultsView = new QListView; - searchResultsView->setUniformItemSizes(false); - searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection); - searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - searchResultsView->setVisible(false); - searchResultsView->setStyleSheet( - "QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); - - searchModel = new SettingsSearchModel(this); - searchDelegate = new SettingsSearchDelegate(this); - searchResultsView->setModel(searchModel); - searchResultsView->setItemDelegate(searchDelegate); - connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked); - - connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] { - const QStringList icons = pageIconResources(); - for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) { - tabButtons[i]->setIcon(QPixmap(icons[i])); - } - searchDelegate->setPageIcons(icons); - searchResultsView->viewport()->update(); - }); - - // Build search index after pages are created - buildSearchIndex(); - - // Pages container (stacked widget + search results overlay) - pagesContainer = new QWidget; - auto *containerLayout = new QStackedLayout; - containerLayout->setStackingMode(QStackedLayout::StackAll); - containerLayout->addWidget(pagesWidget); - containerLayout->addWidget(searchResultsView); - pagesContainer->setLayout(containerLayout); + settingsView = new SettingsView(this); // Bottom buttons auto *buttonBox = new QHBoxLayout; @@ -211,241 +47,25 @@ void DlgSettings::setupUi() // Main layout auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(searchLayout); - mainLayout->addWidget(tabBarWidget); - auto *separator = new QFrame; - separator->setFrameShape(QFrame::HLine); - separator->setFrameShadow(QFrame::Sunken); - mainLayout->addWidget(separator); - mainLayout->addWidget(pagesContainer); + mainLayout->addWidget(settingsView); mainLayout->addSpacing(4); mainLayout->addLayout(buttonBox); setLayout(mainLayout); - // Keyboard shortcuts - auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this); - connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus)); + connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); + retranslateUi(); - auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this); - connect(nextTabShortcut, &QShortcut::activated, this, [this] { - int next = (currentTabIndex + 1) % tabButtons.size(); - setActiveTab(next); - }); - - auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this); - connect(prevTabShortcut, &QShortcut::activated, this, [this] { - int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size(); - setActiveTab(prev); - }); - - // Initialize to first tab - setActiveTab(0); -} - -void DlgSettings::setupTabBar() -{ - tabBarWidget = new QWidget; - auto *tabLayout = new QHBoxLayout; - tabLayout->setContentsMargins(0, 0, 0, 0); - tabLayout->setSpacing(2); - - const QStringList iconResources = pageIconResources(); - - for (int i = 0; i < iconResources.size(); ++i) { - auto *tabButton = new QToolButton; - tabButton->setCheckable(true); - tabButton->setIcon(QPixmap(iconResources[i])); - tabButton->setIconSize(QSize(48, 48)); - tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); - tabButton->setAutoExclusive(true); - tabButton->setMinimumHeight(85); - tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); }); - - tabButtons.append(tabButton); - tabLayout->addWidget(tabButton); - } - - tabBarWidget->setLayout(tabLayout); -} - -void DlgSettings::buildSearchIndex() -{ - QList allEntries; - - const QStringList pageNames = translatedPageNames(); - searchDelegate->setPageNames(pageNames); - searchDelegate->setPageIcons(pageIconResources()); - - for (int i = 0; i < pages.size(); ++i) { - QList pageEntries = pages[i]->getSearchEntries(); - for (auto &entry : pageEntries) { - if (entry.pageIndex == -1) { - entry.pageIndex = i; - } - } - allEntries.append(pageEntries); - } - - searchModel->setSourceEntries(allEntries); -} - -void DlgSettings::onTabClicked(int index) -{ - if (searchActive) { - switchToTabMode(); - } - setActiveTab(index); -} - -void DlgSettings::setActiveTab(int index) -{ - if (index < 0 || index >= tabButtons.size()) { - return; - } - - currentTabIndex = index; - pagesWidget->setCurrentIndex(index); - - for (int i = 0; i < tabButtons.size(); ++i) { - tabButtons[i]->setChecked(i == index); - } - - // Style active tab with a thick accent border + subtle background tint - for (int i = 0; i < tabButtons.size(); ++i) { - if (i == index) { - tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); " - "border-top-left-radius: 4px; border-top-right-radius: 4px; " - "background: palette(window); padding-bottom: 1px; }"); - } else { - tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; " - "border-top-left-radius: 4px; border-top-right-radius: 4px; " - "background: transparent; }"); - } - } -} - -void DlgSettings::flashWidget(QWidget *widget) -{ - auto *overlay = new QWidget(widget); - overlay->setGeometry(widget->rect()); - overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true); - - QPalette pal = overlay->palette(); - QColor flashColor = pal.color(QPalette::Highlight); - flashColor.setAlpha(100); - pal.setBrush(QPalette::Window, flashColor); - overlay->setPalette(pal); - overlay->setAutoFillBackground(true); - - auto *effect = new QGraphicsOpacityEffect(overlay); - effect->setOpacity(0.0); - overlay->setGraphicsEffect(effect); - overlay->show(); - overlay->raise(); - - auto *flashIn = new QPropertyAnimation(effect, "opacity"); - flashIn->setDuration(120); - flashIn->setStartValue(0.0); - flashIn->setEndValue(0.6); - flashIn->setEasingCurve(QEasingCurve::OutCubic); - - auto *fadeOut = new QPropertyAnimation(effect, "opacity"); - fadeOut->setDuration(900); - fadeOut->setStartValue(0.6); - fadeOut->setEndValue(0.0); - fadeOut->setEasingCurve(QEasingCurve::InCubic); - - auto *group = new QSequentialAnimationGroup(overlay); - group->addAnimation(flashIn); - group->addAnimation(fadeOut); - - connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater); - - group->start(QAbstractAnimation::DeleteWhenStopped); -} - -void DlgSettings::onSearchTextChanged(const QString &text) -{ - searchModel->setFilterString(text); - - if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) { - if (!searchActive) { - switchToSearchMode(); - } - if (searchModel->rowCount(QModelIndex()) > 0) { - searchResultsView->setCurrentIndex(searchModel->index(0)); - } - } else if (searchActive) { - switchToTabMode(); - } -} - -void DlgSettings::switchToSearchMode() -{ - searchActive = true; - tabBarWidget->setVisible(false); - pagesWidget->setVisible(false); - searchResultsView->setVisible(true); - if (searchModel->rowCount(QModelIndex()) > 0) { - searchResultsView->setCurrentIndex(searchModel->index(0)); - } -} - -void DlgSettings::switchToTabMode() -{ - searchActive = false; - tabBarWidget->setVisible(true); - pagesWidget->setVisible(true); - searchResultsView->setVisible(false); - searchEdit->blockSignals(true); - searchEdit->clear(); - searchEdit->blockSignals(false); - setActiveTab(currentTabIndex); -} - -void DlgSettings::onSearchResultClicked(const QModelIndex &index) -{ - navigateToSearchResult(index); -} - -void DlgSettings::navigateToSearchResult(const QModelIndex &index) -{ - SettingsSearchEntry entry = searchModel->entryForIndex(index); - if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) { - return; - } - - // Switch to the page - switchToTabMode(); - setActiveTab(entry.pageIndex); - - // Scroll to the widget, focus it, and flash to highlight it - if (entry.widget) { - QWidget *widget = entry.widget; - while (widget) { - if (auto *scrollArea = qobject_cast(widget)) { - scrollArea->ensureWidgetVisible(entry.widget); - break; - } - widget = widget->parentWidget(); - } - entry.widget->setFocus(); - flashWidget(entry.widget); - } + adjustSize(); } void DlgSettings::setTab(int index) { - if (index >= 0 && index < tabButtons.size()) { - setActiveTab(index); - } + settingsView->setTab(index); } AbstractSettingsPage *DlgSettings::page(SettingsPage which) const { - return pages.value(static_cast(which)); + return settingsView->page(which); } void DlgSettings::updateLanguage() @@ -454,49 +74,6 @@ void DlgSettings::updateLanguage() installNewTranslator(); } -bool DlgSettings::eventFilter(QObject *watched, QEvent *event) -{ - if (watched == searchEdit && event->type() == QEvent::KeyPress) { - auto *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Escape) { - if (searchActive) { - switchToTabMode(); - return true; - } - } else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { - if (searchActive) { - if (searchResultsView->currentIndex().isValid()) { - navigateToSearchResult(searchResultsView->currentIndex()); - } - return true; - } - } else if (keyEvent->key() == Qt::Key_Down) { - if (searchActive) { - int nextRow = searchResultsView->currentIndex().row() + 1; - if (nextRow >= searchModel->rowCount()) { - nextRow = 0; - } - searchResultsView->setCurrentIndex(searchModel->index(nextRow)); - searchResultsView->scrollTo(searchModel->index(nextRow)); - return true; - } - } else if (keyEvent->key() == Qt::Key_Up) { - if (searchActive) { - int prevRow = searchResultsView->currentIndex().row() - 1; - if (prevRow < 0) { - prevRow = searchModel->rowCount() - 1; - } - if (prevRow >= 0) { - searchResultsView->setCurrentIndex(searchModel->index(prevRow)); - searchResultsView->scrollTo(searchModel->index(prevRow)); - } - return true; - } - } - } - return QDialog::eventFilter(watched, event); -} - void DlgSettings::closeEvent(QCloseEvent *event) { bool showLoadError = true; @@ -576,26 +153,5 @@ void DlgSettings::closeEvent(QCloseEvent *event) void DlgSettings::retranslateUi() { setWindowTitle(tr("Settings")); - retranslateTabNames(); - - searchEdit->setPlaceholderText(tr("Search settings...")); okButton->setText(tr("OK")); - - // Rebuild search index for translated text - buildSearchIndex(); -} - -QStringList DlgSettings::translatedPageNames() -{ - return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"), - tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")}; -} - -void DlgSettings::retranslateTabNames() -{ - const QStringList tabLabels = translatedPageNames(); - - for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { - tabButtons[i]->setText(tabLabels[i]); - } } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 1f15e76d5..9942b410c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -1,105 +1,59 @@ /** * @file dlg_settings.h - * @brief Main settings dialog for the Cockatrice client + * @brief Main settings dialog for the Cockatrice client (thin frame around SettingsView) * @ingroup Dialogs */ #ifndef DLG_SETTINGS_H #define DLG_SETTINGS_H +#include "settings_view.h" + #include -#include #include -#include -#include #include class QPushButton; -class QModelIndex; -class QWidget; +class AbstractSettingsPage; inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); -class QStackedWidget; -class QToolButton; -class QListView; -class QLineEdit; -class AbstractSettingsPage; -class SettingsSearchModel; -class SettingsSearchDelegate; - /** - * @brief Main application settings dialog with tabbed navigation and search + * @brief Main application settings dialog hosting the shared SettingsView * - * Provides a modern settings interface organized into tabbed pages. Users can - * either navigate by clicking tabs or search for specific settings using the - * built-in search bar. Search results are filtered and ranked by relevance. + * Thin QDialog frame around SettingsView: only the window title, the OK button + * and the close validation live here. All pages, navigation and search are + * owned by SettingsView so other hosts (e.g. a settings tab) can reuse them. */ class DlgSettings : public QDialog { Q_OBJECT public: - /** - * @brief Page order in the tab bar, matching the order pages are added in setupUi() - * - * Use these values instead of raw indices so reordering pages never silently - * breaks external callers like tab_room.cpp. - */ - enum SettingsPage - { - GeneralPage = 0, - AppearancePage, - UserInterfacePage, - DeckEditorPage, - StoragePage, - MessagesPage, - SoundPage, - ShortcutsPage, - NumPages - }; + using SettingsPage = SettingsView::SettingsPage; + static constexpr SettingsPage GeneralPage = SettingsView::GeneralPage; + static constexpr SettingsPage AppearancePage = SettingsView::AppearancePage; + static constexpr SettingsPage UserInterfacePage = SettingsView::UserInterfacePage; + static constexpr SettingsPage DeckEditorPage = SettingsView::DeckEditorPage; + static constexpr SettingsPage StoragePage = SettingsView::StoragePage; + static constexpr SettingsPage MessagesPage = SettingsView::MessagesPage; + static constexpr SettingsPage SoundPage = SettingsView::SoundPage; + static constexpr SettingsPage ShortcutsPage = SettingsView::ShortcutsPage; + static constexpr SettingsPage NumPages = SettingsView::NumPages; explicit DlgSettings(QWidget *parent = nullptr); void setTab(int index); AbstractSettingsPage *page(SettingsPage which) const; private slots: - void onTabClicked(int index); - void onSearchTextChanged(const QString &text); - void onSearchResultClicked(const QModelIndex &index); void updateLanguage(); private: - // UI elements - QLineEdit *searchEdit; ///< Search bar for filtering settings - QWidget *tabBarWidget; ///< Container widget for the tab buttons - QList tabButtons; ///< Navigation tab buttons - QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages - QListView *searchResultsView; ///< Search results list view - QWidget *pagesContainer; ///< Container stacking pages and search results - QPushButton *okButton; ///< Button to close the dialog - - // Data - QList pages; ///< All settings page instances - SettingsSearchModel *searchModel; ///< Model for search results - SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering - int currentTabIndex; ///< Currently active tab index - bool searchActive; ///< Whether search mode is active - - void setupUi(); - void setupTabBar(); - void buildSearchIndex(); - void switchToTabMode(); - void switchToSearchMode(); - void navigateToSearchResult(const QModelIndex &index); - void setActiveTab(int index); - static void flashWidget(QWidget *widget); - static QStringList translatedPageNames(); + SettingsView *settingsView; ///< Shared settings content + QPushButton *okButton; ///< Button to close the dialog void retranslateUi(); - void retranslateTabNames(); protected: void closeEvent(QCloseEvent *event) override; - bool eventFilter(QObject *watched, QEvent *event) override; }; #endif // DLG_SETTINGS_H diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp new file mode 100644 index 000000000..46b964e7e --- /dev/null +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -0,0 +1,498 @@ +/** + * @file settings_view.cpp + * @brief Implementation of the settings content widget + * @ingroup Dialogs + */ +#include "settings_view.h" + +#include "../../../client/settings/cache_settings.h" +#include "../../pixel_map_generator.h" +#include "../settings_page/abstract_settings_page.h" +#include "../settings_page/appearance_settings_page.h" +#include "../settings_page/deck_editor_settings_page.h" +#include "../settings_page/general_settings_page.h" +#include "../settings_page/messages_settings_page.h" +#include "../settings_page/settings_search_delegate.h" +#include "../settings_page/settings_search_model.h" +#include "../settings_page/shortcut_settings_page.h" +#include "../settings_page/sound_settings_page.h" +#include "../settings_page/storage_settings_page.h" +#include "../settings_page/user_interface_settings_page.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Wraps a widget in a scroll area for long settings pages + * @param widget The widget to wrap + * @return The scroll area containing the widget + */ +static QScrollArea *makeScrollable(QWidget *widget) +{ + widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); + + auto *scrollArea = new QScrollArea; + scrollArea->setWidgetResizable(true); + scrollArea->setContentsMargins(0, 0, 0, 0); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->horizontalScrollBar()->setEnabled(false); + scrollArea->setWidget(widget); + return scrollArea; +} + +/** + * @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order + */ +static QStringList pageIconResources() +{ + return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"), + QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"), + QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"), + QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")}; +} + +SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false) +{ + setupUi(); + + connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, + &SettingsView::retranslateUi); + retranslateUi(); + + searchEdit->setFocus(); +} + +void SettingsView::setupUi() +{ + // Search bar + searchEdit = new QLineEdit; + searchEdit->setClearButtonEnabled(true); + searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); + searchEdit->installEventFilter(this); + connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged); + + auto *searchLayout = new QHBoxLayout; + searchLayout->addWidget(searchEdit); + + // Tab bar (built in setupTabBar) + setupTabBar(); + + // Pages stacked widget + pagesWidget = new QStackedWidget; + + auto *generalPage = new GeneralSettingsPage; + auto *appearancePage = new AppearanceSettingsPage; + auto *userInterfacePage = new UserInterfaceSettingsPage; + auto *deckEditorPage = new DeckEditorSettingsPage; + auto *storagePage = new StorageSettingsPage; + auto *messagesPage = new MessagesSettingsPage; + auto *soundPage = new SoundSettingsPage; + auto *shortcutsPage = new ShortcutSettingsPage; + + pages.append(generalPage); + pages.append(appearancePage); + pages.append(userInterfacePage); + pages.append(deckEditorPage); + pages.append(storagePage); + pages.append(messagesPage); + pages.append(soundPage); + pages.append(shortcutsPage); + + pagesWidget->addWidget(makeScrollable(generalPage)); + pagesWidget->addWidget(makeScrollable(appearancePage)); + pagesWidget->addWidget(makeScrollable(userInterfacePage)); + pagesWidget->addWidget(makeScrollable(deckEditorPage)); + pagesWidget->addWidget(makeScrollable(storagePage)); + pagesWidget->addWidget(makeScrollable(messagesPage)); + pagesWidget->addWidget(soundPage); + pagesWidget->addWidget(shortcutsPage); + + Q_ASSERT(pages.size() == NumPages); + + // Search results view (hidden by default) + searchResultsView = new QListView; + searchResultsView->setUniformItemSizes(false); + searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection); + searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + searchResultsView->setVisible(false); + searchResultsView->setStyleSheet( + "QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); + + searchModel = new SettingsSearchModel(this); + searchDelegate = new SettingsSearchDelegate(this); + searchResultsView->setModel(searchModel); + searchResultsView->setItemDelegate(searchDelegate); + connect(searchResultsView, &QListView::clicked, this, &SettingsView::onSearchResultClicked); + + connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] { + const QStringList icons = pageIconResources(); + for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) { + tabButtons[i]->setIcon(QPixmap(icons[i])); + } + searchDelegate->setPageIcons(icons); + searchResultsView->viewport()->update(); + }); + + // Build search index after pages are created + buildSearchIndex(); + + // Pages container (stacked widget + search results overlay) + pagesContainer = new QWidget; + auto *containerLayout = new QStackedLayout; + containerLayout->setStackingMode(QStackedLayout::StackAll); + containerLayout->addWidget(pagesWidget); + containerLayout->addWidget(searchResultsView); + pagesContainer->setLayout(containerLayout); + + // Main layout + auto *mainLayout = new QVBoxLayout; + mainLayout->addLayout(searchLayout); + mainLayout->addWidget(tabBarWidget); + auto *separator = new QFrame; + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Sunken); + mainLayout->addWidget(separator); + mainLayout->addWidget(pagesContainer); + mainLayout->addSpacing(4); + setLayout(mainLayout); + + // Keyboard shortcuts + auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this); + connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus)); + + auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this); + connect(nextTabShortcut, &QShortcut::activated, this, [this] { + int next = (currentTabIndex + 1) % tabButtons.size(); + setActiveTab(next); + }); + + auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this); + connect(prevTabShortcut, &QShortcut::activated, this, [this] { + int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size(); + setActiveTab(prev); + }); + + // Initialize to first tab + setActiveTab(0); +} + +void SettingsView::setupTabBar() +{ + tabBarWidget = new QWidget; + auto *tabLayout = new QHBoxLayout; + tabLayout->setContentsMargins(0, 0, 0, 0); + tabLayout->setSpacing(2); + + const QStringList iconResources = pageIconResources(); + + for (int i = 0; i < iconResources.size(); ++i) { + auto *tabButton = new QToolButton; + tabButton->setCheckable(true); + tabButton->setIcon(QPixmap(iconResources[i])); + tabButton->setIconSize(QSize(48, 48)); + tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + tabButton->setAutoExclusive(true); + tabButton->setMinimumHeight(85); + tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); }); + + tabButtons.append(tabButton); + tabLayout->addWidget(tabButton); + } + + tabBarWidget->setLayout(tabLayout); +} + +void SettingsView::buildSearchIndex() +{ + QList allEntries; + + const QStringList pageNames = translatedPageNames(); + searchDelegate->setPageNames(pageNames); + searchDelegate->setPageIcons(pageIconResources()); + + for (int i = 0; i < pages.size(); ++i) { + QList pageEntries = pages[i]->getSearchEntries(); + for (auto &entry : pageEntries) { + if (entry.pageIndex == -1) { + entry.pageIndex = i; + } + } + allEntries.append(pageEntries); + } + + searchModel->setSourceEntries(allEntries); +} + +void SettingsView::onTabClicked(int index) +{ + if (searchActive) { + switchToTabMode(); + } + setActiveTab(index); +} + +void SettingsView::setActiveTab(int index) +{ + if (index < 0 || index >= tabButtons.size()) { + return; + } + + currentTabIndex = index; + pagesWidget->setCurrentIndex(index); + + for (int i = 0; i < tabButtons.size(); ++i) { + tabButtons[i]->setChecked(i == index); + } + + // Style active tab with a thick accent border + subtle background tint + for (int i = 0; i < tabButtons.size(); ++i) { + if (i == index) { + tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); " + "border-top-left-radius: 4px; border-top-right-radius: 4px; " + "background: palette(window); padding-bottom: 1px; }"); + } else { + tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; " + "border-top-left-radius: 4px; border-top-right-radius: 4px; " + "background: transparent; }"); + } + } +} + +void SettingsView::flashWidget(QWidget *widget) +{ + auto *overlay = new QWidget(widget); + overlay->setGeometry(widget->rect()); + overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true); + + QPalette pal = overlay->palette(); + QColor flashColor = pal.color(QPalette::Highlight); + flashColor.setAlpha(100); + pal.setBrush(QPalette::Window, flashColor); + overlay->setPalette(pal); + overlay->setAutoFillBackground(true); + + auto *effect = new QGraphicsOpacityEffect(overlay); + effect->setOpacity(0.0); + overlay->setGraphicsEffect(effect); + overlay->show(); + overlay->raise(); + + auto *flashIn = new QPropertyAnimation(effect, "opacity"); + flashIn->setDuration(120); + flashIn->setStartValue(0.0); + flashIn->setEndValue(0.6); + flashIn->setEasingCurve(QEasingCurve::OutCubic); + + auto *fadeOut = new QPropertyAnimation(effect, "opacity"); + fadeOut->setDuration(900); + fadeOut->setStartValue(0.6); + fadeOut->setEndValue(0.0); + fadeOut->setEasingCurve(QEasingCurve::InCubic); + + auto *group = new QSequentialAnimationGroup(overlay); + group->addAnimation(flashIn); + group->addAnimation(fadeOut); + + connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater); + + group->start(QAbstractAnimation::DeleteWhenStopped); +} + +void SettingsView::onSearchTextChanged(const QString &text) +{ + searchModel->setFilterString(text); + + if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) { + if (!searchActive) { + switchToSearchMode(); + } + if (searchModel->rowCount(QModelIndex()) > 0) { + searchResultsView->setCurrentIndex(searchModel->index(0)); + } + } else if (searchActive) { + switchToTabMode(); + } +} + +void SettingsView::switchToSearchMode() +{ + searchActive = true; + tabBarWidget->setVisible(false); + pagesWidget->setVisible(false); + searchResultsView->setVisible(true); + if (searchModel->rowCount(QModelIndex()) > 0) { + searchResultsView->setCurrentIndex(searchModel->index(0)); + } +} + +void SettingsView::switchToTabMode() +{ + searchActive = false; + tabBarWidget->setVisible(true); + pagesWidget->setVisible(true); + searchResultsView->setVisible(false); + searchEdit->blockSignals(true); + searchEdit->clear(); + searchEdit->blockSignals(false); + setActiveTab(currentTabIndex); +} + +void SettingsView::onSearchResultClicked(const QModelIndex &index) +{ + navigateToSearchResult(index); +} + +void SettingsView::navigateToSearchResult(const QModelIndex &index) +{ + SettingsSearchEntry entry = searchModel->entryForIndex(index); + if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) { + return; + } + + // Switch to the page + switchToTabMode(); + setActiveTab(entry.pageIndex); + + // Scroll to the widget, focus it, and flash to highlight it + if (entry.widget) { + QWidget *widget = entry.widget; + while (widget) { + if (auto *scrollArea = qobject_cast(widget)) { + scrollArea->ensureWidgetVisible(entry.widget); + break; + } + widget = widget->parentWidget(); + } + entry.widget->setFocus(); + flashWidget(entry.widget); + } +} + +void SettingsView::setTab(int index) +{ + if (index >= 0 && index < tabButtons.size()) { + setActiveTab(index); + } +} + +AbstractSettingsPage *SettingsView::page(SettingsPage which) const +{ + return pages.value(static_cast(which)); +} + +bool SettingsView::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + if (searchActive) { + switchToTabMode(); + return true; + } + } else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + if (searchActive) { + if (searchResultsView->currentIndex().isValid()) { + navigateToSearchResult(searchResultsView->currentIndex()); + } + return true; + } + } else if (keyEvent->key() == Qt::Key_Down) { + if (searchActive) { + int nextRow = searchResultsView->currentIndex().row() + 1; + if (nextRow >= searchModel->rowCount()) { + nextRow = 0; + } + searchResultsView->setCurrentIndex(searchModel->index(nextRow)); + searchResultsView->scrollTo(searchModel->index(nextRow)); + return true; + } + } else if (keyEvent->key() == Qt::Key_Up) { + if (searchActive) { + int prevRow = searchResultsView->currentIndex().row() - 1; + if (prevRow < 0) { + prevRow = searchModel->rowCount() - 1; + } + if (prevRow >= 0) { + searchResultsView->setCurrentIndex(searchModel->index(prevRow)); + searchResultsView->scrollTo(searchModel->index(prevRow)); + } + return true; + } + } + } + return QWidget::eventFilter(watched, event); +} + +void SettingsView::retranslateUi() +{ + retranslateTabNames(); + + // NOTE: the "DlgSettings" context is deliberate — these strings lived in DlgSettings + // before the view was extracted, and keeping the context preserves existing translations. + searchEdit->setPlaceholderText(QCoreApplication::translate("DlgSettings", "Search settings...")); + + // Rebuild search index for translated text + buildSearchIndex(); +} + +QStringList SettingsView::translatedPageNames() +{ + // NOTE: same "DlgSettings" context as above (translation continuity, see retranslateUi). + return {QCoreApplication::translate("DlgSettings", "General"), + QCoreApplication::translate("DlgSettings", "Appearance"), + QCoreApplication::translate("DlgSettings", "User Interface"), + QCoreApplication::translate("DlgSettings", "Card Sources"), + QCoreApplication::translate("DlgSettings", "Storage"), + QCoreApplication::translate("DlgSettings", "Chat"), + QCoreApplication::translate("DlgSettings", "Sound"), + QCoreApplication::translate("DlgSettings", "Shortcuts")}; +} + +void SettingsView::retranslateTabNames() +{ + const QStringList tabLabels = translatedPageNames(); + + for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { + tabButtons[i]->setText(tabLabels[i]); + } +} diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.h b/cockatrice/src/interface/widgets/dialogs/settings_view.h new file mode 100644 index 000000000..fe1b925d8 --- /dev/null +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.h @@ -0,0 +1,98 @@ +/** + * @file settings_view.h + * @brief Settings content widget hosted by the settings dialog (and, later, the settings tab) + * @ingroup Dialogs + */ +#ifndef SETTINGS_VIEW_H +#define SETTINGS_VIEW_H + +#include +#include +#include +#include +#include + +class QModelIndex; +class QObject; +class QEvent; + +class QStackedWidget; +class QToolButton; +class QListView; +class QLineEdit; +class AbstractSettingsPage; +class SettingsSearchModel; +class SettingsSearchDelegate; + +/** + * @brief Settings pages with tabbed navigation and search, without any dialog chrome + * + * Owns the search bar, the tab bar, the stacked settings pages and the search + * results view. Dialog/tab hosts only add their own frame (buttons, titles). + */ +class SettingsView : public QWidget +{ + Q_OBJECT +public: + /** + * @brief Page order in the tab bar, matching the order pages are added in setupUi() + * + * Use these values instead of raw indices so reordering pages never silently + * breaks external callers like tab_room.cpp. + */ + enum SettingsPage + { + GeneralPage = 0, + AppearancePage, + UserInterfacePage, + DeckEditorPage, + StoragePage, + MessagesPage, + SoundPage, + ShortcutsPage, + NumPages + }; + + explicit SettingsView(QWidget *parent = nullptr); + void setTab(int index); + AbstractSettingsPage *page(SettingsPage which) const; + +private slots: + void onTabClicked(int index); + void onSearchTextChanged(const QString &text); + void onSearchResultClicked(const QModelIndex &index); + +private: + // UI elements + QLineEdit *searchEdit; ///< Search bar for filtering settings + QWidget *tabBarWidget; ///< Container widget for the tab buttons + QList tabButtons; ///< Navigation tab buttons + QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages + QListView *searchResultsView; ///< Search results list view + QWidget *pagesContainer; ///< Container stacking pages and search results + + // Data + QList pages; ///< All settings page instances + SettingsSearchModel *searchModel; ///< Model for search results + SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering + int currentTabIndex; ///< Currently active tab index + bool searchActive; ///< Whether search mode is active + + void setupUi(); + void setupTabBar(); + void buildSearchIndex(); + void switchToTabMode(); + void switchToSearchMode(); + void navigateToSearchResult(const QModelIndex &index); + void setActiveTab(int index); + static void flashWidget(QWidget *widget); + static QStringList translatedPageNames(); + + void retranslateUi(); + void retranslateTabNames(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; +}; + +#endif // SETTINGS_VIEW_H From 64f58535dd40e0f5e3f4e0795d3d0baf0e35db1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 11 Oct 2026 13:03:35 +0200 Subject: [PATCH 2/6] [Client] Settings: match CI clang-format-18 line breaking --- cockatrice/src/interface/widgets/dialogs/settings_view.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index 46b964e7e..c2d9860c3 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -93,8 +93,7 @@ SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0 { setupUi(); - connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, - &SettingsView::retranslateUi); + connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi); retranslateUi(); searchEdit->setFocus(); From 7a68486a871676778a777c2afcb421047d45ab08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 15 Oct 2026 07:09:54 +0200 Subject: [PATCH 3/6] [Client] Settings: enlarge the settings dialog Keep settings modal per review: size to ~90% of the main window (5% margins each side) with screen fallback, size grip and 750x700 minimum bounded to the screen. Drops the tab approach while giving dense pages room without the tab-switch friction. --- .../widgets/dialogs/dlg_settings.cpp | 26 ++++++++++++++++--- .../interface/widgets/dialogs/dlg_settings.h | 3 ++- .../interface/widgets/dialogs/settings_view.h | 4 +-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 9b20372db..444c624df 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -30,8 +31,27 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) { - auto rec = QGuiApplication::primaryScreen()->availableGeometry(); - setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height())); + // Fill the main window with small margins so multi-group pages (e.g. Deck Editor) are not buried in a scroll. + // 90% of the host window leaves ~5% margins on each side; falls back to 90% of the screen when parentless. + QWidget *hostWindow = parent ? parent->window() : nullptr; + QScreen *screen = nullptr; + QSize hostSize; + if (hostWindow) { + hostSize = hostWindow->size(); + screen = QGuiApplication::screenAt(hostWindow->geometry().center()); + } + if (!screen) { + screen = QGuiApplication::primaryScreen(); + } + const QRect available = screen ? screen->availableGeometry() : QRect(0, 0, 1280, 800); + const QSize minimum(qMin(750, available.width()), qMin(700, available.height())); + setMinimumSize(minimum); + + QSize target = !hostSize.isEmpty() ? QSize(qRound(hostSize.width() * 0.9), qRound(hostSize.height() * 0.9)) + : QSize(qRound(available.width() * 0.9), qRound(available.height() * 0.9)); + target = target.expandedTo(minimum).boundedTo(available.size()); + resize(target); + setSizeGripEnabled(true); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); @@ -54,8 +74,6 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); retranslateUi(); - - adjustSize(); } void DlgSettings::setTab(int index) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 9942b410c..03ccd6b7e 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -22,7 +22,8 @@ inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); * * Thin QDialog frame around SettingsView: only the window title, the OK button * and the close validation live here. All pages, navigation and search are - * owned by SettingsView so other hosts (e.g. a settings tab) can reuse them. + * owned by SettingsView. The dialog fills ~90% of the main window so dense + * pages stay visible without losing the modal adjust-and-return flow. */ class DlgSettings : public QDialog { diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.h b/cockatrice/src/interface/widgets/dialogs/settings_view.h index fe1b925d8..f1d248679 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.h +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.h @@ -1,6 +1,6 @@ /** * @file settings_view.h - * @brief Settings content widget hosted by the settings dialog (and, later, the settings tab) + * @brief Settings content widget hosted by the settings dialog * @ingroup Dialogs */ #ifndef SETTINGS_VIEW_H @@ -28,7 +28,7 @@ class SettingsSearchDelegate; * @brief Settings pages with tabbed navigation and search, without any dialog chrome * * Owns the search bar, the tab bar, the stacked settings pages and the search - * results view. Dialog/tab hosts only add their own frame (buttons, titles). + * results view. The settings dialog only adds its own frame (buttons, titles). */ class SettingsView : public QWidget { From 64a458f39d5f08ecae3f330d827bc05c05b5cbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 15 Oct 2026 07:14:54 +0200 Subject: [PATCH 4/6] [Client] Settings: grouped left navigation for settings pages Replace the top icon-button bar in SettingsView with a grouped left QTreeWidget: Application, Cards and Data, Chat and Sound, Advanced. The active page is the selected row (palette highlight); search mode hides the tree; group headers expand but never select. Page tr() contexts stay DlgSettings; new group names are SettingsView strings. Reworked to stay in the enlarged modal instead of the settings tab. --- .../widgets/dialogs/settings_view.cpp | 146 +++++++++++------- .../interface/widgets/dialogs/settings_view.h | 27 ++-- 2 files changed, 101 insertions(+), 72 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index c2d9860c3..5861e070b 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -53,7 +52,7 @@ #include #include #include -#include +#include #include #include #include @@ -111,8 +110,8 @@ void SettingsView::setupUi() auto *searchLayout = new QHBoxLayout; searchLayout->addWidget(searchEdit); - // Tab bar (built in setupTabBar) - setupTabBar(); + // Navigation tree (built in setupNavTree) + setupNavTree(); // Pages stacked widget pagesWidget = new QStackedWidget; @@ -163,8 +162,10 @@ void SettingsView::setupUi() connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] { const QStringList icons = pageIconResources(); - for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) { - tabButtons[i]->setIcon(QPixmap(icons[i])); + for (int i = 0; i < pageItems.size() && i < icons.size(); ++i) { + if (pageItems[i]) { + pageItems[i]->setIcon(0, QPixmap(icons[i])); + } } searchDelegate->setPageIcons(icons); searchResultsView->viewport()->update(); @@ -181,16 +182,14 @@ void SettingsView::setupUi() containerLayout->addWidget(searchResultsView); pagesContainer->setLayout(containerLayout); - // Main layout + // Main layout: search on top, navigation + pages side by side below + auto *contentLayout = new QHBoxLayout; + contentLayout->addWidget(navTree); + contentLayout->addWidget(pagesContainer, 1); + auto *mainLayout = new QVBoxLayout; mainLayout->addLayout(searchLayout); - mainLayout->addWidget(tabBarWidget); - auto *separator = new QFrame; - separator->setFrameShape(QFrame::HLine); - separator->setFrameShadow(QFrame::Sunken); - mainLayout->addWidget(separator); - mainLayout->addWidget(pagesContainer); - mainLayout->addSpacing(4); + mainLayout->addLayout(contentLayout); setLayout(mainLayout); // Keyboard shortcuts @@ -199,13 +198,13 @@ void SettingsView::setupUi() auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this); connect(nextTabShortcut, &QShortcut::activated, this, [this] { - int next = (currentTabIndex + 1) % tabButtons.size(); + int next = (currentTabIndex + 1) % pages.size(); setActiveTab(next); }); auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this); connect(prevTabShortcut, &QShortcut::activated, this, [this] { - int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size(); + int prev = (currentTabIndex - 1 + pages.size()) % pages.size(); setActiveTab(prev); }); @@ -213,32 +212,56 @@ void SettingsView::setupUi() setActiveTab(0); } -void SettingsView::setupTabBar() +void SettingsView::setupNavTree() { - tabBarWidget = new QWidget; - auto *tabLayout = new QHBoxLayout; - tabLayout->setContentsMargins(0, 0, 0, 0); - tabLayout->setSpacing(2); + navTree = new QTreeWidget; + navTree->setHeaderHidden(true); + navTree->setRootIsDecorated(true); + navTree->setIconSize(QSize(24, 24)); + navTree->setMinimumWidth(230); + navTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + navTree->setStyleSheet( + "QTreeWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); + // Groups of SettingsPage values, in navigation order + const QList> groupedPages = { + {GeneralPage, AppearancePage, UserInterfacePage}, + {DeckEditorPage, StoragePage}, + {MessagesPage, SoundPage}, + {ShortcutsPage}, + }; + + const QStringList groupNames = translatedGroupNames(); + const QStringList pageNames = translatedPageNames(); const QStringList iconResources = pageIconResources(); - for (int i = 0; i < iconResources.size(); ++i) { - auto *tabButton = new QToolButton; - tabButton->setCheckable(true); - tabButton->setIcon(QPixmap(iconResources[i])); - tabButton->setIconSize(QSize(48, 48)); - tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); - tabButton->setAutoExclusive(true); - tabButton->setMinimumHeight(85); - tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); }); - - tabButtons.append(tabButton); - tabLayout->addWidget(tabButton); + pageItems.resize(NumPages); + for (int i = 0; i < groupedPages.size(); ++i) { + auto *group = new QTreeWidgetItem(navTree, {groupNames[i]}); + group->setFlags(Qt::ItemIsEnabled); // header row: expandable, not selectable + group->setExpanded(true); + groupItems.append(group); + for (SettingsPage page : groupedPages[i]) { + auto *item = new QTreeWidgetItem(group, {pageNames[page]}); + item->setIcon(0, QPixmap(iconResources[page])); + item->setData(0, Qt::UserRole, static_cast(page)); + pageItems[page] = item; + } } - tabBarWidget->setLayout(tabLayout); + Q_ASSERT(groupItems.size() == groupedPages.size()); + Q_ASSERT(!pageItems.contains(nullptr)); + + connect(navTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current) { + if (!current) { + return; + } + bool ok = false; + int index = current->data(0, Qt::UserRole).toInt(&ok); + if (ok) { + onTabClicked(index); + } + }); } void SettingsView::buildSearchIndex() @@ -266,35 +289,27 @@ void SettingsView::onTabClicked(int index) { if (searchActive) { switchToTabMode(); + } else if (index == currentTabIndex) { + return; } setActiveTab(index); } void SettingsView::setActiveTab(int index) { - if (index < 0 || index >= tabButtons.size()) { + if (index < 0 || index >= pageItems.size() || !pageItems[index]) { return; } currentTabIndex = index; pagesWidget->setCurrentIndex(index); - for (int i = 0; i < tabButtons.size(); ++i) { - tabButtons[i]->setChecked(i == index); - } - - // Style active tab with a thick accent border + subtle background tint - for (int i = 0; i < tabButtons.size(); ++i) { - if (i == index) { - tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); " - "border-top-left-radius: 4px; border-top-right-radius: 4px; " - "background: palette(window); padding-bottom: 1px; }"); - } else { - tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; " - "border-top-left-radius: 4px; border-top-right-radius: 4px; " - "background: transparent; }"); - } + // Selecting the item also expands a collapsed group; selecting the already + // current item emits nothing, so the currentItemChanged handler cannot loop. + if (QTreeWidgetItem *parent = pageItems[index]->parent()) { + parent->setExpanded(true); } + navTree->setCurrentItem(pageItems[index]); } void SettingsView::flashWidget(QWidget *widget) @@ -356,7 +371,7 @@ void SettingsView::onSearchTextChanged(const QString &text) void SettingsView::switchToSearchMode() { searchActive = true; - tabBarWidget->setVisible(false); + navTree->setVisible(false); pagesWidget->setVisible(false); searchResultsView->setVisible(true); if (searchModel->rowCount(QModelIndex()) > 0) { @@ -367,7 +382,7 @@ void SettingsView::switchToSearchMode() void SettingsView::switchToTabMode() { searchActive = false; - tabBarWidget->setVisible(true); + navTree->setVisible(true); pagesWidget->setVisible(true); searchResultsView->setVisible(false); searchEdit->blockSignals(true); @@ -409,7 +424,7 @@ void SettingsView::navigateToSearchResult(const QModelIndex &index) void SettingsView::setTab(int index) { - if (index >= 0 && index < tabButtons.size()) { + if (index >= 0 && index < pageItems.size()) { setActiveTab(index); } } @@ -464,7 +479,7 @@ bool SettingsView::eventFilter(QObject *watched, QEvent *event) void SettingsView::retranslateUi() { - retranslateTabNames(); + retranslateNavNames(); // NOTE: the "DlgSettings" context is deliberate — these strings lived in DlgSettings // before the view was extracted, and keeping the context preserves existing translations. @@ -487,11 +502,22 @@ QStringList SettingsView::translatedPageNames() QCoreApplication::translate("DlgSettings", "Shortcuts")}; } -void SettingsView::retranslateTabNames() +void SettingsView::retranslateNavNames() { - const QStringList tabLabels = translatedPageNames(); + const QStringList groupNames = translatedGroupNames(); + for (int i = 0; i < groupItems.size() && i < groupNames.size(); ++i) { + groupItems[i]->setText(0, groupNames[i]); + } - for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { - tabButtons[i]->setText(tabLabels[i]); + const QStringList pageNames = translatedPageNames(); + for (int i = 0; i < pageItems.size() && i < pageNames.size(); ++i) { + if (pageItems[i]) { + pageItems[i]->setText(0, pageNames[i]); + } } } + +QStringList SettingsView::translatedGroupNames() +{ + return {tr("Application"), tr("Cards & Data"), tr("Chat & Sound"), tr("Advanced")}; +} diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.h b/cockatrice/src/interface/widgets/dialogs/settings_view.h index f1d248679..214353660 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.h +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.h @@ -17,7 +17,8 @@ class QObject; class QEvent; class QStackedWidget; -class QToolButton; +class QTreeWidget; +class QTreeWidgetItem; class QListView; class QLineEdit; class AbstractSettingsPage; @@ -25,9 +26,9 @@ class SettingsSearchModel; class SettingsSearchDelegate; /** - * @brief Settings pages with tabbed navigation and search, without any dialog chrome + * @brief Settings pages with grouped left navigation and search, without any dialog chrome * - * Owns the search bar, the tab bar, the stacked settings pages and the search + * Owns the search bar, the navigation tree, the stacked settings pages and the search * results view. The settings dialog only adds its own frame (buttons, titles). */ class SettingsView : public QWidget @@ -35,7 +36,7 @@ class SettingsView : public QWidget Q_OBJECT public: /** - * @brief Page order in the tab bar, matching the order pages are added in setupUi() + * @brief Page order in the navigation, matching the order pages are added in setupUi() * * Use these values instead of raw indices so reordering pages never silently * breaks external callers like tab_room.cpp. @@ -64,12 +65,13 @@ private slots: private: // UI elements - QLineEdit *searchEdit; ///< Search bar for filtering settings - QWidget *tabBarWidget; ///< Container widget for the tab buttons - QList tabButtons; ///< Navigation tab buttons - QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages - QListView *searchResultsView; ///< Search results list view - QWidget *pagesContainer; ///< Container stacking pages and search results + QLineEdit *searchEdit; ///< Search bar for filtering settings + QTreeWidget *navTree; ///< Grouped navigation tree for the pages + QList groupItems; ///< Group header rows, indexed by group + QList pageItems; ///< Page rows, indexed by SettingsPage + QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages + QListView *searchResultsView; ///< Search results list view + QWidget *pagesContainer; ///< Container stacking pages and search results // Data QList pages; ///< All settings page instances @@ -79,7 +81,7 @@ private: bool searchActive; ///< Whether search mode is active void setupUi(); - void setupTabBar(); + void setupNavTree(); void buildSearchIndex(); void switchToTabMode(); void switchToSearchMode(); @@ -87,9 +89,10 @@ private: void setActiveTab(int index); static void flashWidget(QWidget *widget); static QStringList translatedPageNames(); + static QStringList translatedGroupNames(); void retranslateUi(); - void retranslateTabNames(); + void retranslateNavNames(); protected: bool eventFilter(QObject *watched, QEvent *event) override; From 4ff614bb2dbf51c6906c087a75e12f2cd2b8ed64 Mon Sep 17 00:00:00 2001 From: BruebachL Date: Tue, 13 Oct 2026 11:44:23 +0200 Subject: [PATCH 5/6] [Settings] Make groupedPages static const per review --- cockatrice/src/interface/widgets/dialogs/settings_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index 5861e070b..483aa31bf 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -224,7 +224,7 @@ void SettingsView::setupNavTree() "QTreeWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); // Groups of SettingsPage values, in navigation order - const QList> groupedPages = { + static const QList> groupedPages = { {GeneralPage, AppearancePage, UserInterfacePage}, {DeckEditorPage, StoragePage}, {MessagesPage, SoundPage}, From 043c821b7c3b6f0b3e0070314604e074c358d893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 15 Oct 2026 07:15:04 +0200 Subject: [PATCH 6/6] [Client] Settings: add top-right cogwheel shortcut Add a subtle flat settings button to the tab bar corner that opens the settings dialog via the existing menu action slot. Icon and tooltip follow the menu action in retranslateUi. --- cockatrice/src/interface/window_main.cpp | 11 +++++++++++ cockatrice/src/interface/window_main.h | 2 ++ 2 files changed, 13 insertions(+) diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 189585d84..01cfaeb88 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -79,10 +79,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -352,6 +354,8 @@ void MainWindow::retranslateUi() aForgotPassword->setText(tr("&Restore password...")); aSettings->setText(tr("&Settings...")); aSettings->setIcon(themePixmap(QStringLiteral("icons/settings"))); + settingsButton->setToolTip(tr("Settings")); + settingsButton->setIcon(themePixmap(QStringLiteral("icons/settings"))); aExit->setText(tr("&Exit")); #if defined(__APPLE__) /* For OSX */ @@ -556,6 +560,13 @@ MainWindow::MainWindow(QWidget *parent) setCentralWidget(tabSupervisor); + // Subtle settings entry in the tab bar corner, mirroring the menu action + settingsButton = new QToolButton(tabSupervisor); + settingsButton->setAutoRaise(true); + settingsButton->setIconSize(QSize(20, 20)); + connect(settingsButton, &QToolButton::clicked, this, &MainWindow::actSettings); + tabSupervisor->setCornerWidget(settingsButton, Qt::TopRightCorner); + retranslateUi(); if (!restoreGeometry(SettingsCache::instance().layouts().getMainWindowGeometry())) { diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index bf68ed72a..c67146ab7 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -46,6 +46,7 @@ #include class QAction; +class QToolButton; class QWidget; struct LocalGameOptions; @@ -170,6 +171,7 @@ private: QList tabMenus; QMenu *cockatriceMenu, *dbMenu, *tabsMenu, *helpMenu, *trayIconMenu; QAction *aAbout, *aSettings, *aShow, *aExit; + QToolButton *settingsButton = nullptr; QAction *aConnect, *aDisconnect, *aRegister, *aForgotPassword, *aSinglePlayer, *aWatchReplay, *aFullScreen; QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet, *aReloadCardDatabase;