mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
Comments
Took 1 hour 14 minutes
This commit is contained in:
parent
ccd2e15629
commit
8072eef462
9 changed files with 262 additions and 191 deletions
|
|
@ -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]);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,107 +147,44 @@ 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) {
|
|
||||||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct widget children (checkboxes, labels with buddies, etc.)
|
// Strip accelerator markers (&) for search
|
||||||
if (QWidget *widget = item->widget()) {
|
QString cleanLabel = label;
|
||||||
QString label;
|
cleanLabel.remove(QLatin1Char('&'));
|
||||||
|
|
||||||
auto *checkBox = qobject_cast<QCheckBox *>(widget);
|
QWidget *control = widget;
|
||||||
if (checkBox) {
|
if (auto *labelWidget = qobject_cast<QLabel *>(widget)) {
|
||||||
label = checkBox->text();
|
control = controlForLabel(labelWidget, pair.second);
|
||||||
} else {
|
|
||||||
auto *labelWidget = qobject_cast<QLabel *>(widget);
|
|
||||||
if (labelWidget && labelWidget->text().isEmpty()) {
|
|
||||||
continue;
|
|
||||||
} else if (labelWidget) {
|
|
||||||
label = labelWidget->text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (label.contains(QLatin1Char('/')) || label.contains(QLatin1Char('\\'))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (label.size() > 60) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip accelerator markers (&) for search
|
|
||||||
QString cleanLabel = label;
|
|
||||||
cleanLabel.remove(QLatin1Char('&'));
|
|
||||||
|
|
||||||
SettingsSearchEntry entry;
|
|
||||||
entry.pageIndex = pageIndex;
|
|
||||||
entry.groupTitle = groupTitle;
|
|
||||||
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)
|
SettingsSearchEntry entry;
|
||||||
if (item->layout()) {
|
entry.pageIndex = pageIndex;
|
||||||
for (int j = 0; j < item->layout()->count(); ++j) {
|
entry.groupTitle = groupTitle;
|
||||||
QLayoutItem *nestedItem = item->layout()->itemAt(j);
|
entry.widgetLabel = cleanLabel;
|
||||||
if (!nestedItem || !nestedItem->widget()) {
|
entry.widget = control;
|
||||||
continue;
|
entry.fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control);
|
||||||
}
|
entries.append(entry);
|
||||||
|
|
||||||
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;
|
|
||||||
entry.pageIndex = pageIndex;
|
|
||||||
entry.groupTitle = groupTitle;
|
|
||||||
entry.widgetLabel = cleanLabel;
|
|
||||||
entry.fullSearchText =
|
|
||||||
QStringLiteral("%1 %2 %3").arg(groupTitle, cleanLabel, cleanLabel.toLower());
|
|
||||||
entry.widget = nestedWidget;
|
|
||||||
entries.append(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
breadcrumbColor.setAlpha(180);
|
if (!isSelected) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue