mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[Settings][Dialog] Implement search for settings by text, description, tooltip, etc. (#7065)
* [Settings] Implement search Took 38 minutes Took 8 seconds Took 9 minutes Took 5 seconds Took 44 seconds Took 25 seconds * Comments Took 23 minutes Took 15 seconds * Comments Took 1 hour 14 minutes * Minor fixes to search Took 13 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
ba203440af
commit
bf6b2a90bc
13 changed files with 1069 additions and 103 deletions
|
|
@ -257,10 +257,13 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/server/user/user_list_manager.cpp
|
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_painter.cpp
|
||||||
src/interface/widgets/server/user/user_list_widget.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/appearance_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/deck_editor_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/general_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/messages_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/shortcut_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/sound_settings_page.cpp
|
src/interface/widgets/settings_page/sound_settings_page.cpp
|
||||||
src/interface/widgets/settings_page/storage_settings_page.cpp
|
src/interface/widgets/settings_page/storage_settings_page.cpp
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../deck_loader/card_node_function.h"
|
#include "../../deck_loader/card_node_function.h"
|
||||||
#include "../../deck_loader/deck_loader.h"
|
#include "../../deck_loader/deck_loader.h"
|
||||||
#include "dlg_settings.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* @file dlg_settings.cpp
|
||||||
|
* @brief Implementation of the main settings dialog
|
||||||
|
* @ingroup Dialogs
|
||||||
|
*/
|
||||||
#include "dlg_settings.h"
|
#include "dlg_settings.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
|
@ -6,6 +11,8 @@
|
||||||
#include "../settings_page/deck_editor_settings_page.h"
|
#include "../settings_page/deck_editor_settings_page.h"
|
||||||
#include "../settings_page/general_settings_page.h"
|
#include "../settings_page/general_settings_page.h"
|
||||||
#include "../settings_page/messages_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/shortcut_settings_page.h"
|
||||||
#include "../settings_page/sound_settings_page.h"
|
#include "../settings_page/sound_settings_page.h"
|
||||||
#include "../settings_page/storage_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_loader.h"
|
||||||
#include "libcockatrice/card/database/card_database_manager.h"
|
#include "libcockatrice/card/database/card_database_manager.h"
|
||||||
|
|
||||||
|
#include <QAbstractItemView>
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QGraphicsOpacityEffect>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QListWidget>
|
#include <QHBoxLayout>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListView>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
|
#include <QSequentialAnimationGroup>
|
||||||
|
#include <QShortcut>
|
||||||
|
#include <QStackedLayout>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
|
#include <QToolButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
static QScrollArea *makeScrollable(QWidget *widget)
|
||||||
{
|
{
|
||||||
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
||||||
|
|
@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget)
|
||||||
return scrollArea;
|
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();
|
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);
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||||
|
|
||||||
contentsWidget = new QListWidget;
|
setupUi();
|
||||||
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);
|
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
||||||
|
searchEdit->setFocus();
|
||||||
|
|
||||||
adjustSize();
|
adjustSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlgSettings::createIcons()
|
void DlgSettings::setupUi()
|
||||||
{
|
{
|
||||||
generalButton = new QListWidgetItem(contentsWidget);
|
// Search bar
|
||||||
generalButton->setTextAlignment(Qt::AlignHCenter);
|
searchEdit = new QLineEdit;
|
||||||
generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
searchEdit->setClearButtonEnabled(true);
|
||||||
generalButton->setIcon(QPixmap("theme:config/general"));
|
searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||||
|
searchEdit->installEventFilter(this);
|
||||||
|
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
||||||
|
|
||||||
appearanceButton = new QListWidgetItem(contentsWidget);
|
auto *searchLayout = new QHBoxLayout;
|
||||||
appearanceButton->setTextAlignment(Qt::AlignHCenter);
|
searchLayout->addWidget(searchEdit);
|
||||||
appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
||||||
appearanceButton->setIcon(QPixmap("theme:config/appearance"));
|
|
||||||
|
|
||||||
userInterfaceButton = new QListWidgetItem(contentsWidget);
|
// Tab bar (built in setupTabBar)
|
||||||
userInterfaceButton->setTextAlignment(Qt::AlignHCenter);
|
setupTabBar();
|
||||||
userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
||||||
userInterfaceButton->setIcon(QPixmap("theme:config/interface"));
|
|
||||||
|
|
||||||
deckEditorButton = new QListWidgetItem(contentsWidget);
|
// Pages stacked widget
|
||||||
deckEditorButton->setTextAlignment(Qt::AlignHCenter);
|
pagesWidget = new QStackedWidget;
|
||||||
deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
||||||
deckEditorButton->setIcon(QPixmap("theme:config/deckeditor"));
|
|
||||||
|
|
||||||
storageButton = new QListWidgetItem(contentsWidget);
|
auto *generalPage = new GeneralSettingsPage;
|
||||||
storageButton->setTextAlignment(Qt::AlignHCenter);
|
auto *appearancePage = new AppearanceSettingsPage;
|
||||||
storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
auto *userInterfacePage = new UserInterfaceSettingsPage;
|
||||||
storageButton->setIcon(QPixmap("theme:config/storage"));
|
auto *deckEditorPage = new DeckEditorSettingsPage;
|
||||||
|
auto *storagePage = new StorageSettingsPage;
|
||||||
|
auto *messagesPage = new MessagesSettingsPage;
|
||||||
|
auto *soundPage = new SoundSettingsPage;
|
||||||
|
auto *shortcutsPage = new ShortcutSettingsPage;
|
||||||
|
|
||||||
messagesButton = new QListWidgetItem(contentsWidget);
|
pages.append(generalPage);
|
||||||
messagesButton->setTextAlignment(Qt::AlignHCenter);
|
pages.append(appearancePage);
|
||||||
messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
pages.append(userInterfacePage);
|
||||||
messagesButton->setIcon(QPixmap("theme:config/messages"));
|
pages.append(deckEditorPage);
|
||||||
|
pages.append(storagePage);
|
||||||
|
pages.append(messagesPage);
|
||||||
|
pages.append(soundPage);
|
||||||
|
pages.append(shortcutsPage);
|
||||||
|
|
||||||
soundButton = new QListWidgetItem(contentsWidget);
|
pagesWidget->addWidget(makeScrollable(generalPage));
|
||||||
soundButton->setTextAlignment(Qt::AlignHCenter);
|
pagesWidget->addWidget(makeScrollable(appearancePage));
|
||||||
soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
||||||
soundButton->setIcon(QPixmap("theme:config/sound"));
|
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(storagePage));
|
||||||
|
pagesWidget->addWidget(messagesPage);
|
||||||
|
pagesWidget->addWidget(soundPage);
|
||||||
|
pagesWidget->addWidget(shortcutsPage);
|
||||||
|
|
||||||
shortcutsButton = new QListWidgetItem(contentsWidget);
|
Q_ASSERT(pages.size() == NumPages);
|
||||||
shortcutsButton->setTextAlignment(Qt::AlignHCenter);
|
|
||||||
shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
||||||
shortcutsButton->setIcon(QPixmap("theme:config/shorcuts"));
|
|
||||||
|
|
||||||
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) {
|
tabBarWidget = new QWidget;
|
||||||
current = previous;
|
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<SettingsSearchEntry> allEntries;
|
||||||
|
|
||||||
|
const QStringList pageNames = translatedPageNames();
|
||||||
|
searchDelegate->setPageNames(pageNames);
|
||||||
|
searchDelegate->setPageIcons(pageIconResources());
|
||||||
|
|
||||||
|
for (int i = 0; i < pages.size(); ++i) {
|
||||||
|
QList<SettingsSearchEntry> 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<QScrollArea *>(widget)) {
|
||||||
|
scrollArea->ensureWidgetVisible(entry.widget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
widget = widget->parentWidget();
|
||||||
|
}
|
||||||
|
entry.widget->setFocus();
|
||||||
|
flashWidget(entry.widget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlgSettings::setTab(int index)
|
void DlgSettings::setTab(int index)
|
||||||
{
|
{
|
||||||
if (index <= contentsWidget->count() - 1 && index >= 0) {
|
if (index >= 0 && index < tabButtons.size()) {
|
||||||
changePage(contentsWidget->item(index), contentsWidget->currentItem());
|
setActiveTab(index);
|
||||||
contentsWidget->setCurrentRow(index);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +421,49 @@ void DlgSettings::updateLanguage()
|
||||||
installNewTranslator();
|
installNewTranslator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(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)
|
void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
{
|
{
|
||||||
bool showLoadError = true;
|
bool showLoadError = true;
|
||||||
|
|
@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
|
|
||||||
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
|
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
|
||||||
SettingsCache::instance().paths().getDeckPath().isEmpty()) {
|
SettingsCache::instance().paths().getDeckPath().isEmpty()) {
|
||||||
//! \todo Prompt to create the deck directory.
|
|
||||||
if (QMessageBox::critical(
|
if (QMessageBox::critical(
|
||||||
this, tr("Error"),
|
this, tr("Error"),
|
||||||
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
|
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() ||
|
if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() ||
|
||||||
SettingsCache::instance().paths().getPicsPath().isEmpty()) {
|
SettingsCache::instance().paths().getPicsPath().isEmpty()) {
|
||||||
//! \todo Prompt to create the pictures directory.
|
|
||||||
if (QMessageBox::critical(this, tr("Error"),
|
if (QMessageBox::critical(this, tr("Error"),
|
||||||
tr("The path to your card pictures directory is invalid. Would you like to go back "
|
tr("The path to your card pictures directory is invalid. Would you like to go back "
|
||||||
"and set the correct path?"),
|
"and set the correct path?"),
|
||||||
|
|
@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
void DlgSettings::retranslateUi()
|
void DlgSettings::retranslateUi()
|
||||||
{
|
{
|
||||||
setWindowTitle(tr("Settings"));
|
setWindowTitle(tr("Settings"));
|
||||||
|
retranslateTabNames();
|
||||||
|
|
||||||
generalButton->setText(tr("General"));
|
searchEdit->setPlaceholderText(tr("Search settings..."));
|
||||||
appearanceButton->setText(tr("Appearance"));
|
okButton->setText(tr("OK"));
|
||||||
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"));
|
|
||||||
|
|
||||||
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,99 @@
|
||||||
/**
|
/**
|
||||||
* @file dlg_settings.h
|
* @file dlg_settings.h
|
||||||
|
* @brief Main settings dialog for the Cockatrice client
|
||||||
* @ingroup Dialogs
|
* @ingroup Dialogs
|
||||||
*/
|
*/
|
||||||
//! \todo Document this file.
|
|
||||||
|
|
||||||
#ifndef DLG_SETTINGS_H
|
#ifndef DLG_SETTINGS_H
|
||||||
#define DLG_SETTINGS_H
|
#define DLG_SETTINGS_H
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
||||||
|
|
||||||
class QListWidget;
|
|
||||||
class QStackedWidget;
|
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
|
class DlgSettings : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
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);
|
explicit DlgSettings(QWidget *parent = nullptr);
|
||||||
void setTab(int index);
|
void setTab(int index);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void changePage(QListWidgetItem *current, QListWidgetItem *previous);
|
void onTabClicked(int index);
|
||||||
|
void onSearchTextChanged(const QString &text);
|
||||||
|
void onSearchResultClicked(const QModelIndex &index);
|
||||||
void updateLanguage();
|
void updateLanguage();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QListWidget *contentsWidget;
|
// UI elements
|
||||||
QStackedWidget *pagesWidget;
|
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
||||||
QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton,
|
QWidget *tabBarWidget; ///< Container widget for the tab buttons
|
||||||
*messagesButton, *soundButton, *shortcutsButton;
|
QList<QToolButton *> tabButtons; ///< Navigation tab buttons
|
||||||
void createIcons();
|
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<AbstractSettingsPage *> 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 retranslateUi();
|
||||||
|
void retranslateTabNames();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif // DLG_SETTINGS_H
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
#include "abstract_settings_page.h"
|
||||||
|
|
||||||
|
#include "settings_search_model.h"
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QSpinBox>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<QPair<QWidget *, QLayout *>> &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<QGridLayout *>(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<QLineEdit *>(control)) {
|
||||||
|
parts.append(lineEdit->placeholderText());
|
||||||
|
} else if (auto *spinBox = qobject_cast<QSpinBox *>(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<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries()
|
||||||
|
{
|
||||||
|
return autoDetectSearchEntries(this, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex)
|
||||||
|
{
|
||||||
|
QList<SettingsSearchEntry> entries;
|
||||||
|
|
||||||
|
const auto children = page->children();
|
||||||
|
for (QObject *child : children) {
|
||||||
|
auto *groupBox = qobject_cast<QGroupBox *>(child);
|
||||||
|
if (!groupBox) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString groupTitle = groupBox->title();
|
||||||
|
if (groupTitle.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLayout *groupLayout = groupBox->layout();
|
||||||
|
if (!groupLayout) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QPair<QWidget *, QLayout *>> widgets;
|
||||||
|
collectWidgets(groupLayout, widgets);
|
||||||
|
|
||||||
|
for (const auto &pair : widgets) {
|
||||||
|
QWidget *widget = pair.first;
|
||||||
|
QString label;
|
||||||
|
|
||||||
|
auto *checkBox = qobject_cast<QCheckBox *>(widget);
|
||||||
|
if (checkBox) {
|
||||||
|
label = checkBox->text();
|
||||||
|
} else {
|
||||||
|
auto *labelWidget = qobject_cast<QLabel *>(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<QLabel *>(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;
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
#ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
#ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
||||||
#define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
#define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs"
|
#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_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts"
|
||||||
#define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
|
#define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
|
||||||
|
|
||||||
|
struct SettingsSearchEntry;
|
||||||
|
|
||||||
class AbstractSettingsPage : public QWidget
|
class AbstractSettingsPage : public QWidget
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
virtual void retranslateUi() = 0;
|
virtual void retranslateUi() = 0;
|
||||||
|
virtual QList<SettingsSearchEntry> getSearchEntries();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static QList<SettingsSearchEntry> autoDetectSearchEntries(QWidget *page, int pageIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
#endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ MessagesSettingsPage::MessagesSettingsPage()
|
||||||
highlightNotice->addWidget(&hexHighlightLabel, 1, 2);
|
highlightNotice->addWidget(&hexHighlightLabel, 1, 2);
|
||||||
highlightNotice->addWidget(customAlertString, 0, 0);
|
highlightNotice->addWidget(customAlertString, 0, 0);
|
||||||
highlightNotice->addWidget(&customAlertStringLabel, 1, 0);
|
highlightNotice->addWidget(&customAlertStringLabel, 1, 0);
|
||||||
|
customAlertStringLabel.setBuddy(customAlertString);
|
||||||
highlightGroupBox = new QGroupBox;
|
highlightGroupBox = new QGroupBox;
|
||||||
highlightGroupBox->setLayout(highlightNotice);
|
highlightGroupBox->setLayout(highlightNotice);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 <QPainter>
|
||||||
|
|
||||||
|
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<SettingsSearchEntry>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -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 <QPixmap>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<QPixmap> pageIcons; ///< Icons of the related settings pages
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
|
||||||
|
|
@ -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 <QPair>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsSearchModel::setSourceEntries(const QList<SettingsSearchEntry> &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<QPair<int, int>> scored; // <score, index>
|
||||||
|
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()]];
|
||||||
|
}
|
||||||
|
|
@ -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 <QAbstractListModel>
|
||||||
|
#include <QList>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<SettingsSearchEntry> &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<SettingsSearchEntry> sourceEntries; ///< Complete unfiltered entry list
|
||||||
|
QList<int> 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
|
||||||
|
|
@ -123,6 +123,7 @@ void ShortcutSettingsPage::retranslateUi()
|
||||||
currentActionGroupLabel->setText(tr("Section:"));
|
currentActionGroupLabel->setText(tr("Section:"));
|
||||||
currentActionLabel->setText(tr("Action:"));
|
currentActionLabel->setText(tr("Action:"));
|
||||||
currentShortcutLabel->setText(tr("Shortcut:"));
|
currentShortcutLabel->setText(tr("Shortcut:"));
|
||||||
|
editShortcutGroupBox->setTitle(tr("Shortcut editor"));
|
||||||
editTextBox->retranslateUi();
|
editTextBox->retranslateUi();
|
||||||
faqLabel->setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts")));
|
faqLabel->setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts")));
|
||||||
btnResetAll->setText(tr("Restore all default shortcuts"));
|
btnResetAll->setText(tr("Restore all default shortcuts"));
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ void TabRoom::actClearChat()
|
||||||
void TabRoom::actOpenChatSettings()
|
void TabRoom::actOpenChatSettings()
|
||||||
{
|
{
|
||||||
DlgSettings settings(this);
|
DlgSettings settings(this);
|
||||||
settings.setTab(4);
|
settings.setTab(DlgSettings::MessagesPage);
|
||||||
settings.exec();
|
settings.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue