diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index 483aa31bf..3feda61b5 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -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(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(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(page)); pageItems[page] = item; + // Sub-page rows deep-link into the page's sections (scroll + highlight) + for (const SettingsPageSection §ion : pages[page]->getSections()) { + auto *sectionItem = new QTreeWidgetItem(item, {section.title}); + sectionItem->setData(0, Qt::UserRole, static_cast(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())) { + 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(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(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 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() diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.h b/cockatrice/src/interface/widgets/dialogs/settings_view.h index 214353660..8d188bf51 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.h +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.h @@ -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(); diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp index 9c43d1f7b..063f1b2a8 100644 --- a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp @@ -137,6 +137,30 @@ QList AbstractSettingsPage::getSearchEntries() return autoDetectSearchEntries(this, -1); } +QList AbstractSettingsPage::getSections() +{ + QList 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(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 AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex) { QList entries; diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h index 722a91e62..c38a5214b 100644 --- a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h @@ -2,6 +2,7 @@ #define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #include +#include #include #include @@ -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 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 getSections(); protected: static QList autoDetectSearchEntries(QWidget *page, int pageIndex);