diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index c2d9860c3..483aa31bf 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -53,7 +52,7 @@ #include #include #include -#include +#include #include #include #include @@ -111,8 +110,8 @@ void SettingsView::setupUi() auto *searchLayout = new QHBoxLayout; searchLayout->addWidget(searchEdit); - // Tab bar (built in setupTabBar) - setupTabBar(); + // Navigation tree (built in setupNavTree) + setupNavTree(); // Pages stacked widget pagesWidget = new QStackedWidget; @@ -163,8 +162,10 @@ void SettingsView::setupUi() connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] { const QStringList icons = pageIconResources(); - for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) { - tabButtons[i]->setIcon(QPixmap(icons[i])); + for (int i = 0; i < pageItems.size() && i < icons.size(); ++i) { + if (pageItems[i]) { + pageItems[i]->setIcon(0, QPixmap(icons[i])); + } } searchDelegate->setPageIcons(icons); searchResultsView->viewport()->update(); @@ -181,16 +182,14 @@ void SettingsView::setupUi() containerLayout->addWidget(searchResultsView); pagesContainer->setLayout(containerLayout); - // Main layout + // Main layout: search on top, navigation + pages side by side below + auto *contentLayout = new QHBoxLayout; + contentLayout->addWidget(navTree); + contentLayout->addWidget(pagesContainer, 1); + auto *mainLayout = new QVBoxLayout; mainLayout->addLayout(searchLayout); - mainLayout->addWidget(tabBarWidget); - auto *separator = new QFrame; - separator->setFrameShape(QFrame::HLine); - separator->setFrameShadow(QFrame::Sunken); - mainLayout->addWidget(separator); - mainLayout->addWidget(pagesContainer); - mainLayout->addSpacing(4); + mainLayout->addLayout(contentLayout); setLayout(mainLayout); // Keyboard shortcuts @@ -199,13 +198,13 @@ void SettingsView::setupUi() auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this); connect(nextTabShortcut, &QShortcut::activated, this, [this] { - int next = (currentTabIndex + 1) % tabButtons.size(); + int next = (currentTabIndex + 1) % pages.size(); setActiveTab(next); }); auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this); connect(prevTabShortcut, &QShortcut::activated, this, [this] { - int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size(); + int prev = (currentTabIndex - 1 + pages.size()) % pages.size(); setActiveTab(prev); }); @@ -213,32 +212,56 @@ void SettingsView::setupUi() setActiveTab(0); } -void SettingsView::setupTabBar() +void SettingsView::setupNavTree() { - tabBarWidget = new QWidget; - auto *tabLayout = new QHBoxLayout; - tabLayout->setContentsMargins(0, 0, 0, 0); - tabLayout->setSpacing(2); + navTree = new QTreeWidget; + navTree->setHeaderHidden(true); + navTree->setRootIsDecorated(true); + navTree->setIconSize(QSize(24, 24)); + navTree->setMinimumWidth(230); + navTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + navTree->setStyleSheet( + "QTreeWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); + // Groups of SettingsPage values, in navigation order + static const QList> groupedPages = { + {GeneralPage, AppearancePage, UserInterfacePage}, + {DeckEditorPage, StoragePage}, + {MessagesPage, SoundPage}, + {ShortcutsPage}, + }; + + const QStringList groupNames = translatedGroupNames(); + const QStringList pageNames = translatedPageNames(); const QStringList iconResources = pageIconResources(); - for (int i = 0; i < iconResources.size(); ++i) { - auto *tabButton = new QToolButton; - tabButton->setCheckable(true); - tabButton->setIcon(QPixmap(iconResources[i])); - tabButton->setIconSize(QSize(48, 48)); - tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); - tabButton->setAutoExclusive(true); - tabButton->setMinimumHeight(85); - tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); }); - - tabButtons.append(tabButton); - tabLayout->addWidget(tabButton); + pageItems.resize(NumPages); + for (int i = 0; i < groupedPages.size(); ++i) { + auto *group = new QTreeWidgetItem(navTree, {groupNames[i]}); + group->setFlags(Qt::ItemIsEnabled); // header row: expandable, not selectable + group->setExpanded(true); + groupItems.append(group); + for (SettingsPage page : groupedPages[i]) { + auto *item = new QTreeWidgetItem(group, {pageNames[page]}); + item->setIcon(0, QPixmap(iconResources[page])); + item->setData(0, Qt::UserRole, static_cast(page)); + pageItems[page] = item; + } } - tabBarWidget->setLayout(tabLayout); + Q_ASSERT(groupItems.size() == groupedPages.size()); + Q_ASSERT(!pageItems.contains(nullptr)); + + connect(navTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current) { + if (!current) { + return; + } + bool ok = false; + int index = current->data(0, Qt::UserRole).toInt(&ok); + if (ok) { + onTabClicked(index); + } + }); } void SettingsView::buildSearchIndex() @@ -266,35 +289,27 @@ void SettingsView::onTabClicked(int index) { if (searchActive) { switchToTabMode(); + } else if (index == currentTabIndex) { + return; } setActiveTab(index); } void SettingsView::setActiveTab(int index) { - if (index < 0 || index >= tabButtons.size()) { + if (index < 0 || index >= pageItems.size() || !pageItems[index]) { return; } currentTabIndex = index; pagesWidget->setCurrentIndex(index); - for (int i = 0; i < tabButtons.size(); ++i) { - tabButtons[i]->setChecked(i == index); - } - - // Style active tab with a thick accent border + subtle background tint - for (int i = 0; i < tabButtons.size(); ++i) { - if (i == index) { - tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); " - "border-top-left-radius: 4px; border-top-right-radius: 4px; " - "background: palette(window); padding-bottom: 1px; }"); - } else { - tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; " - "border-top-left-radius: 4px; border-top-right-radius: 4px; " - "background: transparent; }"); - } + // Selecting the item also expands a collapsed group; selecting the already + // current item emits nothing, so the currentItemChanged handler cannot loop. + if (QTreeWidgetItem *parent = pageItems[index]->parent()) { + parent->setExpanded(true); } + navTree->setCurrentItem(pageItems[index]); } void SettingsView::flashWidget(QWidget *widget) @@ -356,7 +371,7 @@ void SettingsView::onSearchTextChanged(const QString &text) void SettingsView::switchToSearchMode() { searchActive = true; - tabBarWidget->setVisible(false); + navTree->setVisible(false); pagesWidget->setVisible(false); searchResultsView->setVisible(true); if (searchModel->rowCount(QModelIndex()) > 0) { @@ -367,7 +382,7 @@ void SettingsView::switchToSearchMode() void SettingsView::switchToTabMode() { searchActive = false; - tabBarWidget->setVisible(true); + navTree->setVisible(true); pagesWidget->setVisible(true); searchResultsView->setVisible(false); searchEdit->blockSignals(true); @@ -409,7 +424,7 @@ void SettingsView::navigateToSearchResult(const QModelIndex &index) void SettingsView::setTab(int index) { - if (index >= 0 && index < tabButtons.size()) { + if (index >= 0 && index < pageItems.size()) { setActiveTab(index); } } @@ -464,7 +479,7 @@ bool SettingsView::eventFilter(QObject *watched, QEvent *event) void SettingsView::retranslateUi() { - retranslateTabNames(); + retranslateNavNames(); // NOTE: the "DlgSettings" context is deliberate — these strings lived in DlgSettings // before the view was extracted, and keeping the context preserves existing translations. @@ -487,11 +502,22 @@ QStringList SettingsView::translatedPageNames() QCoreApplication::translate("DlgSettings", "Shortcuts")}; } -void SettingsView::retranslateTabNames() +void SettingsView::retranslateNavNames() { - const QStringList tabLabels = translatedPageNames(); + const QStringList groupNames = translatedGroupNames(); + for (int i = 0; i < groupItems.size() && i < groupNames.size(); ++i) { + groupItems[i]->setText(0, groupNames[i]); + } - for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { - tabButtons[i]->setText(tabLabels[i]); + const QStringList pageNames = translatedPageNames(); + for (int i = 0; i < pageItems.size() && i < pageNames.size(); ++i) { + if (pageItems[i]) { + pageItems[i]->setText(0, pageNames[i]); + } } } + +QStringList SettingsView::translatedGroupNames() +{ + return {tr("Application"), tr("Cards & Data"), tr("Chat & Sound"), tr("Advanced")}; +} diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.h b/cockatrice/src/interface/widgets/dialogs/settings_view.h index f1d248679..214353660 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.h +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.h @@ -17,7 +17,8 @@ class QObject; class QEvent; class QStackedWidget; -class QToolButton; +class QTreeWidget; +class QTreeWidgetItem; class QListView; class QLineEdit; class AbstractSettingsPage; @@ -25,9 +26,9 @@ class SettingsSearchModel; class SettingsSearchDelegate; /** - * @brief Settings pages with tabbed navigation and search, without any dialog chrome + * @brief Settings pages with grouped left navigation and search, without any dialog chrome * - * Owns the search bar, the tab bar, the stacked settings pages and the search + * Owns the search bar, the navigation tree, the stacked settings pages and the search * results view. The settings dialog only adds its own frame (buttons, titles). */ class SettingsView : public QWidget @@ -35,7 +36,7 @@ class SettingsView : public QWidget Q_OBJECT public: /** - * @brief Page order in the tab bar, matching the order pages are added in setupUi() + * @brief Page order in the navigation, 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. @@ -64,12 +65,13 @@ private slots: private: // UI elements - QLineEdit *searchEdit; ///< Search bar for filtering settings - QWidget *tabBarWidget; ///< Container widget for the tab buttons - QList tabButtons; ///< Navigation tab buttons - QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages - QListView *searchResultsView; ///< Search results list view - QWidget *pagesContainer; ///< Container stacking pages and search results + QLineEdit *searchEdit; ///< Search bar for filtering settings + QTreeWidget *navTree; ///< Grouped navigation tree for the pages + QList groupItems; ///< Group header rows, indexed by group + QList pageItems; ///< Page rows, indexed by SettingsPage + QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages + QListView *searchResultsView; ///< Search results list view + QWidget *pagesContainer; ///< Container stacking pages and search results // Data QList pages; ///< All settings page instances @@ -79,7 +81,7 @@ private: bool searchActive; ///< Whether search mode is active void setupUi(); - void setupTabBar(); + void setupNavTree(); void buildSearchIndex(); void switchToTabMode(); void switchToSearchMode(); @@ -87,9 +89,10 @@ private: void setActiveTab(int index); static void flashWidget(QWidget *widget); static QStringList translatedPageNames(); + static QStringList translatedGroupNames(); void retranslateUi(); - void retranslateTabNames(); + void retranslateNavNames(); protected: bool eventFilter(QObject *watched, QEvent *event) override;