diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index eac9bc83d..af24dfc26 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -257,10 +257,13 @@ set(cockatrice_SOURCES src/interface/widgets/server/user/user_list_manager.cpp src/interface/widgets/server/user/user_list_painter.cpp src/interface/widgets/server/user/user_list_widget.cpp + src/interface/widgets/settings_page/abstract_settings_page.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp src/interface/widgets/settings_page/general_settings_page.cpp src/interface/widgets/settings_page/messages_settings_page.cpp + src/interface/widgets/settings_page/settings_search_delegate.cpp + src/interface/widgets/settings_page/settings_search_model.cpp src/interface/widgets/settings_page/shortcut_settings_page.cpp src/interface/widgets/settings_page/sound_settings_page.cpp src/interface/widgets/settings_page/storage_settings_page.cpp diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index 267b80a2c..a3242c797 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -4,7 +4,6 @@ #include "../../../client/settings/shortcuts_settings.h" #include "../../deck_loader/card_node_function.h" #include "../../deck_loader/deck_loader.h" -#include "dlg_settings.h" #include #include diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 279e10c28..883cfcd03 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -1,3 +1,8 @@ +/** + * @file dlg_settings.cpp + * @brief Implementation of the main settings dialog + * @ingroup Dialogs + */ #include "dlg_settings.h" #include "../../../client/settings/cache_settings.h" @@ -6,6 +11,8 @@ #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" @@ -13,20 +20,36 @@ #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 +/** + * @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); @@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget) return scrollArea; } -DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) +/** + * @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) { auto rec = QGuiApplication::primaryScreen()->availableGeometry(); - this->setMinimumSize(qMin(700, rec.width()), qMin(700, rec.height())); + setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height())); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); - contentsWidget = new QListWidget; - contentsWidget->setViewMode(QListView::IconMode); - contentsWidget->setIconSize(QSize(58, 50)); - contentsWidget->setMovement(QListView::Static); - contentsWidget->setMinimumHeight(85); - contentsWidget->setMaximumHeight(85); - contentsWidget->setSpacing(5); - - pagesWidget = new QStackedWidget; - pagesWidget->addWidget(makeScrollable(new GeneralSettingsPage)); - pagesWidget->addWidget(makeScrollable(new AppearanceSettingsPage)); - pagesWidget->addWidget(makeScrollable(new UserInterfaceSettingsPage)); - pagesWidget->addWidget(new DeckEditorSettingsPage); - pagesWidget->addWidget(makeScrollable(new StorageSettingsPage)); - pagesWidget->addWidget(new MessagesSettingsPage); - pagesWidget->addWidget(new SoundSettingsPage); - pagesWidget->addWidget(new ShortcutSettingsPage); - - createIcons(); - contentsWidget->setCurrentRow(0); - - auto *vboxLayout = new QVBoxLayout; - vboxLayout->addWidget(contentsWidget); - vboxLayout->addWidget(pagesWidget); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgSettings::close); - - auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(vboxLayout); - mainLayout->addSpacing(2); - mainLayout->addWidget(buttonBox); - setLayout(mainLayout); + setupUi(); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); retranslateUi(); + searchEdit->setFocus(); + adjustSize(); } -void DlgSettings::createIcons() +void DlgSettings::setupUi() { - generalButton = new QListWidgetItem(contentsWidget); - generalButton->setTextAlignment(Qt::AlignHCenter); - generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - generalButton->setIcon(QPixmap("theme:config/general")); + // Search bar + searchEdit = new QLineEdit; + searchEdit->setClearButtonEnabled(true); + searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + searchEdit->installEventFilter(this); + connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); - appearanceButton = new QListWidgetItem(contentsWidget); - appearanceButton->setTextAlignment(Qt::AlignHCenter); - appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - appearanceButton->setIcon(QPixmap("theme:config/appearance")); + auto *searchLayout = new QHBoxLayout; + searchLayout->addWidget(searchEdit); - userInterfaceButton = new QListWidgetItem(contentsWidget); - userInterfaceButton->setTextAlignment(Qt::AlignHCenter); - userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - userInterfaceButton->setIcon(QPixmap("theme:config/interface")); + // Tab bar (built in setupTabBar) + setupTabBar(); - deckEditorButton = new QListWidgetItem(contentsWidget); - deckEditorButton->setTextAlignment(Qt::AlignHCenter); - deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - deckEditorButton->setIcon(QPixmap("theme:config/deckeditor")); + // Pages stacked widget + pagesWidget = new QStackedWidget; - storageButton = new QListWidgetItem(contentsWidget); - storageButton->setTextAlignment(Qt::AlignHCenter); - storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - storageButton->setIcon(QPixmap("theme:config/storage")); + 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; - messagesButton = new QListWidgetItem(contentsWidget); - messagesButton->setTextAlignment(Qt::AlignHCenter); - messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - messagesButton->setIcon(QPixmap("theme:config/messages")); + pages.append(generalPage); + pages.append(appearancePage); + pages.append(userInterfacePage); + pages.append(deckEditorPage); + pages.append(storagePage); + pages.append(messagesPage); + pages.append(soundPage); + pages.append(shortcutsPage); - soundButton = new QListWidgetItem(contentsWidget); - soundButton->setTextAlignment(Qt::AlignHCenter); - soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - soundButton->setIcon(QPixmap("theme:config/sound")); + pagesWidget->addWidget(makeScrollable(generalPage)); + pagesWidget->addWidget(makeScrollable(appearancePage)); + pagesWidget->addWidget(makeScrollable(userInterfacePage)); + pagesWidget->addWidget(makeScrollable(deckEditorPage)); + pagesWidget->addWidget(makeScrollable(storagePage)); + pagesWidget->addWidget(messagesPage); + pagesWidget->addWidget(soundPage); + pagesWidget->addWidget(shortcutsPage); - shortcutsButton = new QListWidgetItem(contentsWidget); - shortcutsButton->setTextAlignment(Qt::AlignHCenter); - shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - shortcutsButton->setIcon(QPixmap("theme:config/shorcuts")); + Q_ASSERT(pages.size() == NumPages); - connect(contentsWidget, &QListWidget::currentItemChanged, this, &DlgSettings::changePage); + // 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); + + // Bottom buttons + auto *buttonBox = new QHBoxLayout; + buttonBox->addStretch(); + okButton = new QPushButton; + okButton->setDefault(true); + connect(okButton, &QPushButton::clicked, this, &DlgSettings::close); + buttonBox->addWidget(okButton); + + // 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); + 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)); + + 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::changePage(QListWidgetItem *current, QListWidgetItem *previous) +void DlgSettings::setupTabBar() { - if (!current) { - current = previous; + 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); } - pagesWidget->setCurrentIndex(contentsWidget->row(current)); + 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); + } } void DlgSettings::setTab(int index) { - if (index <= contentsWidget->count() - 1 && index >= 0) { - changePage(contentsWidget->item(index), contentsWidget->currentItem()); - contentsWidget->setCurrentRow(index); + if (index >= 0 && index < tabButtons.size()) { + setActiveTab(index); } } @@ -155,6 +421,49 @@ 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; @@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event) if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() || SettingsCache::instance().paths().getDeckPath().isEmpty()) { - //! \todo Prompt to create the deck directory. if (QMessageBox::critical( this, tr("Error"), tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"), @@ -221,7 +529,6 @@ void DlgSettings::closeEvent(QCloseEvent *event) if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() || SettingsCache::instance().paths().getPicsPath().isEmpty()) { - //! \todo Prompt to create the pictures directory. if (QMessageBox::critical(this, tr("Error"), tr("The path to your card pictures directory is invalid. Would you like to go back " "and set the correct path?"), @@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event) void DlgSettings::retranslateUi() { setWindowTitle(tr("Settings")); + retranslateTabNames(); - generalButton->setText(tr("General")); - appearanceButton->setText(tr("Appearance")); - userInterfaceButton->setText(tr("User Interface")); - storageButton->setText(tr("Storage")); - deckEditorButton->setText(tr("Card Sources")); - messagesButton->setText(tr("Chat")); - soundButton->setText(tr("Sound")); - shortcutsButton->setText(tr("Shortcuts")); + searchEdit->setPlaceholderText(tr("Search settings...")); + okButton->setText(tr("OK")); - contentsWidget->reset(); + // 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 3ffee6388..b700f7af9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -1,43 +1,99 @@ /** * @file dlg_settings.h + * @brief Main settings dialog for the Cockatrice client * @ingroup Dialogs */ -//! \todo Document this file. - #ifndef DLG_SETTINGS_H #define DLG_SETTINGS_H -#include #include #include +class QPushButton; + inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); -class QListWidget; class QStackedWidget; -class QListWidgetItem; +class QToolButton; +class QListView; +class QLineEdit; +class AbstractSettingsPage; +class SettingsSearchModel; +class SettingsSearchDelegate; + +/** + * @brief Main application settings dialog with tabbed navigation and search + * + * 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. + */ 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 + }; + explicit DlgSettings(QWidget *parent = nullptr); void setTab(int index); private slots: - void changePage(QListWidgetItem *current, QListWidgetItem *previous); + void onTabClicked(int index); + void onSearchTextChanged(const QString &text); + void onSearchResultClicked(const QModelIndex &index); void updateLanguage(); private: - QListWidget *contentsWidget; - QStackedWidget *pagesWidget; - QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton, - *messagesButton, *soundButton, *shortcutsButton; - void createIcons(); + // 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(); + void retranslateUi(); + void retranslateTabNames(); protected: void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; }; -#endif +#endif // DLG_SETTINGS_H diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp new file mode 100644 index 000000000..9a70e94c5 --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp @@ -0,0 +1,196 @@ +#include "abstract_settings_page.h" + +#include "settings_search_model.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Recursively collects all widgets within a layout + * @param layout The layout to walk + * @param widgets Output list of (widget, containing layout) pairs + */ +static void collectWidgets(QLayout *layout, QList> &widgets) +{ + for (int i = 0; i < layout->count(); ++i) { + QLayoutItem *item = layout->itemAt(i); + if (!item) { + continue; + } + if (QWidget *widget = item->widget()) { + widgets.append({widget, layout}); + } else if (QLayout *subLayout = item->layout()) { + collectWidgets(subLayout, widgets); + } + } +} + +/** + * @brief Rejects QLabels that are not setting names + * + * HTML link labels, path values, and excessively long labels are filtered out. + */ +static bool isValidSettingLabel(const QLabel *label) +{ + const QString &text = label->text(); + if (Qt::mightBeRichText(text)) { + return false; + } + if (text.contains(QLatin1Char('/')) || text.contains(QLatin1Char('\\'))) { + return false; + } + if (text.size() > 60) { + return false; + } + return true; +} + +/** + * @brief Finds the control associated with a setting label + * + * Uses the explicit buddy if set, otherwise the widget in the cell (or slot) + * immediately following the label within the same layout. Returns nullptr when + * no obvious control is found. + */ +static QWidget *controlForLabel(QLabel *label, QLayout *containingLayout) +{ + if (QWidget *buddy = label->buddy()) { + return buddy; + } + + if (auto *grid = qobject_cast(containingLayout)) { + int index = grid->indexOf(label); + if (index != -1) { + int row = 0; + int column = 0; + int rowSpan = 1; + int columnSpan = 1; + grid->getItemPosition(index, &row, &column, &rowSpan, &columnSpan); + if (QLayoutItem *next = grid->itemAtPosition(row, column + columnSpan)) { + if (QWidget *nextWidget = next->widget()) { + return nextWidget; + } + } + } + } else { + int index = containingLayout->indexOf(label); + if (index != -1) { + for (int i = index + 1; i < containingLayout->count(); ++i) { + if (QLayoutItem *next = containingLayout->itemAt(i)) { + if (QWidget *nextWidget = next->widget()) { + return nextWidget; + } + } + } + } + } + + return nullptr; +} + +/** + * @brief Builds the extended search text for an entry + * + * Combines the group title, label, and any extra searchable text derived from + * the associated control (placeholder, prefix/suffix, tooltip). Combo values + * and numeric tooltips are excluded since they change at runtime and would + * make the search index stale. + */ +static QString buildFullSearchText(const QString &groupTitle, const QString &cleanLabel, QWidget *control) +{ + QStringList parts = {groupTitle, cleanLabel}; + if (control) { + if (auto *lineEdit = qobject_cast(control)) { + parts.append(lineEdit->placeholderText()); + } else if (auto *spinBox = qobject_cast(control)) { + parts.append(spinBox->prefix()); + parts.append(spinBox->suffix()); + } + if (!control->toolTip().isEmpty()) { + bool isNumeric = false; + control->toolTip().toInt(&isNumeric); + if (!isNumeric) { + parts.append(control->toolTip()); + } + } + } + parts.removeAll(QString()); + return parts.join(QLatin1Char(' ')); +} + +QList AbstractSettingsPage::getSearchEntries() +{ + return autoDetectSearchEntries(this, -1); +} + +QList AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex) +{ + QList entries; + + const auto children = page->children(); + for (QObject *child : children) { + auto *groupBox = qobject_cast(child); + if (!groupBox) { + continue; + } + + QString groupTitle = groupBox->title(); + if (groupTitle.isEmpty()) { + continue; + } + + QLayout *groupLayout = groupBox->layout(); + if (!groupLayout) { + continue; + } + + QList> widgets; + collectWidgets(groupLayout, widgets); + + for (const auto &pair : widgets) { + QWidget *widget = pair.first; + QString label; + + auto *checkBox = qobject_cast(widget); + if (checkBox) { + label = checkBox->text(); + } else { + auto *labelWidget = qobject_cast(widget); + if (!labelWidget || labelWidget->text().isEmpty() || !isValidSettingLabel(labelWidget)) { + continue; + } + label = labelWidget->text(); + } + + if (label.isEmpty()) { + continue; + } + + // Strip accelerator markers (&) for search + QString cleanLabel = label; + cleanLabel.remove(QLatin1Char('&')); + + QWidget *control = widget; + if (auto *labelWidget = qobject_cast(widget)) { + control = controlForLabel(labelWidget, pair.second); + if (!control) { + continue; + } + } + + entries.append(SettingsSearchEntry{.pageIndex = pageIndex, + .groupTitle = groupTitle, + .widgetLabel = cleanLabel, + .fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control), + .widget = control}); + } + } + + return entries; +} diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h index 4cbf2d71a..3140f94f3 100644 --- a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h @@ -1,16 +1,24 @@ #ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H +#include #include #define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs" #define WIKI_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts" #define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" +struct SettingsSearchEntry; + class AbstractSettingsPage : public QWidget { + Q_OBJECT public: virtual void retranslateUi() = 0; + virtual QList getSearchEntries(); + +protected: + static QList autoDetectSearchEntries(QWidget *page, int pageIndex); }; #endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index e9878434b..e4f24ab73 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -90,6 +90,7 @@ MessagesSettingsPage::MessagesSettingsPage() highlightNotice->addWidget(&hexHighlightLabel, 1, 2); highlightNotice->addWidget(customAlertString, 0, 0); highlightNotice->addWidget(&customAlertStringLabel, 1, 0); + customAlertStringLabel.setBuddy(customAlertString); highlightGroupBox = new QGroupBox; highlightGroupBox->setLayout(highlightNotice); diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp new file mode 100644 index 000000000..2fdff3bce --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp @@ -0,0 +1,125 @@ +/** + * @file settings_search_delegate.cpp + * @brief Implementation of the custom settings search result delegate + * @ingroup Dialogs + */ +#include "settings_search_delegate.h" + +#include "settings_search_model.h" + +#include + +SettingsSearchDelegate::SettingsSearchDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +void SettingsSearchDelegate::setPageNames(const QStringList &names) +{ + pageNames = names; +} + +void SettingsSearchDelegate::setPageIcons(const QStringList &iconResources) +{ + pageIcons.clear(); + for (const QString &resource : iconResources) { + pageIcons.append(QPixmap(resource)); + } +} + +void SettingsSearchDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + painter->save(); + + SettingsSearchEntry entry = index.data(SettingsSearchModel::EntryRole).value(); + + bool isSelected = option.state & QStyle::State_Selected; + bool isHovered = option.state & QStyle::State_MouseOver; + + // Background + QColor bgColor = isSelected ? option.palette.color(QPalette::Highlight) + : isHovered ? option.palette.color(QPalette::Midlight) + : option.palette.color(QPalette::Base); + painter->fillRect(option.rect, bgColor); + + if (isSelected) { + // Accent bar on the left to make the selection unmistakable + painter->fillRect(QRect(option.rect.left(), option.rect.top(), 4, option.rect.height()), + option.palette.color(QPalette::Highlight).darker(150)); + } + + int leftMargin = 12; + int topMargin = 8; + int rightMargin = 12; + int bottomMargin = 4; + + QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin); + int yPos = contentRect.top(); + + // Icon of the related settings page + const int iconSize = 24; + QPixmap pageIcon = + (entry.pageIndex >= 0 && entry.pageIndex < pageIcons.size()) ? pageIcons.at(entry.pageIndex) : QPixmap(); + int iconOffset = pageIcon.isNull() ? 0 : iconSize + 8; + if (!pageIcon.isNull()) { + QRect iconRect(contentRect.left(), contentRect.top() + (contentRect.height() - iconSize) / 2, iconSize, + iconSize); + painter->drawPixmap(iconRect, pageIcon); + } + + QRect textRect = contentRect.adjusted(iconOffset, 0, 0, 0); + + // Breadcrumb: "Page > Group" + QFont breadcrumbFont = option.font; + breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1); + breadcrumbFont.setBold(true); + + QColor breadcrumbColor = + isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); + if (!isSelected) { + breadcrumbColor.setAlpha(180); + } + + QString pageName; + if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) { + pageName = pageNames[entry.pageIndex]; + } else { + pageName = QString::number(entry.pageIndex); + } + + QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle); + painter->setFont(breadcrumbFont); + painter->setPen(breadcrumbColor); + painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter, + breadcrumbText); + yPos += 20; + + // Setting label + QFont labelFont = option.font; + labelFont.setPointSize(labelFont.pointSize() + 1); + labelFont.setBold(isSelected); + + QColor labelColor = + isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); + + painter->setFont(labelFont); + painter->setPen(labelColor); + painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter, + entry.widgetLabel); + yPos += 24; + + // Bottom separator + QPen separatorPen(option.palette.color(QPalette::Mid), 1); + painter->setPen(separatorPen); + painter->drawLine(option.rect.left() + leftMargin, option.rect.bottom(), option.rect.right() - rightMargin, + option.rect.bottom()); + + painter->restore(); +} + +QSize SettingsSearchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(index); + return QSize(option.rect.width(), 56); +} diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h new file mode 100644 index 000000000..aa296049a --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h @@ -0,0 +1,38 @@ +/** + * @file settings_search_delegate.h + * @brief Custom delegate for rendering settings search results + * @ingroup Dialogs + */ +#ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H +#define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H + +#include +#include + +/** + * @brief Custom paint delegate for settings search result items + * + * Renders each search result with a breadcrumb line ("Page > Group"), + * the setting label, and a subtle separator. Supports selected/hovered states. + */ +class SettingsSearchDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit SettingsSearchDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + /** @brief Sets the translated page names for breadcrumb display */ + void setPageNames(const QStringList &names); + + /** @brief Sets the icons shown in front of results, indexed by page position */ + void setPageIcons(const QStringList &iconResources); + +private: + QStringList pageNames; ///< Translated page names indexed by page position + QList pageIcons; ///< Icons of the related settings pages +}; + +#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp new file mode 100644 index 000000000..71f859d4b --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp @@ -0,0 +1,145 @@ +/** + * @file settings_search_model.cpp + * @brief Implementation of the settings search list model + * @ingroup Dialogs + */ +#include "settings_search_model.h" + +#include +#include + +SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +void SettingsSearchModel::setSourceEntries(const QList &entries) +{ + beginResetModel(); + sourceEntries = entries; + endResetModel(); + rebuildFilter(); +} + +void SettingsSearchModel::setFilterString(const QString &text) +{ + filterActive = !text.trimmed().isEmpty(); + if (filterActive) { + filterQuery = text.trimmed(); + filterRegex = + QRegularExpression(QRegularExpression::escape(filterQuery), QRegularExpression::CaseInsensitiveOption); + } + rebuildFilter(); +} + +bool SettingsSearchModel::isFilterActive() const +{ + return filterActive; +} + +/** + * @brief Calculates a relevance score for a single entry against the query + * + * Scoring priorities (highest to lowest): + * 1. Label starts with query -> 100 + * 2. Label contains query -> 80 + * 3. Group title starts with query -> 60 + * 4. Group title contains query -> 40 + * 5. Full text regex match -> 20 + * 6. No match -> 0 (excluded from results) + */ +static int relevanceScore(const SettingsSearchEntry &entry, const QString &query, const QRegularExpression ®ex) +{ + QString lowerQuery = query.toLower(); + + // Label matches are most relevant + QString label = entry.widgetLabel.toLower(); + if (label.startsWith(lowerQuery)) { + return 100; + } + if (label.contains(lowerQuery)) { + return 80; + } + + // Group title matches are next + QString group = entry.groupTitle.toLower(); + if (group.startsWith(lowerQuery)) { + return 60; + } + if (group.contains(lowerQuery)) { + return 40; + } + + // Full text match is least relevant + if (entry.fullSearchText.contains(regex)) { + return 20; + } + + return 0; +} + +void SettingsSearchModel::rebuildFilter() +{ + beginResetModel(); + filteredIndices.clear(); + + if (!filterActive) { + for (int i = 0; i < sourceEntries.size(); ++i) { + filteredIndices.append(i); + } + } else { + QList> scored; // + for (int i = 0; i < sourceEntries.size(); ++i) { + const SettingsSearchEntry &entry = sourceEntries[i]; + // Skip conditional settings that are currently disabled or hidden + if (entry.widget && (!entry.widget->isEnabled() || entry.widget->isHidden())) { + continue; + } + int score = relevanceScore(entry, filterQuery, filterRegex); + if (score > 0) { + scored.append({-score, i}); // negative for descending sort + } + } + std::sort(scored.begin(), scored.end()); + for (const auto &pair : scored) { + filteredIndices.append(pair.second); + } + } + endResetModel(); +} + +int SettingsSearchModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return filteredIndices.size(); +} + +QVariant SettingsSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= filteredIndices.size()) { + return {}; + } + + const SettingsSearchEntry &entry = sourceEntries[filteredIndices[index.row()]]; + + switch (role) { + case EntryRole: + return QVariant::fromValue(entry); + case Qt::DisplayRole: + return entry.widgetLabel; + case Qt::ToolTipRole: + return QStringLiteral("%1 > %2 > %3") + .arg(QString::number(entry.pageIndex), entry.groupTitle, entry.widgetLabel); + default: + return {}; + } +} + +SettingsSearchEntry SettingsSearchModel::entryForIndex(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= filteredIndices.size()) { + return {}; + } + return sourceEntries[filteredIndices[index.row()]]; +} diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_model.h b/cockatrice/src/interface/widgets/settings_page/settings_search_model.h new file mode 100644 index 000000000..0b590f180 --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_model.h @@ -0,0 +1,76 @@ +/** + * @file settings_search_model.h + * @brief Data model for the settings search feature + * @ingroup Dialogs + */ +#ifndef COCKATRICE_SETTINGS_SEARCH_MODEL_H +#define COCKATRICE_SETTINGS_SEARCH_MODEL_H + +#include +#include +#include +#include + +/** + * @brief Represents a single searchable setting entry + * + * Each settings page provides a list of these entries via getSearchEntries(). + * The model uses them for filtering, relevance scoring, and display. + */ +struct SettingsSearchEntry +{ + int pageIndex; ///< Index of the settings page this entry belongs to + QString groupTitle; ///< Title of the group/section within the page + QString widgetLabel; ///< Display label for the setting widget + QString fullSearchText; ///< Extended search text (label, control text, tooltip) for full-text matching + QWidget *widget; ///< Pointer to the setting widget for focus/scrolling +}; + +/** + * @brief List model providing filtered, ranked search results + * + * Manages a list of SettingsSearchEntry items. When a filter string is set, + * entries are scored by relevance and sorted so the best matches appear first. + * Supports custom roles for accessing entry fields from views and delegates. + */ +class SettingsSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + /** + * @brief Custom data role for accessing the full entry + */ + enum Roles + { + EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object + }; + + explicit SettingsSearchModel(QObject *parent = nullptr); + + /** @brief Replaces the source entries and rebuilds the filter */ + void setSourceEntries(const QList &entries); + /** @brief Sets the filter string and recalculates the results */ + void setFilterString(const QString &text); + /** @brief Whether a non-empty filter is currently active */ + bool isFilterActive() const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + /** @brief Returns the full entry for a given model index */ + SettingsSearchEntry entryForIndex(const QModelIndex &index) const; + +private: + QList sourceEntries; ///< Complete unfiltered entry list + QList filteredIndices; ///< Indices into sourceEntries matching the filter + QRegularExpression filterRegex; ///< Compiled regex for the current filter + QString filterQuery; ///< Current filter query string + bool filterActive = false; ///< Whether filtering is active + + /** @brief Recalculates the filtered index list and ranking */ + void rebuildFilter(); +}; + +Q_DECLARE_METATYPE(SettingsSearchEntry) + +#endif // COCKATRICE_SETTINGS_SEARCH_MODEL_H diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp index e3b9994ea..1f1867f7c 100644 --- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp @@ -123,6 +123,7 @@ void ShortcutSettingsPage::retranslateUi() currentActionGroupLabel->setText(tr("Section:")); currentActionLabel->setText(tr("Action:")); currentShortcutLabel->setText(tr("Shortcut:")); + editShortcutGroupBox->setTitle(tr("Shortcut editor")); editTextBox->retranslateUi(); faqLabel->setText(QString("%2").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts"))); btnResetAll->setText(tr("Restore all default shortcuts")); diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 899f38ec2..5705c184e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -242,7 +242,7 @@ void TabRoom::actClearChat() void TabRoom::actOpenChatSettings() { DlgSettings settings(this); - settings.setTab(4); + settings.setTab(DlgSettings::MessagesPage); settings.exec(); }