Cockatrice/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp
BruebachL bf6b2a90bc
[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>
2026-08-08 22:19:21 +02:00

196 lines
6 KiB
C++

#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;
}