mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-10-26 01:20:39 -05:00
[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:
@@ -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")};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user