mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Client] Settings: extract SettingsView from DlgSettings (#7482)
* [Client] Settings: extract SettingsView from DlgSettings Move the search bar, tab bar, stacked pages and search results out of DlgSettings into a reusable SettingsView widget. The dialog becomes a thin frame (title, OK button, close validation) delegating to the view. No visual or behavior change; tr() contexts preserved as DlgSettings. * [Client] Settings: match CI clang-format-18 line breaking --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
a12b97ec50
commit
679762c624
5 changed files with 625 additions and 519 deletions
|
|
@ -60,6 +60,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||||
src/interface/widgets/dialogs/path_safety_warning.cpp
|
src/interface/widgets/dialogs/path_safety_warning.cpp
|
||||||
|
src/interface/widgets/dialogs/settings_view.cpp
|
||||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||||
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
||||||
|
|
|
||||||
|
|
@ -1,205 +1,41 @@
|
||||||
/**
|
/**
|
||||||
* @file dlg_settings.cpp
|
* @file dlg_settings.cpp
|
||||||
* @brief Implementation of the main settings dialog
|
* @brief Implementation of the main settings dialog (thin frame around SettingsView)
|
||||||
* @ingroup Dialogs
|
* @ingroup Dialogs
|
||||||
*/
|
*/
|
||||||
#include "dlg_settings.h"
|
#include "dlg_settings.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../pixel_map_generator.h"
|
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../settings_page/abstract_settings_page.h"
|
#include "../settings_page/abstract_settings_page.h"
|
||||||
#include "../settings_page/appearance_settings_page.h"
|
|
||||||
#include "../settings_page/deck_editor_settings_page.h"
|
|
||||||
#include "../settings_page/general_settings_page.h"
|
|
||||||
#include "../settings_page/messages_settings_page.h"
|
|
||||||
#include "../settings_page/settings_search_delegate.h"
|
|
||||||
#include "../settings_page/settings_search_model.h"
|
|
||||||
#include "../settings_page/shortcut_settings_page.h"
|
|
||||||
#include "../settings_page/sound_settings_page.h"
|
|
||||||
#include "../settings_page/storage_settings_page.h"
|
|
||||||
#include "../settings_page/user_interface_settings_page.h"
|
|
||||||
#include "libcockatrice/card/database/card_database.h"
|
#include "libcockatrice/card/database/card_database.h"
|
||||||
#include "libcockatrice/card/database/card_database_loader.h"
|
#include "libcockatrice/card/database/card_database_loader.h"
|
||||||
#include "libcockatrice/card/database/card_database_manager.h"
|
#include "libcockatrice/card/database/card_database_manager.h"
|
||||||
|
|
||||||
#include <QAbstractAnimation>
|
|
||||||
#include <QAbstractItemView>
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBrush>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QColor>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QEasingCurve>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFlags>
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QIcon>
|
|
||||||
#include <QKeyEvent>
|
|
||||||
#include <QKeySequence>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QListView>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QMessageLogger>
|
#include <QMessageLogger>
|
||||||
#include <QModelIndex>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QPalette>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRect>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSequentialAnimationGroup>
|
|
||||||
#include <QShortcut>
|
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QSizePolicy>
|
|
||||||
#include <QStackedLayout>
|
|
||||||
#include <QStackedWidget>
|
|
||||||
#include <QStringLiteral>
|
|
||||||
#include <QToolButton>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
#include <qnamespace.h>
|
|
||||||
|
|
||||||
class QModelIndex;
|
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Wraps a widget in a scroll area for long settings pages
|
|
||||||
* @param widget The widget to wrap
|
|
||||||
* @return The scroll area containing the widget
|
|
||||||
*/
|
|
||||||
static QScrollArea *makeScrollable(QWidget *widget)
|
|
||||||
{
|
|
||||||
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
|
||||||
|
|
||||||
auto *scrollArea = new QScrollArea;
|
|
||||||
scrollArea->setWidgetResizable(true);
|
|
||||||
scrollArea->setContentsMargins(0, 0, 0, 0);
|
|
||||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
scrollArea->horizontalScrollBar()->setEnabled(false);
|
|
||||||
scrollArea->setWidget(widget);
|
|
||||||
return scrollArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
|
|
||||||
*/
|
|
||||||
static QStringList pageIconResources()
|
|
||||||
{
|
|
||||||
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
|
|
||||||
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
|
|
||||||
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
|
|
||||||
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
|
|
||||||
}
|
|
||||||
|
|
||||||
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false)
|
|
||||||
{
|
{
|
||||||
auto rec = QGuiApplication::primaryScreen()->availableGeometry();
|
auto rec = QGuiApplication::primaryScreen()->availableGeometry();
|
||||||
setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
|
setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||||
|
|
||||||
setupUi();
|
settingsView = new SettingsView(this);
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
|
||||||
retranslateUi();
|
|
||||||
|
|
||||||
searchEdit->setFocus();
|
|
||||||
|
|
||||||
adjustSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::setupUi()
|
|
||||||
{
|
|
||||||
// Search bar
|
|
||||||
searchEdit = new QLineEdit;
|
|
||||||
searchEdit->setClearButtonEnabled(true);
|
|
||||||
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
|
||||||
searchEdit->installEventFilter(this);
|
|
||||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
|
||||||
|
|
||||||
auto *searchLayout = new QHBoxLayout;
|
|
||||||
searchLayout->addWidget(searchEdit);
|
|
||||||
|
|
||||||
// Tab bar (built in setupTabBar)
|
|
||||||
setupTabBar();
|
|
||||||
|
|
||||||
// Pages stacked widget
|
|
||||||
pagesWidget = new QStackedWidget;
|
|
||||||
|
|
||||||
auto *generalPage = new GeneralSettingsPage;
|
|
||||||
auto *appearancePage = new AppearanceSettingsPage;
|
|
||||||
auto *userInterfacePage = new UserInterfaceSettingsPage;
|
|
||||||
auto *deckEditorPage = new DeckEditorSettingsPage;
|
|
||||||
auto *storagePage = new StorageSettingsPage;
|
|
||||||
auto *messagesPage = new MessagesSettingsPage;
|
|
||||||
auto *soundPage = new SoundSettingsPage;
|
|
||||||
auto *shortcutsPage = new ShortcutSettingsPage;
|
|
||||||
|
|
||||||
pages.append(generalPage);
|
|
||||||
pages.append(appearancePage);
|
|
||||||
pages.append(userInterfacePage);
|
|
||||||
pages.append(deckEditorPage);
|
|
||||||
pages.append(storagePage);
|
|
||||||
pages.append(messagesPage);
|
|
||||||
pages.append(soundPage);
|
|
||||||
pages.append(shortcutsPage);
|
|
||||||
|
|
||||||
pagesWidget->addWidget(makeScrollable(generalPage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(appearancePage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(storagePage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(messagesPage));
|
|
||||||
pagesWidget->addWidget(soundPage);
|
|
||||||
pagesWidget->addWidget(shortcutsPage);
|
|
||||||
|
|
||||||
Q_ASSERT(pages.size() == NumPages);
|
|
||||||
|
|
||||||
// Search results view (hidden by default)
|
|
||||||
searchResultsView = new QListView;
|
|
||||||
searchResultsView->setUniformItemSizes(false);
|
|
||||||
searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
searchResultsView->setVisible(false);
|
|
||||||
searchResultsView->setStyleSheet(
|
|
||||||
"QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
|
|
||||||
|
|
||||||
searchModel = new SettingsSearchModel(this);
|
|
||||||
searchDelegate = new SettingsSearchDelegate(this);
|
|
||||||
searchResultsView->setModel(searchModel);
|
|
||||||
searchResultsView->setItemDelegate(searchDelegate);
|
|
||||||
connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked);
|
|
||||||
|
|
||||||
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]));
|
|
||||||
}
|
|
||||||
searchDelegate->setPageIcons(icons);
|
|
||||||
searchResultsView->viewport()->update();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build search index after pages are created
|
|
||||||
buildSearchIndex();
|
|
||||||
|
|
||||||
// Pages container (stacked widget + search results overlay)
|
|
||||||
pagesContainer = new QWidget;
|
|
||||||
auto *containerLayout = new QStackedLayout;
|
|
||||||
containerLayout->setStackingMode(QStackedLayout::StackAll);
|
|
||||||
containerLayout->addWidget(pagesWidget);
|
|
||||||
containerLayout->addWidget(searchResultsView);
|
|
||||||
pagesContainer->setLayout(containerLayout);
|
|
||||||
|
|
||||||
// Bottom buttons
|
// Bottom buttons
|
||||||
auto *buttonBox = new QHBoxLayout;
|
auto *buttonBox = new QHBoxLayout;
|
||||||
|
|
@ -211,241 +47,25 @@ void DlgSettings::setupUi()
|
||||||
|
|
||||||
// Main layout
|
// Main layout
|
||||||
auto *mainLayout = new QVBoxLayout;
|
auto *mainLayout = new QVBoxLayout;
|
||||||
mainLayout->addLayout(searchLayout);
|
mainLayout->addWidget(settingsView);
|
||||||
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->addSpacing(4);
|
||||||
mainLayout->addLayout(buttonBox);
|
mainLayout->addLayout(buttonBox);
|
||||||
setLayout(mainLayout);
|
setLayout(mainLayout);
|
||||||
|
|
||||||
// Keyboard shortcuts
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
||||||
auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this);
|
retranslateUi();
|
||||||
connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus));
|
|
||||||
|
|
||||||
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
|
adjustSize();
|
||||||
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
|
|
||||||
int next = (currentTabIndex + 1) % tabButtons.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();
|
|
||||||
setActiveTab(prev);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize to first tab
|
|
||||||
setActiveTab(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::setupTabBar()
|
|
||||||
{
|
|
||||||
tabBarWidget = new QWidget;
|
|
||||||
auto *tabLayout = new QHBoxLayout;
|
|
||||||
tabLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
tabLayout->setSpacing(2);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
tabBarWidget->setLayout(tabLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::buildSearchIndex()
|
|
||||||
{
|
|
||||||
QList<SettingsSearchEntry> allEntries;
|
|
||||||
|
|
||||||
const QStringList pageNames = translatedPageNames();
|
|
||||||
searchDelegate->setPageNames(pageNames);
|
|
||||||
searchDelegate->setPageIcons(pageIconResources());
|
|
||||||
|
|
||||||
for (int i = 0; i < pages.size(); ++i) {
|
|
||||||
QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries();
|
|
||||||
for (auto &entry : pageEntries) {
|
|
||||||
if (entry.pageIndex == -1) {
|
|
||||||
entry.pageIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allEntries.append(pageEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
searchModel->setSourceEntries(allEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::onTabClicked(int index)
|
|
||||||
{
|
|
||||||
if (searchActive) {
|
|
||||||
switchToTabMode();
|
|
||||||
}
|
|
||||||
setActiveTab(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::setActiveTab(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= tabButtons.size()) {
|
|
||||||
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; }");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::flashWidget(QWidget *widget)
|
|
||||||
{
|
|
||||||
auto *overlay = new QWidget(widget);
|
|
||||||
overlay->setGeometry(widget->rect());
|
|
||||||
overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
||||||
|
|
||||||
QPalette pal = overlay->palette();
|
|
||||||
QColor flashColor = pal.color(QPalette::Highlight);
|
|
||||||
flashColor.setAlpha(100);
|
|
||||||
pal.setBrush(QPalette::Window, flashColor);
|
|
||||||
overlay->setPalette(pal);
|
|
||||||
overlay->setAutoFillBackground(true);
|
|
||||||
|
|
||||||
auto *effect = new QGraphicsOpacityEffect(overlay);
|
|
||||||
effect->setOpacity(0.0);
|
|
||||||
overlay->setGraphicsEffect(effect);
|
|
||||||
overlay->show();
|
|
||||||
overlay->raise();
|
|
||||||
|
|
||||||
auto *flashIn = new QPropertyAnimation(effect, "opacity");
|
|
||||||
flashIn->setDuration(120);
|
|
||||||
flashIn->setStartValue(0.0);
|
|
||||||
flashIn->setEndValue(0.6);
|
|
||||||
flashIn->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
|
|
||||||
auto *fadeOut = new QPropertyAnimation(effect, "opacity");
|
|
||||||
fadeOut->setDuration(900);
|
|
||||||
fadeOut->setStartValue(0.6);
|
|
||||||
fadeOut->setEndValue(0.0);
|
|
||||||
fadeOut->setEasingCurve(QEasingCurve::InCubic);
|
|
||||||
|
|
||||||
auto *group = new QSequentialAnimationGroup(overlay);
|
|
||||||
group->addAnimation(flashIn);
|
|
||||||
group->addAnimation(fadeOut);
|
|
||||||
|
|
||||||
connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater);
|
|
||||||
|
|
||||||
group->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::onSearchTextChanged(const QString &text)
|
|
||||||
{
|
|
||||||
searchModel->setFilterString(text);
|
|
||||||
|
|
||||||
if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) {
|
|
||||||
if (!searchActive) {
|
|
||||||
switchToSearchMode();
|
|
||||||
}
|
|
||||||
if (searchModel->rowCount(QModelIndex()) > 0) {
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(0));
|
|
||||||
}
|
|
||||||
} else if (searchActive) {
|
|
||||||
switchToTabMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::switchToSearchMode()
|
|
||||||
{
|
|
||||||
searchActive = true;
|
|
||||||
tabBarWidget->setVisible(false);
|
|
||||||
pagesWidget->setVisible(false);
|
|
||||||
searchResultsView->setVisible(true);
|
|
||||||
if (searchModel->rowCount(QModelIndex()) > 0) {
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::switchToTabMode()
|
|
||||||
{
|
|
||||||
searchActive = false;
|
|
||||||
tabBarWidget->setVisible(true);
|
|
||||||
pagesWidget->setVisible(true);
|
|
||||||
searchResultsView->setVisible(false);
|
|
||||||
searchEdit->blockSignals(true);
|
|
||||||
searchEdit->clear();
|
|
||||||
searchEdit->blockSignals(false);
|
|
||||||
setActiveTab(currentTabIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::onSearchResultClicked(const QModelIndex &index)
|
|
||||||
{
|
|
||||||
navigateToSearchResult(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::navigateToSearchResult(const QModelIndex &index)
|
|
||||||
{
|
|
||||||
SettingsSearchEntry entry = searchModel->entryForIndex(index);
|
|
||||||
if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to the page
|
|
||||||
switchToTabMode();
|
|
||||||
setActiveTab(entry.pageIndex);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
entry.widget->setFocus();
|
|
||||||
flashWidget(entry.widget);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlgSettings::setTab(int index)
|
void DlgSettings::setTab(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < tabButtons.size()) {
|
settingsView->setTab(index);
|
||||||
setActiveTab(index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
||||||
{
|
{
|
||||||
return pages.value(static_cast<int>(which));
|
return settingsView->page(which);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlgSettings::updateLanguage()
|
void DlgSettings::updateLanguage()
|
||||||
|
|
@ -454,49 +74,6 @@ void DlgSettings::updateLanguage()
|
||||||
installNewTranslator();
|
installNewTranslator();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
|
|
||||||
{
|
|
||||||
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
|
||||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
|
||||||
if (keyEvent->key() == Qt::Key_Escape) {
|
|
||||||
if (searchActive) {
|
|
||||||
switchToTabMode();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
|
|
||||||
if (searchActive) {
|
|
||||||
if (searchResultsView->currentIndex().isValid()) {
|
|
||||||
navigateToSearchResult(searchResultsView->currentIndex());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (keyEvent->key() == Qt::Key_Down) {
|
|
||||||
if (searchActive) {
|
|
||||||
int nextRow = searchResultsView->currentIndex().row() + 1;
|
|
||||||
if (nextRow >= searchModel->rowCount()) {
|
|
||||||
nextRow = 0;
|
|
||||||
}
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(nextRow));
|
|
||||||
searchResultsView->scrollTo(searchModel->index(nextRow));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (keyEvent->key() == Qt::Key_Up) {
|
|
||||||
if (searchActive) {
|
|
||||||
int prevRow = searchResultsView->currentIndex().row() - 1;
|
|
||||||
if (prevRow < 0) {
|
|
||||||
prevRow = searchModel->rowCount() - 1;
|
|
||||||
}
|
|
||||||
if (prevRow >= 0) {
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(prevRow));
|
|
||||||
searchResultsView->scrollTo(searchModel->index(prevRow));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QDialog::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::closeEvent(QCloseEvent *event)
|
void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
{
|
{
|
||||||
bool showLoadError = true;
|
bool showLoadError = true;
|
||||||
|
|
@ -576,26 +153,5 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
void DlgSettings::retranslateUi()
|
void DlgSettings::retranslateUi()
|
||||||
{
|
{
|
||||||
setWindowTitle(tr("Settings"));
|
setWindowTitle(tr("Settings"));
|
||||||
retranslateTabNames();
|
|
||||||
|
|
||||||
searchEdit->setPlaceholderText(tr("Search settings..."));
|
|
||||||
okButton->setText(tr("OK"));
|
okButton->setText(tr("OK"));
|
||||||
|
|
||||||
// Rebuild search index for translated text
|
|
||||||
buildSearchIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DlgSettings::translatedPageNames()
|
|
||||||
{
|
|
||||||
return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"),
|
|
||||||
tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")};
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::retranslateTabNames()
|
|
||||||
{
|
|
||||||
const QStringList tabLabels = translatedPageNames();
|
|
||||||
|
|
||||||
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
|
|
||||||
tabButtons[i]->setText(tabLabels[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,105 +1,59 @@
|
||||||
/**
|
/**
|
||||||
* @file dlg_settings.h
|
* @file dlg_settings.h
|
||||||
* @brief Main settings dialog for the Cockatrice client
|
* @brief Main settings dialog for the Cockatrice client (thin frame around SettingsView)
|
||||||
* @ingroup Dialogs
|
* @ingroup Dialogs
|
||||||
*/
|
*/
|
||||||
#ifndef DLG_SETTINGS_H
|
#ifndef DLG_SETTINGS_H
|
||||||
#define DLG_SETTINGS_H
|
#define DLG_SETTINGS_H
|
||||||
|
|
||||||
|
#include "settings_view.h"
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QList>
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QModelIndex;
|
class AbstractSettingsPage;
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
||||||
|
|
||||||
class QStackedWidget;
|
|
||||||
class QToolButton;
|
|
||||||
class QListView;
|
|
||||||
class QLineEdit;
|
|
||||||
class AbstractSettingsPage;
|
|
||||||
class SettingsSearchModel;
|
|
||||||
class SettingsSearchDelegate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Main application settings dialog with tabbed navigation and search
|
* @brief Main application settings dialog hosting the shared SettingsView
|
||||||
*
|
*
|
||||||
* Provides a modern settings interface organized into tabbed pages. Users can
|
* Thin QDialog frame around SettingsView: only the window title, the OK button
|
||||||
* either navigate by clicking tabs or search for specific settings using the
|
* and the close validation live here. All pages, navigation and search are
|
||||||
* built-in search bar. Search results are filtered and ranked by relevance.
|
* owned by SettingsView so other hosts (e.g. a settings tab) can reuse them.
|
||||||
*/
|
*/
|
||||||
class DlgSettings : public QDialog
|
class DlgSettings : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
/**
|
using SettingsPage = SettingsView::SettingsPage;
|
||||||
* @brief Page order in the tab bar, matching the order pages are added in setupUi()
|
static constexpr SettingsPage GeneralPage = SettingsView::GeneralPage;
|
||||||
*
|
static constexpr SettingsPage AppearancePage = SettingsView::AppearancePage;
|
||||||
* Use these values instead of raw indices so reordering pages never silently
|
static constexpr SettingsPage UserInterfacePage = SettingsView::UserInterfacePage;
|
||||||
* breaks external callers like tab_room.cpp.
|
static constexpr SettingsPage DeckEditorPage = SettingsView::DeckEditorPage;
|
||||||
*/
|
static constexpr SettingsPage StoragePage = SettingsView::StoragePage;
|
||||||
enum SettingsPage
|
static constexpr SettingsPage MessagesPage = SettingsView::MessagesPage;
|
||||||
{
|
static constexpr SettingsPage SoundPage = SettingsView::SoundPage;
|
||||||
GeneralPage = 0,
|
static constexpr SettingsPage ShortcutsPage = SettingsView::ShortcutsPage;
|
||||||
AppearancePage,
|
static constexpr SettingsPage NumPages = SettingsView::NumPages;
|
||||||
UserInterfacePage,
|
|
||||||
DeckEditorPage,
|
|
||||||
StoragePage,
|
|
||||||
MessagesPage,
|
|
||||||
SoundPage,
|
|
||||||
ShortcutsPage,
|
|
||||||
NumPages
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit DlgSettings(QWidget *parent = nullptr);
|
explicit DlgSettings(QWidget *parent = nullptr);
|
||||||
void setTab(int index);
|
void setTab(int index);
|
||||||
AbstractSettingsPage *page(SettingsPage which) const;
|
AbstractSettingsPage *page(SettingsPage which) const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onTabClicked(int index);
|
|
||||||
void onSearchTextChanged(const QString &text);
|
|
||||||
void onSearchResultClicked(const QModelIndex &index);
|
|
||||||
void updateLanguage();
|
void updateLanguage();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// UI elements
|
SettingsView *settingsView; ///< Shared settings content
|
||||||
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
QPushButton *okButton; ///< Button to close the dialog
|
||||||
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
|
|
||||||
QPushButton *okButton; ///< Button to close the dialog
|
|
||||||
|
|
||||||
// Data
|
|
||||||
QList<AbstractSettingsPage *> pages; ///< All settings page instances
|
|
||||||
SettingsSearchModel *searchModel; ///< Model for search results
|
|
||||||
SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering
|
|
||||||
int currentTabIndex; ///< Currently active tab index
|
|
||||||
bool searchActive; ///< Whether search mode is active
|
|
||||||
|
|
||||||
void setupUi();
|
|
||||||
void setupTabBar();
|
|
||||||
void buildSearchIndex();
|
|
||||||
void switchToTabMode();
|
|
||||||
void switchToSearchMode();
|
|
||||||
void navigateToSearchResult(const QModelIndex &index);
|
|
||||||
void setActiveTab(int index);
|
|
||||||
static void flashWidget(QWidget *widget);
|
|
||||||
static QStringList translatedPageNames();
|
|
||||||
|
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void retranslateTabNames();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DLG_SETTINGS_H
|
#endif // DLG_SETTINGS_H
|
||||||
|
|
|
||||||
497
cockatrice/src/interface/widgets/dialogs/settings_view.cpp
Normal file
497
cockatrice/src/interface/widgets/dialogs/settings_view.cpp
Normal file
|
|
@ -0,0 +1,497 @@
|
||||||
|
/**
|
||||||
|
* @file settings_view.cpp
|
||||||
|
* @brief Implementation of the settings content widget
|
||||||
|
* @ingroup Dialogs
|
||||||
|
*/
|
||||||
|
#include "settings_view.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
|
#include "../settings_page/abstract_settings_page.h"
|
||||||
|
#include "../settings_page/appearance_settings_page.h"
|
||||||
|
#include "../settings_page/deck_editor_settings_page.h"
|
||||||
|
#include "../settings_page/general_settings_page.h"
|
||||||
|
#include "../settings_page/messages_settings_page.h"
|
||||||
|
#include "../settings_page/settings_search_delegate.h"
|
||||||
|
#include "../settings_page/settings_search_model.h"
|
||||||
|
#include "../settings_page/shortcut_settings_page.h"
|
||||||
|
#include "../settings_page/sound_settings_page.h"
|
||||||
|
#include "../settings_page/storage_settings_page.h"
|
||||||
|
#include "../settings_page/user_interface_settings_page.h"
|
||||||
|
|
||||||
|
#include <QAbstractAnimation>
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QBrush>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QEasingCurve>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFlags>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QGraphicsOpacityEffect>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListView>
|
||||||
|
#include <QMessageLogger>
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QSequentialAnimationGroup>
|
||||||
|
#include <QShortcut>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
#include <QStackedLayout>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QStringLiteral>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wraps a widget in a scroll area for long settings pages
|
||||||
|
* @param widget The widget to wrap
|
||||||
|
* @return The scroll area containing the widget
|
||||||
|
*/
|
||||||
|
static QScrollArea *makeScrollable(QWidget *widget)
|
||||||
|
{
|
||||||
|
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
||||||
|
|
||||||
|
auto *scrollArea = new QScrollArea;
|
||||||
|
scrollArea->setWidgetResizable(true);
|
||||||
|
scrollArea->setContentsMargins(0, 0, 0, 0);
|
||||||
|
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
scrollArea->horizontalScrollBar()->setEnabled(false);
|
||||||
|
scrollArea->setWidget(widget);
|
||||||
|
return scrollArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
|
||||||
|
*/
|
||||||
|
static QStringList pageIconResources()
|
||||||
|
{
|
||||||
|
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
|
||||||
|
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
|
||||||
|
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
|
||||||
|
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
|
||||||
|
{
|
||||||
|
setupUi();
|
||||||
|
|
||||||
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi);
|
||||||
|
retranslateUi();
|
||||||
|
|
||||||
|
searchEdit->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setupUi()
|
||||||
|
{
|
||||||
|
// Search bar
|
||||||
|
searchEdit = new QLineEdit;
|
||||||
|
searchEdit->setClearButtonEnabled(true);
|
||||||
|
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||||
|
searchEdit->installEventFilter(this);
|
||||||
|
connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged);
|
||||||
|
|
||||||
|
auto *searchLayout = new QHBoxLayout;
|
||||||
|
searchLayout->addWidget(searchEdit);
|
||||||
|
|
||||||
|
// Tab bar (built in setupTabBar)
|
||||||
|
setupTabBar();
|
||||||
|
|
||||||
|
// Pages stacked widget
|
||||||
|
pagesWidget = new QStackedWidget;
|
||||||
|
|
||||||
|
auto *generalPage = new GeneralSettingsPage;
|
||||||
|
auto *appearancePage = new AppearanceSettingsPage;
|
||||||
|
auto *userInterfacePage = new UserInterfaceSettingsPage;
|
||||||
|
auto *deckEditorPage = new DeckEditorSettingsPage;
|
||||||
|
auto *storagePage = new StorageSettingsPage;
|
||||||
|
auto *messagesPage = new MessagesSettingsPage;
|
||||||
|
auto *soundPage = new SoundSettingsPage;
|
||||||
|
auto *shortcutsPage = new ShortcutSettingsPage;
|
||||||
|
|
||||||
|
pages.append(generalPage);
|
||||||
|
pages.append(appearancePage);
|
||||||
|
pages.append(userInterfacePage);
|
||||||
|
pages.append(deckEditorPage);
|
||||||
|
pages.append(storagePage);
|
||||||
|
pages.append(messagesPage);
|
||||||
|
pages.append(soundPage);
|
||||||
|
pages.append(shortcutsPage);
|
||||||
|
|
||||||
|
pagesWidget->addWidget(makeScrollable(generalPage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(appearancePage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(storagePage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(messagesPage));
|
||||||
|
pagesWidget->addWidget(soundPage);
|
||||||
|
pagesWidget->addWidget(shortcutsPage);
|
||||||
|
|
||||||
|
Q_ASSERT(pages.size() == NumPages);
|
||||||
|
|
||||||
|
// Search results view (hidden by default)
|
||||||
|
searchResultsView = new QListView;
|
||||||
|
searchResultsView->setUniformItemSizes(false);
|
||||||
|
searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
searchResultsView->setVisible(false);
|
||||||
|
searchResultsView->setStyleSheet(
|
||||||
|
"QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
|
||||||
|
|
||||||
|
searchModel = new SettingsSearchModel(this);
|
||||||
|
searchDelegate = new SettingsSearchDelegate(this);
|
||||||
|
searchResultsView->setModel(searchModel);
|
||||||
|
searchResultsView->setItemDelegate(searchDelegate);
|
||||||
|
connect(searchResultsView, &QListView::clicked, this, &SettingsView::onSearchResultClicked);
|
||||||
|
|
||||||
|
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]));
|
||||||
|
}
|
||||||
|
searchDelegate->setPageIcons(icons);
|
||||||
|
searchResultsView->viewport()->update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build search index after pages are created
|
||||||
|
buildSearchIndex();
|
||||||
|
|
||||||
|
// Pages container (stacked widget + search results overlay)
|
||||||
|
pagesContainer = new QWidget;
|
||||||
|
auto *containerLayout = new QStackedLayout;
|
||||||
|
containerLayout->setStackingMode(QStackedLayout::StackAll);
|
||||||
|
containerLayout->addWidget(pagesWidget);
|
||||||
|
containerLayout->addWidget(searchResultsView);
|
||||||
|
pagesContainer->setLayout(containerLayout);
|
||||||
|
|
||||||
|
// Main layout
|
||||||
|
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);
|
||||||
|
setLayout(mainLayout);
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this);
|
||||||
|
connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus));
|
||||||
|
|
||||||
|
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
|
||||||
|
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
|
||||||
|
int next = (currentTabIndex + 1) % tabButtons.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();
|
||||||
|
setActiveTab(prev);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize to first tab
|
||||||
|
setActiveTab(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setupTabBar()
|
||||||
|
{
|
||||||
|
tabBarWidget = new QWidget;
|
||||||
|
auto *tabLayout = new QHBoxLayout;
|
||||||
|
tabLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
tabLayout->setSpacing(2);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabBarWidget->setLayout(tabLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::buildSearchIndex()
|
||||||
|
{
|
||||||
|
QList<SettingsSearchEntry> allEntries;
|
||||||
|
|
||||||
|
const QStringList pageNames = translatedPageNames();
|
||||||
|
searchDelegate->setPageNames(pageNames);
|
||||||
|
searchDelegate->setPageIcons(pageIconResources());
|
||||||
|
|
||||||
|
for (int i = 0; i < pages.size(); ++i) {
|
||||||
|
QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries();
|
||||||
|
for (auto &entry : pageEntries) {
|
||||||
|
if (entry.pageIndex == -1) {
|
||||||
|
entry.pageIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allEntries.append(pageEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchModel->setSourceEntries(allEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::onTabClicked(int index)
|
||||||
|
{
|
||||||
|
if (searchActive) {
|
||||||
|
switchToTabMode();
|
||||||
|
}
|
||||||
|
setActiveTab(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setActiveTab(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= tabButtons.size()) {
|
||||||
|
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; }");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::flashWidget(QWidget *widget)
|
||||||
|
{
|
||||||
|
auto *overlay = new QWidget(widget);
|
||||||
|
overlay->setGeometry(widget->rect());
|
||||||
|
overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
|
||||||
|
QPalette pal = overlay->palette();
|
||||||
|
QColor flashColor = pal.color(QPalette::Highlight);
|
||||||
|
flashColor.setAlpha(100);
|
||||||
|
pal.setBrush(QPalette::Window, flashColor);
|
||||||
|
overlay->setPalette(pal);
|
||||||
|
overlay->setAutoFillBackground(true);
|
||||||
|
|
||||||
|
auto *effect = new QGraphicsOpacityEffect(overlay);
|
||||||
|
effect->setOpacity(0.0);
|
||||||
|
overlay->setGraphicsEffect(effect);
|
||||||
|
overlay->show();
|
||||||
|
overlay->raise();
|
||||||
|
|
||||||
|
auto *flashIn = new QPropertyAnimation(effect, "opacity");
|
||||||
|
flashIn->setDuration(120);
|
||||||
|
flashIn->setStartValue(0.0);
|
||||||
|
flashIn->setEndValue(0.6);
|
||||||
|
flashIn->setEasingCurve(QEasingCurve::OutCubic);
|
||||||
|
|
||||||
|
auto *fadeOut = new QPropertyAnimation(effect, "opacity");
|
||||||
|
fadeOut->setDuration(900);
|
||||||
|
fadeOut->setStartValue(0.6);
|
||||||
|
fadeOut->setEndValue(0.0);
|
||||||
|
fadeOut->setEasingCurve(QEasingCurve::InCubic);
|
||||||
|
|
||||||
|
auto *group = new QSequentialAnimationGroup(overlay);
|
||||||
|
group->addAnimation(flashIn);
|
||||||
|
group->addAnimation(fadeOut);
|
||||||
|
|
||||||
|
connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater);
|
||||||
|
|
||||||
|
group->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::onSearchTextChanged(const QString &text)
|
||||||
|
{
|
||||||
|
searchModel->setFilterString(text);
|
||||||
|
|
||||||
|
if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) {
|
||||||
|
if (!searchActive) {
|
||||||
|
switchToSearchMode();
|
||||||
|
}
|
||||||
|
if (searchModel->rowCount(QModelIndex()) > 0) {
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(0));
|
||||||
|
}
|
||||||
|
} else if (searchActive) {
|
||||||
|
switchToTabMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::switchToSearchMode()
|
||||||
|
{
|
||||||
|
searchActive = true;
|
||||||
|
tabBarWidget->setVisible(false);
|
||||||
|
pagesWidget->setVisible(false);
|
||||||
|
searchResultsView->setVisible(true);
|
||||||
|
if (searchModel->rowCount(QModelIndex()) > 0) {
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::switchToTabMode()
|
||||||
|
{
|
||||||
|
searchActive = false;
|
||||||
|
tabBarWidget->setVisible(true);
|
||||||
|
pagesWidget->setVisible(true);
|
||||||
|
searchResultsView->setVisible(false);
|
||||||
|
searchEdit->blockSignals(true);
|
||||||
|
searchEdit->clear();
|
||||||
|
searchEdit->blockSignals(false);
|
||||||
|
setActiveTab(currentTabIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::onSearchResultClicked(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
navigateToSearchResult(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::navigateToSearchResult(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
SettingsSearchEntry entry = searchModel->entryForIndex(index);
|
||||||
|
if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the page
|
||||||
|
switchToTabMode();
|
||||||
|
setActiveTab(entry.pageIndex);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
entry.widget->setFocus();
|
||||||
|
flashWidget(entry.widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setTab(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < tabButtons.size()) {
|
||||||
|
setActiveTab(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractSettingsPage *SettingsView::page(SettingsPage which) const
|
||||||
|
{
|
||||||
|
return pages.value(static_cast<int>(which));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsView::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
if (keyEvent->key() == Qt::Key_Escape) {
|
||||||
|
if (searchActive) {
|
||||||
|
switchToTabMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
|
||||||
|
if (searchActive) {
|
||||||
|
if (searchResultsView->currentIndex().isValid()) {
|
||||||
|
navigateToSearchResult(searchResultsView->currentIndex());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (keyEvent->key() == Qt::Key_Down) {
|
||||||
|
if (searchActive) {
|
||||||
|
int nextRow = searchResultsView->currentIndex().row() + 1;
|
||||||
|
if (nextRow >= searchModel->rowCount()) {
|
||||||
|
nextRow = 0;
|
||||||
|
}
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(nextRow));
|
||||||
|
searchResultsView->scrollTo(searchModel->index(nextRow));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (keyEvent->key() == Qt::Key_Up) {
|
||||||
|
if (searchActive) {
|
||||||
|
int prevRow = searchResultsView->currentIndex().row() - 1;
|
||||||
|
if (prevRow < 0) {
|
||||||
|
prevRow = searchModel->rowCount() - 1;
|
||||||
|
}
|
||||||
|
if (prevRow >= 0) {
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(prevRow));
|
||||||
|
searchResultsView->scrollTo(searchModel->index(prevRow));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::retranslateUi()
|
||||||
|
{
|
||||||
|
retranslateTabNames();
|
||||||
|
|
||||||
|
// NOTE: the "DlgSettings" context is deliberate — these strings lived in DlgSettings
|
||||||
|
// before the view was extracted, and keeping the context preserves existing translations.
|
||||||
|
searchEdit->setPlaceholderText(QCoreApplication::translate("DlgSettings", "Search settings..."));
|
||||||
|
|
||||||
|
// Rebuild search index for translated text
|
||||||
|
buildSearchIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList SettingsView::translatedPageNames()
|
||||||
|
{
|
||||||
|
// NOTE: same "DlgSettings" context as above (translation continuity, see retranslateUi).
|
||||||
|
return {QCoreApplication::translate("DlgSettings", "General"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Appearance"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "User Interface"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Card Sources"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Storage"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Chat"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Sound"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Shortcuts")};
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::retranslateTabNames()
|
||||||
|
{
|
||||||
|
const QStringList tabLabels = translatedPageNames();
|
||||||
|
|
||||||
|
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
|
||||||
|
tabButtons[i]->setText(tabLabels[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
cockatrice/src/interface/widgets/dialogs/settings_view.h
Normal file
98
cockatrice/src/interface/widgets/dialogs/settings_view.h
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* @file settings_view.h
|
||||||
|
* @brief Settings content widget hosted by the settings dialog (and, later, the settings tab)
|
||||||
|
* @ingroup Dialogs
|
||||||
|
*/
|
||||||
|
#ifndef SETTINGS_VIEW_H
|
||||||
|
#define SETTINGS_VIEW_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
class QModelIndex;
|
||||||
|
class QObject;
|
||||||
|
class QEvent;
|
||||||
|
|
||||||
|
class QStackedWidget;
|
||||||
|
class QToolButton;
|
||||||
|
class QListView;
|
||||||
|
class QLineEdit;
|
||||||
|
class AbstractSettingsPage;
|
||||||
|
class SettingsSearchModel;
|
||||||
|
class SettingsSearchDelegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Settings pages with tabbed navigation and search, without any dialog chrome
|
||||||
|
*
|
||||||
|
* Owns the search bar, the tab bar, the stacked settings pages and the search
|
||||||
|
* results view. Dialog/tab hosts only add their own frame (buttons, titles).
|
||||||
|
*/
|
||||||
|
class SettingsView : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Page order in the tab bar, 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.
|
||||||
|
*/
|
||||||
|
enum SettingsPage
|
||||||
|
{
|
||||||
|
GeneralPage = 0,
|
||||||
|
AppearancePage,
|
||||||
|
UserInterfacePage,
|
||||||
|
DeckEditorPage,
|
||||||
|
StoragePage,
|
||||||
|
MessagesPage,
|
||||||
|
SoundPage,
|
||||||
|
ShortcutsPage,
|
||||||
|
NumPages
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit SettingsView(QWidget *parent = nullptr);
|
||||||
|
void setTab(int index);
|
||||||
|
AbstractSettingsPage *page(SettingsPage which) const;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onTabClicked(int index);
|
||||||
|
void onSearchTextChanged(const QString &text);
|
||||||
|
void onSearchResultClicked(const QModelIndex &index);
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Data
|
||||||
|
QList<AbstractSettingsPage *> pages; ///< All settings page instances
|
||||||
|
SettingsSearchModel *searchModel; ///< Model for search results
|
||||||
|
SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering
|
||||||
|
int currentTabIndex; ///< Currently active tab index
|
||||||
|
bool searchActive; ///< Whether search mode is active
|
||||||
|
|
||||||
|
void setupUi();
|
||||||
|
void setupTabBar();
|
||||||
|
void buildSearchIndex();
|
||||||
|
void switchToTabMode();
|
||||||
|
void switchToSearchMode();
|
||||||
|
void navigateToSearchResult(const QModelIndex &index);
|
||||||
|
void setActiveTab(int index);
|
||||||
|
static void flashWidget(QWidget *widget);
|
||||||
|
static QStringList translatedPageNames();
|
||||||
|
|
||||||
|
void retranslateUi();
|
||||||
|
void retranslateTabNames();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SETTINGS_VIEW_H
|
||||||
Loading…
Add table
Add a link
Reference in a new issue