[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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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()