Took 1 hour 14 minutes
This commit is contained in:
Lukas Brübach 2026-08-03 10:29:44 +02:00
parent ccd2e15629
commit 8072eef462
9 changed files with 262 additions and 191 deletions

View file

@ -17,7 +17,6 @@
#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"
#include "../settings_page/user_interface_settings_page.h" #include "../settings_page/user_interface_settings_page.h"
#include "../utility/custom_line_edit.h"
#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"
@ -29,6 +28,7 @@
#include <QGuiApplication> #include <QGuiApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QKeyEvent> #include <QKeyEvent>
#include <QLineEdit>
#include <QListView> #include <QListView>
#include <QMessageBox> #include <QMessageBox>
#include <QPropertyAnimation> #include <QPropertyAnimation>
@ -63,6 +63,17 @@ static QScrollArea *makeScrollable(QWidget *widget)
return scrollArea; return scrollArea;
} }
/**
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
*/
static QStringList pageIconResources()
{
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
}
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false) DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false)
{ {
auto rec = QGuiApplication::primaryScreen()->availableGeometry(); auto rec = QGuiApplication::primaryScreen()->availableGeometry();
@ -121,9 +132,11 @@ void DlgSettings::setupUi()
pagesWidget->addWidget(makeScrollable(userInterfacePage)); pagesWidget->addWidget(makeScrollable(userInterfacePage));
pagesWidget->addWidget(makeScrollable(deckEditorPage)); pagesWidget->addWidget(makeScrollable(deckEditorPage));
pagesWidget->addWidget(makeScrollable(storagePage)); pagesWidget->addWidget(makeScrollable(storagePage));
pagesWidget->addWidget(makeScrollable(messagesPage)); pagesWidget->addWidget(messagesPage);
pagesWidget->addWidget(makeScrollable(soundPage)); pagesWidget->addWidget(soundPage);
pagesWidget->addWidget(makeScrollable(shortcutsPage)); pagesWidget->addWidget(shortcutsPage);
Q_ASSERT(pages.size() == NumPages);
// Search results view (hidden by default) // Search results view (hidden by default)
searchResultsView = new QListView; searchResultsView = new QListView;
@ -138,7 +151,12 @@ void DlgSettings::setupUi()
searchDelegate = new SettingsSearchDelegate(this); searchDelegate = new SettingsSearchDelegate(this);
searchResultsView->setModel(searchModel); searchResultsView->setModel(searchModel);
searchResultsView->setItemDelegate(searchDelegate); searchResultsView->setItemDelegate(searchDelegate);
connect(searchResultsView, &QListView::activated, this, &DlgSettings::onSearchResultActivated); connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] {
searchDelegate->setPageIcons(pageIconResources());
searchResultsView->viewport()->update();
});
// Build search index after pages are created // Build search index after pages are created
buildSearchIndex(); buildSearchIndex();
@ -152,13 +170,10 @@ void DlgSettings::setupUi()
pagesContainer->setLayout(containerLayout); pagesContainer->setLayout(containerLayout);
// Bottom buttons // Bottom buttons
resetButton = new QPushButton;
connect(resetButton, &QPushButton::clicked, this, &DlgSettings::onResetDefaultsClicked);
auto *buttonBox = new QHBoxLayout; auto *buttonBox = new QHBoxLayout;
buttonBox->addWidget(resetButton);
buttonBox->addStretch(); buttonBox->addStretch();
okButton = new QPushButton; okButton = new QPushButton;
okButton->setDefault(true);
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close); connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
buttonBox->addWidget(okButton); buttonBox->addWidget(okButton);
@ -202,28 +217,19 @@ void DlgSettings::setupTabBar()
tabLayout->setContentsMargins(0, 0, 0, 0); tabLayout->setContentsMargins(0, 0, 0, 0);
tabLayout->setSpacing(2); tabLayout->setSpacing(2);
struct TabInfo const QStringList iconResources = pageIconResources();
{
QString iconResource;
int pageIndex;
};
const TabInfo tabInfos[] = {{"theme:config/general", 0}, {"theme:config/appearance", 1}, for (int i = 0; i < iconResources.size(); ++i) {
{"theme:config/interface", 2}, {"theme:config/deckeditor", 3},
{"theme:config/storage", 4}, {"theme:config/messages", 5},
{"theme:config/sound", 6}, {"theme:config/shorcuts", 7}};
for (const auto &info : tabInfos) {
auto *tabButton = new QToolButton; auto *tabButton = new QToolButton;
tabButton->setCheckable(true); tabButton->setCheckable(true);
tabButton->setIcon(QPixmap(info.iconResource)); tabButton->setIcon(QPixmap(iconResources[i]));
tabButton->setIconSize(QSize(48, 48)); tabButton->setIconSize(QSize(48, 48));
tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
tabButton->setAutoExclusive(true); tabButton->setAutoExclusive(true);
tabButton->setMinimumHeight(85); tabButton->setMinimumHeight(85);
tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(tabButton, &QToolButton::clicked, this, [this, idx = info.pageIndex] { onTabClicked(idx); }); connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); });
tabButtons.append(tabButton); tabButtons.append(tabButton);
tabLayout->addWidget(tabButton); tabLayout->addWidget(tabButton);
@ -236,11 +242,9 @@ void DlgSettings::buildSearchIndex()
{ {
QList<SettingsSearchEntry> allEntries; QList<SettingsSearchEntry> allEntries;
if (pageNames.isEmpty()) { const QStringList pageNames = translatedPageNames();
pageNames << tr("General") << tr("Appearance") << tr("User Interface") << tr("Card Sources") << tr("Storage")
<< tr("Chat") << tr("Sound") << tr("Shortcuts");
}
searchDelegate->setPageNames(pageNames); searchDelegate->setPageNames(pageNames);
searchDelegate->setPageIcons(pageIconResources());
for (int i = 0; i < pages.size(); ++i) { for (int i = 0; i < pages.size(); ++i) {
QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries(); QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries();
@ -321,7 +325,7 @@ void DlgSettings::flashWidget(QWidget *widget)
fadeOut->setEndValue(0.0); fadeOut->setEndValue(0.0);
fadeOut->setEasingCurve(QEasingCurve::InCubic); fadeOut->setEasingCurve(QEasingCurve::InCubic);
auto *group = new QSequentialAnimationGroup; auto *group = new QSequentialAnimationGroup(overlay);
group->addAnimation(flashIn); group->addAnimation(flashIn);
group->addAnimation(fadeOut); group->addAnimation(fadeOut);
@ -338,10 +342,11 @@ void DlgSettings::onSearchTextChanged(const QString &text)
if (!searchActive) { if (!searchActive) {
switchToSearchMode(); switchToSearchMode();
} }
} else { if (searchModel->rowCount(QModelIndex()) > 0) {
if (searchActive) { searchResultsView->setCurrentIndex(searchModel->index(0));
switchToTabMode();
} }
} else if (searchActive) {
switchToTabMode();
} }
} }
@ -368,7 +373,7 @@ void DlgSettings::switchToTabMode()
setActiveTab(currentTabIndex); setActiveTab(currentTabIndex);
} }
void DlgSettings::onSearchResultActivated(const QModelIndex &index) void DlgSettings::onSearchResultClicked(const QModelIndex &index)
{ {
navigateToSearchResult(index); navigateToSearchResult(index);
} }
@ -390,26 +395,11 @@ void DlgSettings::navigateToSearchResult(const QModelIndex &index)
if (scrollArea) { if (scrollArea) {
scrollArea->ensureWidgetVisible(entry.widget); scrollArea->ensureWidgetVisible(entry.widget);
} }
entry.widget->setFocus();
flashWidget(entry.widget); flashWidget(entry.widget);
} }
} }
void DlgSettings::onResetDefaultsClicked()
{
if (QMessageBox::question(this, tr("Reset to Defaults"),
tr("Are you sure you want to reset all settings on this page to their defaults?")) !=
QMessageBox::Yes) {
return;
}
if (currentTabIndex >= 0 && currentTabIndex < pages.size()) {
pages[currentTabIndex]->resetToDefaults();
}
// Rebuild search index after reset
buildSearchIndex();
}
void DlgSettings::setTab(int index) void DlgSettings::setTab(int index)
{ {
if (index >= 0 && index < tabButtons.size()) { if (index >= 0 && index < tabButtons.size()) {
@ -546,20 +536,21 @@ void DlgSettings::retranslateUi()
retranslateTabNames(); retranslateTabNames();
searchEdit->setPlaceholderText(tr("Search settings...")); searchEdit->setPlaceholderText(tr("Search settings..."));
resetButton->setText(tr("Reset to Defaults"));
okButton->setText(tr("OK")); okButton->setText(tr("OK"));
// Rebuild search index for translated text // Rebuild search index for translated text
buildSearchIndex(); 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() void DlgSettings::retranslateTabNames()
{ {
pageNames.clear(); const QStringList tabLabels = translatedPageNames();
pageNames << tr("General") << tr("Appearance") << tr("User Interface") << tr("Card Sources") << tr("Storage")
<< tr("Chat") << tr("Sound") << tr("Shortcuts");
const QStringList tabLabels = pageNames;
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
tabButtons[i]->setText(tabLabels[i]); tabButtons[i]->setText(tabLabels[i]);

View file

@ -33,14 +33,32 @@ 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 onTabClicked(int index); void onTabClicked(int index);
void onSearchTextChanged(const QString &text); void onSearchTextChanged(const QString &text);
void onSearchResultActivated(const QModelIndex &index); void onSearchResultClicked(const QModelIndex &index);
void onResetDefaultsClicked();
void updateLanguage(); void updateLanguage();
private: private:
@ -51,7 +69,6 @@ private:
QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
QListView *searchResultsView; ///< Search results list view QListView *searchResultsView; ///< Search results list view
QWidget *pagesContainer; ///< Container stacking pages and search results QWidget *pagesContainer; ///< Container stacking pages and search results
QPushButton *resetButton; ///< Button to reset current page to defaults
QPushButton *okButton; ///< Button to close the dialog QPushButton *okButton; ///< Button to close the dialog
// Data // Data
@ -60,7 +77,6 @@ private:
SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering
int currentTabIndex; ///< Currently active tab index int currentTabIndex; ///< Currently active tab index
bool searchActive; ///< Whether search mode is active bool searchActive; ///< Whether search mode is active
QStringList pageNames; ///< Translated page names for breadcrumb display
void setupUi(); void setupUi();
void setupTabBar(); void setupTabBar();
@ -69,7 +85,8 @@ private:
void switchToSearchMode(); void switchToSearchMode();
void navigateToSearchResult(const QModelIndex &index); void navigateToSearchResult(const QModelIndex &index);
void setActiveTab(int index); void setActiveTab(int index);
void flashWidget(QWidget *widget); static void flashWidget(QWidget *widget);
static QStringList translatedPageNames();
void retranslateUi(); void retranslateUi();
void retranslateTabNames(); void retranslateTabNames();

View file

@ -3,19 +3,129 @@
#include "settings_search_model.h" #include "settings_search_model.h"
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QLabel> #include <QLabel>
#include <QLayout> #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. Falls back to the
* label itself 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 label;
}
/**
* @brief Builds the extended search text for an entry
*
* Combines the group title, label, and any extra searchable text derived from
* the associated control (tooltip, placeholder, prefix/suffix, combo text).
*/
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());
} else if (auto *comboBox = qobject_cast<QComboBox *>(control)) {
parts.append(comboBox->currentText());
}
if (!control->toolTip().isEmpty()) {
parts.append(control->toolTip());
}
}
parts.removeAll(QString());
return parts.join(QLatin1Char(' '));
}
QList<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries() QList<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries()
{ {
return autoDetectSearchEntries(this, -1); return autoDetectSearchEntries(this, -1);
} }
void AbstractSettingsPage::resetToDefaults()
{
}
QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex) QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex)
{ {
QList<SettingsSearchEntry> entries; QList<SettingsSearchEntry> entries;
@ -37,14 +147,11 @@ QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget
continue; continue;
} }
for (int i = 0; i < groupLayout->count(); ++i) { QList<QPair<QWidget *, QLayout *>> widgets;
QLayoutItem *item = groupLayout->itemAt(i); collectWidgets(groupLayout, widgets);
if (!item) {
continue;
}
// Direct widget children (checkboxes, labels with buddies, etc.) for (const auto &pair : widgets) {
if (QWidget *widget = item->widget()) { QWidget *widget = pair.first;
QString label; QString label;
auto *checkBox = qobject_cast<QCheckBox *>(widget); auto *checkBox = qobject_cast<QCheckBox *>(widget);
@ -52,94 +159,34 @@ QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget
label = checkBox->text(); label = checkBox->text();
} else { } else {
auto *labelWidget = qobject_cast<QLabel *>(widget); auto *labelWidget = qobject_cast<QLabel *>(widget);
if (labelWidget && labelWidget->text().isEmpty()) { if (!labelWidget || labelWidget->text().isEmpty() || !isValidSettingLabel(labelWidget)) {
continue; continue;
} else if (labelWidget) { }
label = labelWidget->text(); label = labelWidget->text();
} }
}
if (!label.isEmpty()) { if (label.isEmpty()) {
// Skip QLabels that are not setting names: HTML, path values, or excessively long
if (qobject_cast<QLabel *>(widget)) {
auto *labelWidget = static_cast<QLabel *>(widget);
if (labelWidget->textFormat() != Qt::PlainText) {
continue; continue;
} }
if (label.contains(QLatin1Char('/')) || label.contains(QLatin1Char('\\'))) {
continue;
}
if (label.size() > 60) {
continue;
}
}
// Strip accelerator markers (&) for search // Strip accelerator markers (&) for search
QString cleanLabel = label; QString cleanLabel = label;
cleanLabel.remove(QLatin1Char('&')); cleanLabel.remove(QLatin1Char('&'));
SettingsSearchEntry entry; QWidget *control = widget;
entry.pageIndex = pageIndex; if (auto *labelWidget = qobject_cast<QLabel *>(widget)) {
entry.groupTitle = groupTitle; control = controlForLabel(labelWidget, pair.second);
entry.widgetLabel = cleanLabel;
entry.fullSearchText = QStringLiteral("%1 %2 %3").arg(groupTitle, cleanLabel, cleanLabel.toLower());
entry.widget = widget;
entries.append(entry);
} }
}
// Layout items (nested grids, hboxes, vboxes)
if (item->layout()) {
for (int j = 0; j < item->layout()->count(); ++j) {
QLayoutItem *nestedItem = item->layout()->itemAt(j);
if (!nestedItem || !nestedItem->widget()) {
continue;
}
QWidget *nestedWidget = nestedItem->widget();
QString label;
auto *checkBox = qobject_cast<QCheckBox *>(nestedWidget);
if (checkBox) {
label = checkBox->text();
} else {
auto *labelWidget = qobject_cast<QLabel *>(nestedWidget);
if (labelWidget && !labelWidget->text().isEmpty()) {
label = labelWidget->text();
}
}
if (!label.isEmpty()) {
// Skip QLabels that are not setting names: HTML, path values, or excessively long
if (qobject_cast<QLabel *>(nestedWidget)) {
auto *labelWidget = static_cast<QLabel *>(nestedWidget);
if (labelWidget->textFormat() != Qt::PlainText) {
continue;
}
if (label.contains(QLatin1Char('/')) || label.contains(QLatin1Char('\\'))) {
continue;
}
if (label.size() > 60) {
continue;
}
}
QString cleanLabel = label;
cleanLabel.remove(QLatin1Char('&'));
SettingsSearchEntry entry; SettingsSearchEntry entry;
entry.pageIndex = pageIndex; entry.pageIndex = pageIndex;
entry.groupTitle = groupTitle; entry.groupTitle = groupTitle;
entry.widgetLabel = cleanLabel; entry.widgetLabel = cleanLabel;
entry.fullSearchText = entry.widget = control;
QStringLiteral("%1 %2 %3").arg(groupTitle, cleanLabel, cleanLabel.toLower()); entry.fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control);
entry.widget = nestedWidget;
entries.append(entry); entries.append(entry);
} }
} }
}
}
}
return entries; return entries;
} }

