[Client] Settings: grouped left navigation for settings pages (#7484)

* [Client] Settings: grouped left navigation for settings pages

Replace the top icon-button bar in SettingsView with a grouped left
QTreeWidget: Application, Cards and Data, Chat and Sound, Advanced. The
active page is the selected row (palette highlight); search mode hides
the tree; group headers expand but never select. Page tr() contexts
stay DlgSettings; new group names are SettingsView strings.

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

* [Settings] Make groupedPages static const per review

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL
2026-10-18 22:48:50 +02:00
committed by GitHub
parent 665f7267e4
commit 860625be4d
2 changed files with 101 additions and 72 deletions

View File

@@ -28,7 +28,6 @@
#include <QEasingCurve>
#include <QEvent>
#include <QFlags>
#include <QFrame>
#include <QGraphicsOpacityEffect>
#include <QHBoxLayout>
#include <QIcon>
@@ -53,7 +52,7 @@
#include <QStackedLayout>
#include <QStackedWidget>
#include <QStringLiteral>
#include <QToolButton>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <QWidget>
#include <QtGlobal>
@@ -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<QList<SettingsPage>> 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<int>(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")};
}

View File

@@ -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<QToolButton *> 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<QTreeWidgetItem *> groupItems; ///< Group header rows, indexed by group
QList<QTreeWidgetItem *> 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<AbstractSettingsPage *> 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;