[Client] Settings: sub-page rows deep-linking into page sections (#7487)

Add AbstractSettingsPage::getSections built from each page's group boxes.
Show each multi-group page's sections as child rows in the navigation
tree. Section rows show the parent page and scroll to and highlight the
group box. Start pages collapsed. Keep single-group pages flat.

Reworked to stay in the enlarged modal instead of the settings tab.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL
2026-10-18 22:48:51 +02:00
committed by GitHub
parent 4974cac59a
commit 4750e67b7f
4 changed files with 132 additions and 13 deletions

View File

@@ -54,6 +54,7 @@
#include <QStringLiteral>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/settings/personal_settings.h>
@@ -88,6 +89,33 @@ static QStringList pageIconResources()
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
}
namespace
{
/// Item data role holding a sub-page row's section widget, stored as a WidgetId
constexpr int SectionWidgetRole = Qt::UserRole + 1;
using WidgetId = quintptr;
/**
* @brief Packs a section widget for storage in a tree item's data role
* @param widget The section's group box (owned by its settings page)
* @return The widget as an opaque id
*/
WidgetId toWidgetId(QWidget *widget)
{
return reinterpret_cast<WidgetId>(widget);
}
/**
* @brief Unpacks a section widget from a tree item's data role
* @param id The stored id, or 0 for page rows without a section
* @return The section widget, or nullptr when unset
*/
QWidget *fromWidgetId(WidgetId id)
{
return reinterpret_cast<QWidget *>(id);
}
} // namespace
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
{
setupUi();
@@ -110,9 +138,6 @@ void SettingsView::setupUi()
auto *searchLayout = new QHBoxLayout;
searchLayout->addWidget(searchEdit);
// Navigation tree (built in setupNavTree)
setupNavTree();
// Pages stacked widget
pagesWidget = new QStackedWidget;
@@ -145,6 +170,9 @@ void SettingsView::setupUi()
Q_ASSERT(pages.size() == NumPages);
// Navigation tree (needs the pages for their sections)
setupNavTree();
// Search results view (hidden by default)
searchResultsView = new QListView;
searchResultsView->setUniformItemSizes(false);
@@ -246,6 +274,12 @@ void SettingsView::setupNavTree()
item->setIcon(0, QPixmap(iconResources[page]));
item->setData(0, Qt::UserRole, static_cast<int>(page));
pageItems[page] = item;
// Sub-page rows deep-link into the page's sections (scroll + highlight)
for (const SettingsPageSection &section : pages[page]->getSections()) {
auto *sectionItem = new QTreeWidgetItem(item, {section.title});
sectionItem->setData(0, Qt::UserRole, static_cast<int>(page));
sectionItem->setData(0, SectionWidgetRole, QVariant::fromValue(toWidgetId(section.sectionWidget)));
}
}
}
@@ -258,7 +292,12 @@ void SettingsView::setupNavTree()
}
bool ok = false;
int index = current->data(0, Qt::UserRole).toInt(&ok);
if (ok) {
if (!ok) {
return;
}
if (QWidget *section = fromWidgetId(current->data(0, SectionWidgetRole).value<WidgetId>())) {
navigateToSection(index, section);
} else {
onTabClicked(index);
}
});
@@ -409,19 +448,44 @@ void SettingsView::navigateToSearchResult(const QModelIndex &index)
// 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();
}
scrollToAndFlash(entry.widget);
entry.widget->setFocus();
flashWidget(entry.widget);
}
}
void SettingsView::navigateToSection(int pageIndex, QWidget *sectionWidget)
{
if (pageIndex < 0 || pageIndex >= pages.size() || !sectionWidget) {
return;
}
if (currentTabIndex != pageIndex) {
if (searchActive) {
switchToTabMode();
}
setActiveTab(pageIndex);
}
// Focus stays in the navigation tree; only scroll and highlight the section
scrollToAndFlash(sectionWidget);
}
void SettingsView::scrollToAndFlash(QWidget *target)
{
if (!target) {
return;
}
QWidget *widget = target;
while (widget) {
if (auto *scrollArea = qobject_cast<QScrollArea *>(widget)) {
scrollArea->ensureWidgetVisible(target);
break;
}
widget = widget->parentWidget();
}
flashWidget(target);
}
void SettingsView::setTab(int index)
{
if (index >= 0 && index < pageItems.size()) {
@@ -515,6 +579,18 @@ void SettingsView::retranslateNavNames()
pageItems[i]->setText(0, pageNames[i]);
}
}
// Sub-page rows mirror the pages' group box titles (already retranslated by the pages)
for (int i = 0; i < pageItems.size() && i < pages.size(); ++i) {
if (!pageItems[i]) {
continue;
}
const QList<SettingsPageSection> sections = pages[i]->getSections();
Q_ASSERT(pageItems[i]->childCount() == sections.size());
for (int c = 0; c < pageItems[i]->childCount() && c < sections.size(); ++c) {
pageItems[i]->child(c)->setText(0, sections[c].title);
}
}
}
QStringList SettingsView::translatedGroupNames()

View File

@@ -86,8 +86,10 @@ private:
void switchToTabMode();
void switchToSearchMode();
void navigateToSearchResult(const QModelIndex &index);
void navigateToSection(int pageIndex, QWidget *sectionWidget);
void setActiveTab(int index);
static void flashWidget(QWidget *widget);
static void scrollToAndFlash(QWidget *target);
static QStringList translatedPageNames();
static QStringList translatedGroupNames();

View File

@@ -137,6 +137,30 @@ QList<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries()
return autoDetectSearchEntries(this, -1);
}
QList<SettingsPageSection> AbstractSettingsPage::getSections()
{
QList<SettingsPageSection> sections;
// Walk the top-level layout in display order (creation order differs on some pages)
if (QLayout *mainLayout = layout()) {
for (int i = 0; i < mainLayout->count(); ++i) {
QLayoutItem *item = mainLayout->itemAt(i);
QWidget *widget = item ? item->widget() : nullptr;
auto *groupBox = qobject_cast<QGroupBox *>(widget);
if (!groupBox || groupBox->title().isEmpty()) {
continue;
}
sections.append({groupBox->title(), groupBox});
}
}
// A single group is the page itself; sub-page entries would only duplicate it
if (sections.size() < 2) {
sections.clear();
}
return sections;
}
QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex)
{
QList<SettingsSearchEntry> entries;

View File

@@ -2,6 +2,7 @@
#define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
#include <QList>
#include <QString>
#include <QWidget>
#include <qtmetamacros.h>
@@ -11,12 +12,28 @@
struct SettingsSearchEntry;
/**
* @brief A navigable section of a settings page (one group box)
*/
struct SettingsPageSection
{
QString title; ///< Translated group title
QWidget *sectionWidget = nullptr; ///< Group box to scroll to and highlight
};
class AbstractSettingsPage : public QWidget
{
Q_OBJECT
public:
virtual void retranslateUi() = 0;
virtual QList<SettingsSearchEntry> getSearchEntries();
/**
* @brief Sections of this page for sub-page navigation, in display order
*
* Scans the page's direct-child group boxes; pages with a single group
* report no sections so the nav stays flat for them.
*/
QList<SettingsPageSection> getSections();
protected:
static QList<SettingsSearchEntry> autoDetectSearchEntries(QWidget *page, int pageIndex);