View file

@ -16,7 +16,6 @@ class AbstractSettingsPage : public QWidget
public: public:
virtual void retranslateUi() = 0; virtual void retranslateUi() = 0;
virtual QList<SettingsSearchEntry> getSearchEntries(); virtual QList<SettingsSearchEntry> getSearchEntries();
virtual void resetToDefaults();
protected: protected:
static QList<SettingsSearchEntry> autoDetectSearchEntries(QWidget *page, int pageIndex); static QList<SettingsSearchEntry> autoDetectSearchEntries(QWidget *page, int pageIndex);

View file

@ -18,6 +18,14 @@ void SettingsSearchDelegate::setPageNames(const QStringList &names)
pageNames = 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, void SettingsSearchDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option, const QStyleOptionViewItem &option,
const QModelIndex &index) const const QModelIndex &index) const
@ -35,6 +43,12 @@ void SettingsSearchDelegate::paint(QPainter *painter,
: option.palette.color(QPalette::Base); : option.palette.color(QPalette::Base);
painter->fillRect(option.rect, bgColor); 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 leftMargin = 12;
int topMargin = 8; int topMargin = 8;
int rightMargin = 12; int rightMargin = 12;
@ -43,6 +57,19 @@ void SettingsSearchDelegate::paint(QPainter *painter,
QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin); QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin);
int yPos = contentRect.top(); 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" // Breadcrumb: "Page > Group"
QFont breadcrumbFont = option.font; QFont breadcrumbFont = option.font;
breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1); breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1);
@ -50,7 +77,9 @@ void SettingsSearchDelegate::paint(QPainter *painter,
QColor breadcrumbColor = QColor breadcrumbColor =
isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text);
if (!isSelected) {
breadcrumbColor.setAlpha(180); breadcrumbColor.setAlpha(180);
}
QString pageName; QString pageName;
if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) { if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) {
@ -62,20 +91,21 @@ void SettingsSearchDelegate::paint(QPainter *painter,
QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle); QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle);
painter->setFont(breadcrumbFont); painter->setFont(breadcrumbFont);
painter->setPen(breadcrumbColor); painter->setPen(breadcrumbColor);
painter->drawText(QRect(contentRect.left(), yPos, contentRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter, painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter,
breadcrumbText); breadcrumbText);
yPos += 20; yPos += 20;
// Setting label // Setting label
QFont labelFont = option.font; QFont labelFont = option.font;
labelFont.setPointSize(labelFont.pointSize() + 1); labelFont.setPointSize(labelFont.pointSize() + 1);
labelFont.setBold(isSelected);
QColor labelColor = QColor labelColor =
isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text);
painter->setFont(labelFont); painter->setFont(labelFont);
painter->setPen(labelColor); painter->setPen(labelColor);
painter->drawText(QRect(contentRect.left(), yPos, contentRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter, painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter,
entry.widgetLabel); entry.widgetLabel);
yPos += 24; yPos += 24;

View file

@ -6,6 +6,7 @@
#ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H #ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
#define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H #define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
#include <QPixmap>
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
/** /**
@ -26,8 +27,12 @@ public:
/** @brief Sets the translated page names for breadcrumb display */ /** @brief Sets the translated page names for breadcrumb display */
void setPageNames(const QStringList &names); void setPageNames(const QStringList &names);
/** @brief Sets the icons shown in front of results, indexed by page position */
void setPageIcons(const QStringList &iconResources);
private: private:
QStringList pageNames; ///< Translated page names indexed by page position QStringList pageNames; ///< Translated page names indexed by page position
QList<QPixmap> pageIcons; ///< Icons of the related settings pages
}; };
#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H #endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H

View file

@ -5,7 +5,8 @@
*/ */
#include "settings_search_model.h" #include "settings_search_model.h"
#include <QSet> #include <QPair>
#include <algorithm>
SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent) SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent)
{ {
@ -88,7 +89,12 @@ void SettingsSearchModel::rebuildFilter()
} else { } else {
QList<QPair<int, int>> scored; // <score, index> QList<QPair<int, int>> scored; // <score, index>
for (int i = 0; i < sourceEntries.size(); ++i) { for (int i = 0; i < sourceEntries.size(); ++i) {
int score = relevanceScore(sourceEntries[i], filterQuery, filterRegex); 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) { if (score > 0) {
scored.append({-score, i}); // negative for descending sort scored.append({-score, i}); // negative for descending sort
} }
@ -120,12 +126,6 @@ QVariant SettingsSearchModel::data(const QModelIndex &index, int role) const
switch (role) { switch (role) {
case EntryRole: case EntryRole:
return QVariant::fromValue(entry); return QVariant::fromValue(entry);
case PageIndexRole:
return entry.pageIndex;
case GroupTitleRole:
return entry.groupTitle;
case WidgetLabelRole:
return entry.widgetLabel;
case Qt::DisplayRole: case Qt::DisplayRole:
return entry.widgetLabel; return entry.widgetLabel;
case Qt::ToolTipRole: case Qt::ToolTipRole:
@ -143,14 +143,3 @@ SettingsSearchEntry SettingsSearchModel::entryForIndex(const QModelIndex &index)
} }
return sourceEntries[filteredIndices[index.row()]]; return sourceEntries[filteredIndices[index.row()]];
} }
QList<int> SettingsSearchModel::filteredPageIndices() const
{
QSet<int> pages;
for (int idx : filteredIndices) {
pages.insert(sourceEntries[idx].pageIndex);
}
QList<int> result = pages.values();
std::sort(result.begin(), result.end());
return result;
}

View file

@ -22,7 +22,7 @@ struct SettingsSearchEntry
int pageIndex; ///< Index of the settings page this entry belongs to int pageIndex; ///< Index of the settings page this entry belongs to
QString groupTitle; ///< Title of the group/section within the page QString groupTitle; ///< Title of the group/section within the page
QString widgetLabel; ///< Display label for the setting widget QString widgetLabel; ///< Display label for the setting widget
QString fullSearchText; ///< Extended search text for full-text matching QString fullSearchText; ///< Extended search text (label, control text, tooltip) for full-text matching
QWidget *widget; ///< Pointer to the setting widget for focus/scrolling QWidget *widget; ///< Pointer to the setting widget for focus/scrolling
}; };
@ -38,16 +38,11 @@ class SettingsSearchModel : public QAbstractListModel
Q_OBJECT Q_OBJECT
public: public:
/** /**
* @brief Custom data roles for accessing entry fields * @brief Custom data role for accessing the full entry
*/ */
enum Roles enum Roles
{ {
EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object
PageIndexRole, ///< Entry's page index (int)
GroupTitleRole, ///< Entry's group title (QString)
WidgetLabelRole, ///< Entry's widget label (QString)
HighlightStartRole, ///< Start of highlight range
HighlightEndRole, ///< End of highlight range
}; };
explicit SettingsSearchModel(QObject *parent = nullptr); explicit SettingsSearchModel(QObject *parent = nullptr);
@ -64,8 +59,6 @@ public:
/** @brief Returns the full entry for a given model index */ /** @brief Returns the full entry for a given model index */
SettingsSearchEntry entryForIndex(const QModelIndex &index) const; SettingsSearchEntry entryForIndex(const QModelIndex &index) const;
/** @brief Returns sorted list of page indices present in filtered results */
QList<int> filteredPageIndices() const;
private: private:
QList<SettingsSearchEntry> sourceEntries; ///< Complete unfiltered entry list QList<SettingsSearchEntry> sourceEntries; ///< Complete unfiltered entry list

View file

@ -242,7 +242,7 @@ void TabRoom::actClearChat()
void TabRoom::actOpenChatSettings() void TabRoom::actOpenChatSettings()
{ {
DlgSettings settings(this); DlgSettings settings(this);
settings.setTab(5); settings.setTab(DlgSettings::MessagesPage);
settings.exec(); settings.exec();
} }