mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-08-05 18:22:20 -05:00
Merge 2e6df2ea94 into c7ab75c332
This commit is contained in:
commit
29eecb8f24
|
|
@ -10,8 +10,10 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
|||
ninja \
|
||||
protobuf \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
qt6-imageformats \
|
||||
qt6-multimedia \
|
||||
qt6-shadertools \
|
||||
qt6-svg \
|
||||
qt6-tools \
|
||||
qt6-translations \
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ RUN apt-get update && \
|
|||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ RUN apt-get update && \
|
|||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ RUN dnf install -y \
|
|||
mariadb-devel \
|
||||
ninja-build \
|
||||
protobuf-devel \
|
||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
rpm-build \
|
||||
xz-devel \
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ RUN dnf install -y \
|
|||
mariadb-devel \
|
||||
ninja-build \
|
||||
protobuf-devel \
|
||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
rpm-build \
|
||||
xz-devel \
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ RUN apt-get update && \
|
|||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ RUN apt-get update && \
|
|||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-declarative-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
|
|
|
|||
14
.github/workflows/desktop-build.yml
vendored
14
.github/workflows/desktop-build.yml
vendored
|
|
@ -271,7 +271,7 @@ jobs:
|
|||
override_target: 13
|
||||
package_suffix: "-macOS13_Intel"
|
||||
qt_version: 6.11.0
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Intel
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
|
|
@ -286,7 +286,7 @@ jobs:
|
|||
make_package: 1
|
||||
package_suffix: "-macOS14"
|
||||
qt_version: 6.11.0
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
|
|
@ -301,7 +301,7 @@ jobs:
|
|||
make_package: 1
|
||||
package_suffix: "-macOS15"
|
||||
qt_version: 6.11.0
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
|
|
@ -314,7 +314,7 @@ jobs:
|
|||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
qt_version: 6.11.0
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Debug
|
||||
use_ccache: 1
|
||||
|
|
@ -329,7 +329,7 @@ jobs:
|
|||
make_package: 1
|
||||
package_suffix: "-Win10"
|
||||
qt_version: 6.11.0
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
type: Release
|
||||
|
||||
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||
|
|
@ -384,7 +384,7 @@ jobs:
|
|||
id: restore_qt
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}-${{ matrix.qt_modules }}
|
||||
path: ${{ github.workspace }}/Qt
|
||||
|
||||
# Using jurplel/install-qt-action to install Qt without using brew
|
||||
|
|
@ -406,7 +406,7 @@ jobs:
|
|||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}-${{ matrix.qt_modules }}
|
||||
path: ${{ github.workspace }}/Qt
|
||||
|
||||
- name: "[Windows] Install Qt ${{ matrix.qt_version }}"
|
||||
|
|
|
|||
|
|
@ -19,10 +19,13 @@ if(WITH_CLIENT)
|
|||
Multimedia
|
||||
Network
|
||||
PrintSupport
|
||||
ShaderTools
|
||||
Svg
|
||||
WebSockets
|
||||
Widgets
|
||||
Xml
|
||||
Quick
|
||||
QuickWidgets
|
||||
)
|
||||
endif()
|
||||
if(WITH_ORACLE)
|
||||
|
|
|
|||
|
|
@ -354,14 +354,35 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||
src/interface/key_signals.cpp
|
||||
src/interface/logger.cpp
|
||||
src/interface/widgets/onboarding/banner_shader_config.h
|
||||
src/interface/widgets/onboarding/first_run_wizard.cpp
|
||||
src/interface/widgets/onboarding/first_run_wizard.h
|
||||
src/interface/widgets/onboarding/first_run_wizard_page.cpp
|
||||
src/interface/widgets/onboarding/first_run_wizard_page.h
|
||||
src/interface/widgets/onboarding/pages/account_setup_page.cpp
|
||||
src/interface/widgets/onboarding/pages/account_setup_page.h
|
||||
src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
|
||||
src/interface/widgets/onboarding/pages/card_database_setup_page.h
|
||||
src/interface/widgets/onboarding/pages/finish_page.cpp
|
||||
src/interface/widgets/onboarding/pages/finish_page.h
|
||||
src/interface/widgets/onboarding/pages/preferences_setup_page.cpp
|
||||
src/interface/widgets/onboarding/pages/preferences_setup_page.h
|
||||
src/interface/widgets/onboarding/pages/theme_setup_page.cpp
|
||||
src/interface/widgets/onboarding/pages/theme_setup_page.h
|
||||
src/interface/widgets/onboarding/pages/welcome_page.cpp
|
||||
src/interface/widgets/onboarding/pages/welcome_page.h
|
||||
src/interface/widgets/onboarding/shader_banner_widget.cpp
|
||||
src/interface/widgets/onboarding/shader_banner_widget.h
|
||||
src/interface/widgets/onboarding/step_indicator_widget.cpp
|
||||
src/interface/widgets/onboarding/step_indicator_widget.h
|
||||
src/interface/widgets/server/user/user_info_popup.cpp
|
||||
src/interface/widgets/server/user/user_info_popup.h
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||
src/interface/widgets/utility/compact_push_button.cpp
|
||||
src/interface/widgets/utility/compact_push_button.h
|
||||
src/interface/widgets/server/user/user_info_popup.cpp
|
||||
src/interface/widgets/server/user/user_info_popup.h
|
||||
)
|
||||
|
||||
add_subdirectory(sounds)
|
||||
|
|
@ -433,6 +454,27 @@ if(Qt6_FOUND)
|
|||
${cockatrice_MOC_SRCS}
|
||||
MANUAL_FINALIZATION
|
||||
)
|
||||
qt6_add_shaders(
|
||||
cockatrice
|
||||
"onboarding_shaders"
|
||||
PREFIX
|
||||
"/onboarding/shaders"
|
||||
BASE
|
||||
"src/interface/widgets/onboarding/shaders"
|
||||
FILES
|
||||
src/interface/widgets/onboarding/shaders/brand_banner.frag
|
||||
)
|
||||
|
||||
qt6_add_resources(
|
||||
cockatrice
|
||||
"onboarding_qml"
|
||||
PREFIX
|
||||
"/onboarding/qml"
|
||||
BASE
|
||||
"src/interface/widgets/onboarding/qml"
|
||||
FILES
|
||||
src/interface/widgets/onboarding/qml/BrandBanner.qml
|
||||
)
|
||||
elseif(Qt5_FOUND)
|
||||
# Qt5 Translations need to be linked at executable creation time
|
||||
if(Qt5LinguistTools_FOUND)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<qresource prefix="/">
|
||||
<file>resources/cardback.svg</file>
|
||||
<file>resources/cockatrice.svg</file>
|
||||
<file>resources/cockatrice-logo-white.svg</file>
|
||||
<file>resources/hand.svg</file>
|
||||
<file>resources/hr.jpg</file>
|
||||
|
||||
|
|
|
|||
21
cockatrice/resources/cockatrice-logo-white.svg
Normal file
21
cockatrice/resources/cockatrice-logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.8 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 51 KiB |
|
|
@ -290,28 +290,42 @@ void PaletteEditorDialog::onSave()
|
|||
// Persist every scheme that changed, not just the one on screen. Each scheme
|
||||
// has its own file, so edits to the non-active scheme would otherwise be
|
||||
// silently discarded when the dialog closes.
|
||||
//
|
||||
// Save the loaded scheme last so commitPalette's global colour-scheme
|
||||
// update (ThemeConfig::colorScheme) points at the active scheme.
|
||||
for (auto it = workingConfig.begin(); it != workingConfig.end(); ++it) {
|
||||
const QString &scheme = it.key();
|
||||
if (it.value().colors == savedConfig.value(scheme).colors) {
|
||||
continue; // unchanged — leave the on-disk file alone
|
||||
if (it.key() == loadedScheme) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ThemeManager::savePaletteConfig(saveDir, scheme, it.value())) {
|
||||
if (it.value().colors == savedConfig.value(it.key()).colors) {
|
||||
continue;
|
||||
}
|
||||
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
|
||||
QMessageBox::warning(this, tr("Save failed"),
|
||||
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(scheme), saveDir));
|
||||
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(it.key()), saveDir));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the active scheme last so the global colour scheme matches.
|
||||
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
|
||||
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
|
||||
QMessageBox::warning(this, tr("Save failed"),
|
||||
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No palette change but scheme may have switched -- still update global config.
|
||||
ThemeConfig globalCfg = ThemeConfig::fromThemeDir(saveDir);
|
||||
globalCfg.colorScheme = loadedScheme;
|
||||
globalCfg.save(saveDir);
|
||||
}
|
||||
|
||||
// Keep the saved snapshot in sync so Reset behaves correctly afterwards.
|
||||
for (auto it = workingConfig.begin(); it != workingConfig.end(); ++it) {
|
||||
savedConfig[it.key()] = it.value();
|
||||
}
|
||||
|
||||
ThemeConfig globalCfg = ThemeConfig::fromThemeDir(saveDir);
|
||||
globalCfg.colorScheme = loadedScheme;
|
||||
globalCfg.save(saveDir);
|
||||
|
||||
themeManager->reloadCurrentTheme();
|
||||
accept();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,6 +268,19 @@ PaletteConfig ThemeManager::loadDefaultPaletteConfig(const QString &themeDirPath
|
|||
return cfg;
|
||||
}
|
||||
|
||||
bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg)
|
||||
{
|
||||
if (!savePaletteConfig(themeDirPath, colorScheme, cfg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath);
|
||||
globalCfg.colorScheme = colorScheme;
|
||||
globalCfg.save(themeDirPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ThemeManager::setColorScheme(const QString &scheme)
|
||||
{
|
||||
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ public:
|
|||
// theme directory when it is absent from the resolved (user) directory.
|
||||
static PaletteConfig
|
||||
loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme);
|
||||
/** @brief Writes cfg to disk as the theme's palette-<scheme>.toml and updates the
|
||||
* theme's stored colour scheme to match. Shared by PaletteEditorDialog::onSave
|
||||
* and FirstRunWizard's theme step so the two "generate + keep" paths can't drift. */
|
||||
static bool commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
||||
void setColorScheme(const QString &scheme);
|
||||
void setStyleName(const QString &styleName);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,62 @@
|
|||
#include "dlg_register.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../server/handle_public_servers.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
// ── Server picker ──────────────────────────────────────────────────
|
||||
previousHostButton = new QRadioButton(tr("Known Hosts"), this);
|
||||
previousHosts = new QComboBox(this);
|
||||
|
||||
btnDeleteServer = new QPushButton(this);
|
||||
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||
btnDeleteServer->setFixedWidth(30);
|
||||
|
||||
connect(btnDeleteServer, &QPushButton::clicked, this, &DlgRegister::actRemoveSavedServer);
|
||||
|
||||
hps = new HandlePublicServers(this);
|
||||
btnRefreshServers = new QPushButton(this);
|
||||
btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
|
||||
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
||||
btnRefreshServers->setFixedWidth(30);
|
||||
|
||||
connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, this, [this] { rebuildComboBoxList(); });
|
||||
connect(hps, &HandlePublicServers::sigPublicServersDownloadedUnsuccessfully, this,
|
||||
&DlgRegister::rebuildComboBoxList);
|
||||
connect(btnRefreshServers, &QPushButton::released, this, &DlgRegister::downloadThePublicServers);
|
||||
|
||||
newHostButton = new QRadioButton(tr("New Host"), this);
|
||||
|
||||
auto *serverPickerRow = new QHBoxLayout;
|
||||
serverPickerRow->addWidget(previousHosts);
|
||||
serverPickerRow->addWidget(btnDeleteServer);
|
||||
serverPickerRow->addWidget(btnRefreshServers);
|
||||
|
||||
auto *serverGroupLayout = new QVBoxLayout;
|
||||
serverGroupLayout->addWidget(previousHostButton);
|
||||
serverGroupLayout->addLayout(serverPickerRow);
|
||||
serverGroupLayout->addWidget(newHostButton);
|
||||
|
||||
auto *serverGroupBox = new QGroupBox(tr("Server"));
|
||||
serverGroupBox->setLayout(serverGroupLayout);
|
||||
|
||||
// ── Registration fields ────────────────────────────────────────────
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
infoLabel = new QLabel(tr("Enter your information and the information of the server you'd like to register to.\n"
|
||||
"Your email will be used to verify your account."));
|
||||
|
|
@ -321,26 +365,28 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
|||
realnameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
realnameLabel->setBuddy(realnameEdit);
|
||||
|
||||
// ── Layout ─────────────────────────────────────────────────────────
|
||||
auto *grid = new QGridLayout;
|
||||
grid->addWidget(infoLabel, 0, 0, 1, 2);
|
||||
grid->addWidget(hostLabel, 1, 0);
|
||||
grid->addWidget(hostEdit, 1, 1);
|
||||
grid->addWidget(portLabel, 2, 0);
|
||||
grid->addWidget(portEdit, 2, 1);
|
||||
grid->addWidget(playernameLabel, 3, 0);
|
||||
grid->addWidget(playernameEdit, 3, 1);
|
||||
grid->addWidget(passwordLabel, 4, 0);
|
||||
grid->addWidget(passwordEdit, 4, 1);
|
||||
grid->addWidget(passwordConfirmationLabel, 5, 0);
|
||||
grid->addWidget(passwordConfirmationEdit, 5, 1);
|
||||
grid->addWidget(emailLabel, 6, 0);
|
||||
grid->addWidget(emailEdit, 6, 1);
|
||||
grid->addWidget(emailConfirmationLabel, 7, 0);
|
||||
grid->addWidget(emailConfirmationEdit, 7, 1);
|
||||
grid->addWidget(countryLabel, 9, 0);
|
||||
grid->addWidget(countryEdit, 9, 1);
|
||||
grid->addWidget(realnameLabel, 10, 0);
|
||||
grid->addWidget(realnameEdit, 10, 1);
|
||||
grid->addWidget(serverGroupBox, 0, 0, 1, 2);
|
||||
grid->addWidget(infoLabel, 1, 0, 1, 2);
|
||||
grid->addWidget(hostLabel, 2, 0);
|
||||
grid->addWidget(hostEdit, 2, 1);
|
||||
grid->addWidget(portLabel, 3, 0);
|
||||
grid->addWidget(portEdit, 3, 1);
|
||||
grid->addWidget(playernameLabel, 4, 0);
|
||||
grid->addWidget(playernameEdit, 4, 1);
|
||||
grid->addWidget(passwordLabel, 5, 0);
|
||||
grid->addWidget(passwordEdit, 5, 1);
|
||||
grid->addWidget(passwordConfirmationLabel, 6, 0);
|
||||
grid->addWidget(passwordConfirmationEdit, 6, 1);
|
||||
grid->addWidget(emailLabel, 7, 0);
|
||||
grid->addWidget(emailEdit, 7, 1);
|
||||
grid->addWidget(emailConfirmationLabel, 8, 0);
|
||||
grid->addWidget(emailConfirmationEdit, 8, 1);
|
||||
grid->addWidget(countryLabel, 10, 0);
|
||||
grid->addWidget(countryEdit, 10, 1);
|
||||
grid->addWidget(realnameLabel, 11, 0);
|
||||
grid->addWidget(realnameEdit, 11, 1);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgRegister::actOk);
|
||||
|
|
@ -352,13 +398,115 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
|||
setLayout(mainLayout);
|
||||
|
||||
setWindowTitle(tr("Register to server"));
|
||||
setFixedHeight(sizeHint().height());
|
||||
setMinimumWidth(300);
|
||||
setMinimumWidth(360);
|
||||
|
||||
connect(previousHostButton, &QRadioButton::toggled, this, &DlgRegister::previousHostSelected);
|
||||
connect(newHostButton, &QRadioButton::toggled, this, &DlgRegister::newHostSelected);
|
||||
connect(previousHosts, &QComboBox::currentTextChanged, this, &DlgRegister::updateDisplayInfo);
|
||||
|
||||
previousHostButton->setChecked(true);
|
||||
|
||||
preRebuildComboBoxList();
|
||||
}
|
||||
|
||||
DlgRegister::~DlgRegister() = default;
|
||||
|
||||
void DlgRegister::downloadThePublicServers()
|
||||
{
|
||||
btnRefreshServers->setDisabled(true);
|
||||
previousHosts->clear();
|
||||
previousHosts->addItem(placeHolderText);
|
||||
hps->downloadPublicServers();
|
||||
}
|
||||
|
||||
void DlgRegister::preRebuildComboBoxList()
|
||||
{
|
||||
UserConnection_Information uci;
|
||||
savedHostList = uci.getServerInfo();
|
||||
|
||||
if (savedHostList.size() == 1) {
|
||||
downloadThePublicServers();
|
||||
} else {
|
||||
rebuildComboBoxList();
|
||||
}
|
||||
}
|
||||
|
||||
void DlgRegister::rebuildComboBoxList(int failure)
|
||||
{
|
||||
Q_UNUSED(failure);
|
||||
|
||||
previousHosts->clear();
|
||||
|
||||
UserConnection_Information uci;
|
||||
savedHostList = uci.getServerInfo();
|
||||
|
||||
auto &servers = SettingsCache::instance().servers();
|
||||
QString previousHostName = servers.getPrevioushostName();
|
||||
|
||||
for (const auto &pair : savedHostList) {
|
||||
const auto &tmp = pair.second;
|
||||
QString saveName = tmp.getSaveName();
|
||||
if (saveName.size()) {
|
||||
previousHosts->addItem(saveName);
|
||||
if (saveName.compare(previousHostName) == 0) {
|
||||
previousHosts->setCurrentIndex(previousHosts->count() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
btnRefreshServers->setDisabled(false);
|
||||
}
|
||||
|
||||
void DlgRegister::previousHostSelected(bool state)
|
||||
{
|
||||
if (state) {
|
||||
previousHosts->setDisabled(false);
|
||||
btnRefreshServers->setDisabled(false);
|
||||
hostEdit->setDisabled(true);
|
||||
portEdit->setDisabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DlgRegister::newHostSelected(bool state)
|
||||
{
|
||||
if (state) {
|
||||
previousHosts->setDisabled(true);
|
||||
btnRefreshServers->setDisabled(true);
|
||||
hostEdit->setDisabled(false);
|
||||
hostEdit->clear();
|
||||
hostEdit->setPlaceholderText(tr("Server URL"));
|
||||
portEdit->setDisabled(false);
|
||||
portEdit->clear();
|
||||
portEdit->setPlaceholderText(tr("Communication Port"));
|
||||
playernameEdit->setDisabled(false);
|
||||
playernameEdit->clear();
|
||||
} else {
|
||||
// Rebuild the list so the previously selected host's details are
|
||||
// repopulated (mirrors DlgConnect::newHostSelected).
|
||||
preRebuildComboBoxList();
|
||||
}
|
||||
}
|
||||
|
||||
void DlgRegister::updateDisplayInfo(const QString &saveName)
|
||||
{
|
||||
if (saveName.isEmpty() || saveName == placeHolderText) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserConnection_Information uci;
|
||||
QStringList _data = uci.getServerInfo(saveName);
|
||||
|
||||
if (_data.size() < 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
hostEdit->setText(_data.at(1));
|
||||
portEdit->setText(_data.at(2));
|
||||
playernameEdit->setText(_data.at(3));
|
||||
}
|
||||
|
||||
void DlgRegister::actOk()
|
||||
{
|
||||
//! \todo This stuff should be using QValidators.
|
||||
if (passwordEdit->text().length() < 8) {
|
||||
QMessageBox::critical(this, tr("Registration Warning"), tr("Your password is too short."));
|
||||
return;
|
||||
|
|
@ -375,5 +523,29 @@ void DlgRegister::actOk()
|
|||
return;
|
||||
}
|
||||
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
|
||||
if (newHostButton->isChecked()) {
|
||||
// Persist the new host so it shows up in the Connect dialog later.
|
||||
// The password is never stored: the account is not verified yet.
|
||||
const QString host = hostEdit->text().trimmed();
|
||||
if (!host.isEmpty()) {
|
||||
servers.addNewServer(host, host, portEdit->text().trimmed(), playernameEdit->text().trimmed(), QString(),
|
||||
false);
|
||||
servers.setPrevioushostName(host);
|
||||
}
|
||||
} else {
|
||||
const QString saveName = previousHosts->currentText();
|
||||
if (!saveName.isEmpty() && saveName != placeHolderText) {
|
||||
servers.setPrevioushostName(saveName);
|
||||
}
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgRegister::actRemoveSavedServer()
|
||||
{
|
||||
SettingsCache::instance().servers().removeServer(hostEdit->text());
|
||||
previousHosts->removeItem(previousHosts->currentIndex());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
/**
|
||||
* @file dlg_register.h
|
||||
* @ingroup AccountDialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_REGISTER_H
|
||||
#define DLG_REGISTER_H
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
|
||||
class HandlePublicServers;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QRadioButton;
|
||||
class UserConnection_Information;
|
||||
|
||||
class DlgRegister : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgRegister(QWidget *parent = nullptr);
|
||||
~DlgRegister() override;
|
||||
|
||||
[[nodiscard]] QString getHost() const
|
||||
{
|
||||
return hostEdit->text();
|
||||
|
|
@ -48,15 +47,35 @@ public:
|
|||
{
|
||||
return realnameEdit->text();
|
||||
}
|
||||
|
||||
public slots:
|
||||
void downloadThePublicServers();
|
||||
|
||||
private slots:
|
||||
void actOk();
|
||||
void previousHostSelected(bool state);
|
||||
void newHostSelected(bool state);
|
||||
void updateDisplayInfo(const QString &saveName);
|
||||
void preRebuildComboBoxList();
|
||||
void rebuildComboBoxList(int failure = -1);
|
||||
void actRemoveSavedServer();
|
||||
|
||||
private:
|
||||
QRadioButton *newHostButton;
|
||||
QRadioButton *previousHostButton;
|
||||
QComboBox *previousHosts;
|
||||
QPushButton *btnDeleteServer;
|
||||
QPushButton *btnRefreshServers;
|
||||
HandlePublicServers *hps;
|
||||
|
||||
QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel, *passwordLabel, *passwordConfirmationLabel,
|
||||
*emailLabel, *emailConfirmationLabel, *countryLabel, *realnameLabel;
|
||||
QLineEdit *hostEdit, *portEdit, *playernameEdit, *passwordEdit, *passwordConfirmationEdit, *emailEdit,
|
||||
*emailConfirmationEdit, *realnameEdit;
|
||||
QComboBox *countryEdit;
|
||||
|
||||
QMap<QString, std::pair<QString, UserConnection_Information>> savedHostList;
|
||||
const QString placeHolderText = QStringLiteral("Downloading...");
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif // DLG_REGISTER_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
#ifndef BANNER_SHADER_CONFIG_H
|
||||
#define BANNER_SHADER_CONFIG_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
|
||||
/**
|
||||
* Uniform values fed to brand_banner.frag, exposed to QML as the
|
||||
* "bannerConfig" context property.
|
||||
*
|
||||
* Two independent "banks" (A/B) each carry their own mode/speed/seed so
|
||||
* BrandBanner.qml can render both simultaneously and crossfade between
|
||||
* them via opacity -- see frontIsA. The shared palette (colorA/colorB/
|
||||
* accent) and clock (time/aspect) apply to both banks identically, since
|
||||
* only the foreground motif changes between onboarding pages, never the
|
||||
* brand palette.
|
||||
*
|
||||
* Deliberately plain `property` (not `required property`) on the QML side
|
||||
* -- a required-property shadowing bug bit the home-screen particle
|
||||
* background before, and there's no reason to reintroduce that risk here.
|
||||
*/
|
||||
class BannerShaderConfig : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(qreal time READ time WRITE setTime NOTIFY timeChanged)
|
||||
Q_PROPERTY(qreal aspect READ aspect WRITE setAspect NOTIFY aspectChanged)
|
||||
|
||||
Q_PROPERTY(qreal modeA READ modeA WRITE setModeA NOTIFY modeAChanged)
|
||||
Q_PROPERTY(qreal speedA READ speedA WRITE setSpeedA NOTIFY speedAChanged)
|
||||
Q_PROPERTY(qreal seedA READ seedA WRITE setSeedA NOTIFY seedAChanged)
|
||||
|
||||
Q_PROPERTY(qreal modeB READ modeB WRITE setModeB NOTIFY modeBChanged)
|
||||
Q_PROPERTY(qreal speedB READ speedB WRITE setSpeedB NOTIFY speedBChanged)
|
||||
Q_PROPERTY(qreal seedB READ seedB WRITE setSeedB NOTIFY seedBChanged)
|
||||
|
||||
Q_PROPERTY(bool frontIsA READ frontIsA WRITE setFrontIsA NOTIFY frontIsAChanged)
|
||||
|
||||
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
|
||||
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
|
||||
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
|
||||
|
||||
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
|
||||
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
|
||||
|
||||
public:
|
||||
explicit BannerShaderConfig(QObject *parent = nullptr) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal time() const
|
||||
{
|
||||
return m_time;
|
||||
}
|
||||
void setTime(qreal v)
|
||||
{
|
||||
if (v != m_time) {
|
||||
m_time = v;
|
||||
emit timeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal aspect() const
|
||||
{
|
||||
return m_aspect;
|
||||
}
|
||||
void setAspect(qreal v)
|
||||
{
|
||||
if (v != m_aspect) {
|
||||
m_aspect = v;
|
||||
emit aspectChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal modeA() const
|
||||
{
|
||||
return m_modeA;
|
||||
}
|
||||
void setModeA(qreal v)
|
||||
{
|
||||
if (v != m_modeA) {
|
||||
m_modeA = v;
|
||||
emit modeAChanged();
|
||||
}
|
||||
}
|
||||
qreal speedA() const
|
||||
{
|
||||
return m_speedA;
|
||||
}
|
||||
void setSpeedA(qreal v)
|
||||
{
|
||||
if (v != m_speedA) {
|
||||
m_speedA = v;
|
||||
emit speedAChanged();
|
||||
}
|
||||
}
|
||||
qreal seedA() const
|
||||
{
|
||||
return m_seedA;
|
||||
}
|
||||
void setSeedA(qreal v)
|
||||
{
|
||||
if (v != m_seedA) {
|
||||
m_seedA = v;
|
||||
emit seedAChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal modeB() const
|
||||
{
|
||||
return m_modeB;
|
||||
}
|
||||
void setModeB(qreal v)
|
||||
{
|
||||
if (v != m_modeB) {
|
||||
m_modeB = v;
|
||||
emit modeBChanged();
|
||||
}
|
||||
}
|
||||
qreal speedB() const
|
||||
{
|
||||
return m_speedB;
|
||||
}
|
||||
void setSpeedB(qreal v)
|
||||
{
|
||||
if (v != m_speedB) {
|
||||
m_speedB = v;
|
||||
emit speedBChanged();
|
||||
}
|
||||
}
|
||||
qreal seedB() const
|
||||
{
|
||||
return m_seedB;
|
||||
}
|
||||
void setSeedB(qreal v)
|
||||
{
|
||||
if (v != m_seedB) {
|
||||
m_seedB = v;
|
||||
emit seedBChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool frontIsA() const
|
||||
{
|
||||
return m_frontIsA;
|
||||
}
|
||||
void setFrontIsA(bool v)
|
||||
{
|
||||
if (v != m_frontIsA) {
|
||||
m_frontIsA = v;
|
||||
emit frontIsAChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QColor colorA() const
|
||||
{
|
||||
return m_colorA;
|
||||
}
|
||||
void setColorA(const QColor &c)
|
||||
{
|
||||
if (c != m_colorA) {
|
||||
m_colorA = c;
|
||||
emit colorAChanged();
|
||||
}
|
||||
}
|
||||
QColor colorB() const
|
||||
{
|
||||
return m_colorB;
|
||||
}
|
||||
void setColorB(const QColor &c)
|
||||
{
|
||||
if (c != m_colorB) {
|
||||
m_colorB = c;
|
||||
emit colorBChanged();
|
||||
}
|
||||
}
|
||||
QColor accent() const
|
||||
{
|
||||
return m_accent;
|
||||
}
|
||||
void setAccent(const QColor &c)
|
||||
{
|
||||
if (c != m_accent) {
|
||||
m_accent = c;
|
||||
emit accentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool logoVisible() const
|
||||
{
|
||||
return m_logoVisible;
|
||||
}
|
||||
void setLogoVisible(bool v)
|
||||
{
|
||||
if (v != m_logoVisible) {
|
||||
m_logoVisible = v;
|
||||
emit logoVisibleChanged();
|
||||
}
|
||||
}
|
||||
|
||||
qreal logoGlow() const
|
||||
{
|
||||
return m_logoGlow;
|
||||
}
|
||||
void setLogoGlow(qreal v)
|
||||
{
|
||||
if (v != m_logoGlow) {
|
||||
m_logoGlow = v;
|
||||
emit logoGlowChanged();
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void timeChanged();
|
||||
void aspectChanged();
|
||||
void modeAChanged();
|
||||
void speedAChanged();
|
||||
void seedAChanged();
|
||||
void modeBChanged();
|
||||
void speedBChanged();
|
||||
void seedBChanged();
|
||||
void frontIsAChanged();
|
||||
void colorAChanged();
|
||||
void colorBChanged();
|
||||
void accentChanged();
|
||||
void logoVisibleChanged();
|
||||
void logoGlowChanged();
|
||||
|
||||
private:
|
||||
qreal m_time = 0.0;
|
||||
qreal m_aspect = 16.0 / 9.0;
|
||||
|
||||
qreal m_modeA = 0.0;
|
||||
qreal m_speedA = 1.0;
|
||||
qreal m_seedA = 0.0;
|
||||
|
||||
qreal m_modeB = 0.0;
|
||||
qreal m_speedB = 1.0;
|
||||
qreal m_seedB = 0.0;
|
||||
|
||||
bool m_frontIsA = true;
|
||||
|
||||
QColor m_colorA{0x1A, 0x1A, 0x20};
|
||||
QColor m_colorB{0x0E, 0x0E, 0x12};
|
||||
QColor m_accent{0x8B, 0xDD, 0x6B};
|
||||
|
||||
bool m_logoVisible = false;
|
||||
qreal m_logoGlow = 1.0;
|
||||
};
|
||||
|
||||
#endif // BANNER_SHADER_CONFIG_H
|
||||
218
cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
Normal file
218
cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
#include "first_run_wizard.h"
|
||||
|
||||
#include "first_run_wizard_page.h"
|
||||
#include "pages/account_setup_page.h"
|
||||
#include "pages/card_database_setup_page.h"
|
||||
#include "pages/finish_page.h"
|
||||
#include "pages/preferences_setup_page.h"
|
||||
#include "pages/theme_setup_page.h"
|
||||
#include "pages/welcome_page.h"
|
||||
#include "shader_banner_widget.h"
|
||||
#include "step_indicator_widget.h"
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QEvent>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
|
||||
setMinimumSize(640, 490);
|
||||
resize(720, 550);
|
||||
|
||||
bannerHost = new BannerHost(this);
|
||||
|
||||
titleLabel = new QLabel(this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4);
|
||||
titleFont.setBold(true);
|
||||
titleLabel->setFont(titleFont);
|
||||
|
||||
subtitleLabel = new QLabel(this);
|
||||
subtitleLabel->setWordWrap(true);
|
||||
|
||||
stack = new QStackedWidget(this);
|
||||
stepIndicator = new StepIndicatorWidget(this);
|
||||
|
||||
backButton = new QPushButton(this);
|
||||
skipButton = new QPushButton(this);
|
||||
nextButton = new QPushButton(this);
|
||||
nextButton->setDefault(true);
|
||||
|
||||
connect(backButton, &QPushButton::clicked, this, &FirstRunWizard::goBack);
|
||||
connect(skipButton, &QPushButton::clicked, this, &FirstRunWizard::skip);
|
||||
connect(nextButton, &QPushButton::clicked, this, &FirstRunWizard::goNext);
|
||||
|
||||
auto *headerLayout = new QVBoxLayout;
|
||||
headerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
headerLayout->addWidget(bannerHost);
|
||||
headerLayout->addSpacing(12);
|
||||
headerLayout->addWidget(titleLabel);
|
||||
headerLayout->addWidget(subtitleLabel);
|
||||
|
||||
auto *navLayout = new QHBoxLayout;
|
||||
navLayout->addWidget(backButton);
|
||||
navLayout->addWidget(skipButton);
|
||||
navLayout->addStretch();
|
||||
navLayout->addWidget(stepIndicator);
|
||||
navLayout->addStretch();
|
||||
navLayout->addWidget(nextButton);
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->addLayout(headerLayout);
|
||||
root->addSpacing(8);
|
||||
root->addWidget(stack, 1);
|
||||
root->addSpacing(8);
|
||||
root->addLayout(navLayout);
|
||||
|
||||
auto *welcome = new WelcomePage(this);
|
||||
auto *cardDb = new CardDatabaseSetupPage(this);
|
||||
auto *theme = new ThemeSetupPage(this);
|
||||
auto *account = new AccountSetupPage(this);
|
||||
auto *prefs = new PreferencesSetupPage(this);
|
||||
auto *finishPg = new FinishPage(this);
|
||||
|
||||
cardDatabasePage = cardDb;
|
||||
|
||||
connect(cardDb, &CardDatabaseSetupPage::updateRequested, this, &FirstRunWizard::cardDatabaseUpdateRequested);
|
||||
connect(cardDb, &CardDatabaseSetupPage::manualSetupRequested, this,
|
||||
&FirstRunWizard::manualCardDatabaseSetupRequested);
|
||||
connect(account, &AccountSetupPage::registerRequested, this, &FirstRunWizard::registerRequested);
|
||||
connect(account, &AccountSetupPage::connectRequested, this, &FirstRunWizard::connectRequested);
|
||||
|
||||
connect(cardDb, &CardDatabaseSetupPage::advanceRequested, this, [this] {
|
||||
if (stack->currentWidget() == cardDatabasePage) {
|
||||
showPage(currentIndex + 1);
|
||||
}
|
||||
});
|
||||
|
||||
addPage(welcome);
|
||||
addPage(cardDb);
|
||||
addPage(theme);
|
||||
addPage(account);
|
||||
addPage(prefs);
|
||||
addPage(finishPg);
|
||||
|
||||
stepIndicator->setStepCount(pages.count());
|
||||
retranslateUi();
|
||||
showPage(0);
|
||||
}
|
||||
|
||||
void FirstRunWizard::addPage(FirstRunWizardPage *page)
|
||||
{
|
||||
pages.append(page);
|
||||
stack->addWidget(page);
|
||||
connect(page, &FirstRunWizardPage::completeChanged, this, &FirstRunWizard::updateChrome);
|
||||
}
|
||||
|
||||
void FirstRunWizard::showPage(int index)
|
||||
{
|
||||
if (index < 0 || index >= pages.count()) {
|
||||
return;
|
||||
}
|
||||
currentIndex = index;
|
||||
stack->setCurrentIndex(index);
|
||||
pages[index]->initializePage();
|
||||
stepIndicator->setCurrentStep(index);
|
||||
static const QList<BannerHost::Motif> motifs = {
|
||||
BannerHost::Motif::Welcome, BannerHost::Motif::CardDatabase, BannerHost::Motif::Theming,
|
||||
BannerHost::Motif::Account, BannerHost::Motif::Preferences, BannerHost::Motif::Finish,
|
||||
};
|
||||
if (index < motifs.size()) {
|
||||
bannerHost->setMotif(motifs[index]);
|
||||
}
|
||||
titleLabel->setText(pages[index]->stepTitle());
|
||||
subtitleLabel->setText(pages[index]->stepSubtitle());
|
||||
subtitleLabel->setVisible(!pages[index]->stepSubtitle().isEmpty());
|
||||
updateChrome();
|
||||
}
|
||||
|
||||
void FirstRunWizard::updateChrome()
|
||||
{
|
||||
if (currentIndex < 0) {
|
||||
return;
|
||||
}
|
||||
FirstRunWizardPage *page = pages[currentIndex];
|
||||
const bool isLast = (currentIndex == pages.count() - 1);
|
||||
|
||||
backButton->setVisible(currentIndex > 0);
|
||||
skipButton->setVisible(page->isSkippable());
|
||||
nextButton->setEnabled(page->isComplete());
|
||||
|
||||
QString customText = page->nextButtonText();
|
||||
if (!customText.isEmpty()) {
|
||||
nextButton->setText(customText);
|
||||
} else {
|
||||
nextButton->setText(isLast ? tr("Finish") : tr("Next"));
|
||||
}
|
||||
}
|
||||
|
||||
void FirstRunWizard::goNext()
|
||||
{
|
||||
FirstRunWizardPage *page = pages[currentIndex];
|
||||
if (!page->validatePage() || !page->handleNextClick()) {
|
||||
return;
|
||||
}
|
||||
if (currentIndex == pages.count() - 1) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
showPage(currentIndex + 1);
|
||||
}
|
||||
|
||||
void FirstRunWizard::goBack()
|
||||
{
|
||||
showPage(currentIndex - 1);
|
||||
}
|
||||
|
||||
void FirstRunWizard::skip()
|
||||
{
|
||||
showPage(currentIndex + 1);
|
||||
}
|
||||
|
||||
void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
|
||||
{
|
||||
if (cardDatabasePage) {
|
||||
cardDatabasePage->onUpdateFinished(success);
|
||||
}
|
||||
}
|
||||
|
||||
void FirstRunWizard::finish()
|
||||
{
|
||||
accept();
|
||||
}
|
||||
|
||||
void FirstRunWizard::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
// Every step persists its own choice as it's made, so closing early
|
||||
// isn't destructive -- treat it exactly like reaching the end.
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
|
||||
void FirstRunWizard::changeEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
retranslateUi();
|
||||
}
|
||||
QDialog::changeEvent(event);
|
||||
}
|
||||
|
||||
void FirstRunWizard::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Welcome to Cockatrice"));
|
||||
backButton->setText(tr("Back"));
|
||||
skipButton->setText(tr("Skip"));
|
||||
for (FirstRunWizardPage *page : std::as_const(pages)) {
|
||||
page->retranslateUi();
|
||||
}
|
||||
if (currentIndex >= 0) {
|
||||
titleLabel->setText(pages[currentIndex]->stepTitle());
|
||||
subtitleLabel->setText(pages[currentIndex]->stepSubtitle());
|
||||
}
|
||||
updateChrome();
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
#ifndef FIRST_RUN_WIZARD_H
|
||||
#define FIRST_RUN_WIZARD_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
class BannerHost;
|
||||
class FirstRunWizardPage;
|
||||
class StepIndicatorWidget;
|
||||
class CardDatabaseSetupPage;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QStackedWidget;
|
||||
|
||||
/** @brief Polished first-run onboarding flow: card database setup, theme
|
||||
* selection, server account setup, and a handful of key preferences.
|
||||
*
|
||||
* Deliberately ignorant of network/registration/download internals --
|
||||
* pages that need them emit request signals for MainWindow to fulfill.
|
||||
* Every choice is written to SettingsCache as it's made (via the pages
|
||||
* themselves, same as AppearanceSettingsPage does), so "Skip" or closing
|
||||
* the window never discards anything already confirmed. */
|
||||
class FirstRunWizard : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FirstRunWizard(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void registerRequested();
|
||||
void connectRequested();
|
||||
void cardDatabaseUpdateRequested();
|
||||
void manualCardDatabaseSetupRequested();
|
||||
|
||||
public slots:
|
||||
/** @brief Forwarded from MainWindow once the background card database update process exits. */
|
||||
void onCardDatabaseUpdateFinished(bool success);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void goNext();
|
||||
void goBack();
|
||||
void skip();
|
||||
void updateChrome();
|
||||
|
||||
private:
|
||||
void addPage(FirstRunWizardPage *page);
|
||||
void showPage(int index);
|
||||
void retranslateUi();
|
||||
void finish();
|
||||
|
||||
QStackedWidget *stack;
|
||||
StepIndicatorWidget *stepIndicator;
|
||||
BannerHost *bannerHost;
|
||||
QLabel *titleLabel;
|
||||
QLabel *subtitleLabel;
|
||||
QPushButton *backButton;
|
||||
QPushButton *skipButton;
|
||||
QPushButton *nextButton;
|
||||
|
||||
CardDatabaseSetupPage *cardDatabasePage = nullptr;
|
||||
|
||||
QList<FirstRunWizardPage *> pages;
|
||||
int currentIndex = -1;
|
||||
};
|
||||
|
||||
#endif // FIRST_RUN_WIZARD_H
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "first_run_wizard_page.h"
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#ifndef FIRST_RUN_WIZARD_PAGE_H
|
||||
#define FIRST_RUN_WIZARD_PAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
/** @brief Base class for a single step of FirstRunWizard.
|
||||
*
|
||||
* QWidget-based rather than QWizardPage-based: FirstRunWizard is a
|
||||
* QDialog + QStackedWidget shell (not a QWizard) so it can own the
|
||||
* banner/step-dot chrome that QWizard's native styles don't give us
|
||||
* consistent control over. Naming mirrors OracleWizardPage for
|
||||
* familiarity only -- the two hierarchies are unrelated. */
|
||||
class FirstRunWizardPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FirstRunWizardPage(QWidget *parent = nullptr) : QWidget(parent)
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Called every time the page becomes visible, including navigating back to it. */
|
||||
virtual void initializePage()
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Called before advancing past this page. Return false to block navigation;
|
||||
the page itself is responsible for telling the user why. */
|
||||
virtual bool validatePage()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief Whether Next/Finish should currently be enabled. Pages doing async work
|
||||
can flip this mid-step; emit completeChanged() when they do. */
|
||||
virtual bool isComplete() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief Whether the wizard's "Skip" button should be offered on this page. */
|
||||
virtual bool isSkippable() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual QString stepTitle() const = 0;
|
||||
virtual QString stepSubtitle() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @brief Override to replace the "Next"/"Finish" button text on this page.
|
||||
Return an empty string to use the default label. */
|
||||
virtual QString nextButtonText() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @brief Called when the user presses the Next button. Return true to allow
|
||||
advancing to the next page, false to stay on this page (e.g. to
|
||||
trigger an async action first). */
|
||||
virtual bool handleNextClick()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void retranslateUi() = 0;
|
||||
|
||||
signals:
|
||||
void completeChanged();
|
||||
void advanceRequested();
|
||||
};
|
||||
|
||||
#endif // FIRST_RUN_WIZARD_PAGE_H
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
#include "account_setup_page.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
AccountSetupPage::AccountSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
registerButton = new QPushButton(this);
|
||||
connectButton = new QPushButton(this);
|
||||
skipHintLabel = new QLabel(this);
|
||||
skipHintLabel->setWordWrap(true);
|
||||
skipHintLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
connect(registerButton, &QPushButton::clicked, this, &AccountSetupPage::registerRequested);
|
||||
connect(connectButton, &QPushButton::clicked, this, &AccountSetupPage::connectRequested);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addSpacing(16);
|
||||
layout->addWidget(registerButton, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(connectButton, 0, Qt::AlignHCenter);
|
||||
layout->addSpacing(16);
|
||||
layout->addWidget(skipHintLabel);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
bool AccountSetupPage::isSkippable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString AccountSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("Join a Server");
|
||||
}
|
||||
|
||||
QString AccountSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("Optional — you can always do this later from the menu.");
|
||||
}
|
||||
|
||||
void AccountSetupPage::retranslateUi()
|
||||
{
|
||||
bodyLabel->setText(tr("Playing online needs a server account."));
|
||||
registerButton->setText(tr("Register a new account…"));
|
||||
connectButton->setText(tr("I already have one — Connect…"));
|
||||
skipHintLabel->setText(tr("Just want to play locally? Skip this and connect whenever you're ready."));
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#ifndef ACCOUNT_SETUP_PAGE_H
|
||||
#define ACCOUNT_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
/** @brief First-run account step. Does NOT embed DlgRegister's fields: they exist
|
||||
* to be handed to ConnectionController's network registration flow, which
|
||||
* this wizard has no visibility into. Reimplementing the fields here
|
||||
* without that wiring would look functional and silently do nothing --
|
||||
* worse than reuse. So: a friendly landing spot that opens the *existing*
|
||||
* DlgRegister / connect flow via signals FirstRunWizard forwards. */
|
||||
class AccountSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AccountSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
signals:
|
||||
void registerRequested();
|
||||
void connectRequested();
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
QPushButton *registerButton;
|
||||
QPushButton *connectButton;
|
||||
QLabel *skipHintLabel;
|
||||
};
|
||||
|
||||
#endif // ACCOUNT_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
#include "card_database_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
|
||||
CardDatabaseSetupPage::CardDatabaseSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setWordWrap(true);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
progressBar = new QProgressBar(this);
|
||||
progressBar->setRange(0, 0);
|
||||
progressBar->setTextVisible(false);
|
||||
progressBar->setFixedWidth(280);
|
||||
|
||||
retryButton = new QPushButton(this);
|
||||
manualButton = new QPushButton(this);
|
||||
|
||||
connect(retryButton, &QPushButton::clicked, this, [this] {
|
||||
setState(State::Running);
|
||||
emit updateRequested();
|
||||
});
|
||||
connect(manualButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::manualSetupRequested);
|
||||
|
||||
// ── Advanced: custom download source ───────────────────────────────
|
||||
advancedToggleButton = new QPushButton(this);
|
||||
advancedToggleButton->setCheckable(true);
|
||||
advancedToggleButton->setChecked(false);
|
||||
advancedToggleButton->setFlat(true);
|
||||
advancedToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }"
|
||||
"QPushButton:checked { }");
|
||||
|
||||
advancedPanel = new QWidget(this);
|
||||
advancedPanel->setVisible(false);
|
||||
|
||||
urlLineEdit = new QLineEdit(advancedPanel);
|
||||
urlHintLabel = new QLabel(advancedPanel);
|
||||
urlHintLabel->setWordWrap(true);
|
||||
|
||||
restoreDefaultUrlButton = new QPushButton(advancedPanel);
|
||||
applyAndRetryButton = new QPushButton(advancedPanel);
|
||||
|
||||
connect(advancedToggleButton, &QPushButton::toggled, this, &CardDatabaseSetupPage::onToggleAdvanced);
|
||||
connect(restoreDefaultUrlButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::onRestoreDefaultUrl);
|
||||
connect(applyAndRetryButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::onApplyCustomUrl);
|
||||
|
||||
auto *advancedButtonRow = new QHBoxLayout;
|
||||
advancedButtonRow->addWidget(restoreDefaultUrlButton);
|
||||
advancedButtonRow->addStretch();
|
||||
advancedButtonRow->addWidget(applyAndRetryButton);
|
||||
|
||||
auto *advancedLayout = new QVBoxLayout(advancedPanel);
|
||||
advancedLayout->setContentsMargins(12, 4, 12, 4);
|
||||
advancedLayout->addWidget(urlLineEdit);
|
||||
advancedLayout->addWidget(urlHintLabel);
|
||||
advancedLayout->addLayout(advancedButtonRow);
|
||||
|
||||
// ── Startup card update check ───────────────────────────────────────
|
||||
auto &upd = SettingsCache::instance().updates();
|
||||
|
||||
const auto updateBehavior = [this] {
|
||||
auto &u = SettingsCache::instance().updates();
|
||||
int idx = startupBehaviorCombo->currentIndex();
|
||||
u.setStartupCardUpdateCheckPromptForUpdate(idx == 1);
|
||||
u.setStartupCardUpdateCheckAlwaysUpdate(idx == 2);
|
||||
};
|
||||
|
||||
startupBehaviorLabel = new QLabel(this);
|
||||
startupBehaviorCombo = new QComboBox(this);
|
||||
startupBehaviorCombo->addItem(QString()); // placeholder, filled in retranslateUi
|
||||
startupBehaviorCombo->addItem(QString());
|
||||
startupBehaviorCombo->addItem(QString());
|
||||
if (upd.getStartupCardUpdateCheckPromptForUpdate()) {
|
||||
startupBehaviorCombo->setCurrentIndex(1);
|
||||
} else if (upd.getStartupCardUpdateCheckAlwaysUpdate()) {
|
||||
startupBehaviorCombo->setCurrentIndex(2);
|
||||
} else {
|
||||
startupBehaviorCombo->setCurrentIndex(0);
|
||||
}
|
||||
connect(startupBehaviorCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, updateBehavior);
|
||||
|
||||
checkIntervalLabel = new QLabel(this);
|
||||
checkIntervalSpinBox = new QSpinBox(this);
|
||||
checkIntervalSpinBox->setMinimum(1);
|
||||
checkIntervalSpinBox->setMaximum(30);
|
||||
checkIntervalSpinBox->setValue(upd.getCardUpdateCheckInterval());
|
||||
connect(checkIntervalSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), &upd,
|
||||
&UpdatesSettings::setCardUpdateCheckInterval);
|
||||
|
||||
auto *checkGrid = new QGridLayout;
|
||||
checkGrid->addWidget(startupBehaviorLabel, 0, 0);
|
||||
checkGrid->addWidget(startupBehaviorCombo, 0, 1);
|
||||
checkGrid->addWidget(checkIntervalLabel, 1, 0);
|
||||
checkGrid->addWidget(checkIntervalSpinBox, 1, 1);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(statusLabel);
|
||||
layout->addSpacing(12);
|
||||
layout->addWidget(progressBar, 0, Qt::AlignHCenter);
|
||||
layout->addSpacing(12);
|
||||
layout->addWidget(retryButton, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(manualButton, 0, Qt::AlignHCenter);
|
||||
layout->addSpacing(16);
|
||||
layout->addWidget(advancedToggleButton);
|
||||
layout->addWidget(advancedPanel);
|
||||
layout->addSpacing(8);
|
||||
layout->addLayout(checkGrid);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::alreadyHaveDatabase() const
|
||||
{
|
||||
return CardDatabaseManager::getInstance()->getCardList().count() > 0;
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::oracleSettingsFilePath() const
|
||||
{
|
||||
return SettingsCache::instance().getSettingsPath() + "oracle.ini";
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::readCustomUrl() const
|
||||
{
|
||||
QSettings oracleSettings(oracleSettingsFilePath(), QSettings::IniFormat);
|
||||
return oracleSettings.value("allsetsurl").toString();
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::writeCustomUrl(const QString &url)
|
||||
{
|
||||
QSettings oracleSettings(oracleSettingsFilePath(), QSettings::IniFormat);
|
||||
if (url.isEmpty()) {
|
||||
oracleSettings.remove("allsetsurl");
|
||||
} else {
|
||||
oracleSettings.setValue("allsetsurl", url);
|
||||
}
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::initializePage()
|
||||
{
|
||||
urlLineEdit->setText(readCustomUrl());
|
||||
|
||||
if (state != State::NotStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyHaveDatabase()) {
|
||||
setState(State::Succeeded);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't auto-download — wait for the user to press "Download".
|
||||
setState(State::NotStarted);
|
||||
statusLabel->setText(tr("Press Download to fetch the card database, or Skip to do it later."));
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onUpdateFinished(bool success)
|
||||
{
|
||||
setState(success ? State::Succeeded : State::Failed);
|
||||
if (success) {
|
||||
emit advanceRequested();
|
||||
}
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::nextButtonText() const
|
||||
{
|
||||
return state == State::NotStarted ? tr("Download") : QString();
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::handleNextClick()
|
||||
{
|
||||
if (state == State::NotStarted) {
|
||||
setState(State::Running);
|
||||
emit updateRequested();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onToggleAdvanced(bool open)
|
||||
{
|
||||
advancedToggleButton->setText(open ? tr("▼ Advanced: custom download source")
|
||||
: tr("▶ Advanced: custom download source"));
|
||||
advancedPanel->setVisible(open);
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onApplyCustomUrl()
|
||||
{
|
||||
const QString text = urlLineEdit->text().trimmed();
|
||||
|
||||
if (!text.isEmpty()) {
|
||||
const QUrl url = QUrl::fromUserInput(text);
|
||||
if (!url.isValid()) {
|
||||
QMessageBox::warning(this, tr("Invalid URL"),
|
||||
tr("That doesn't look like a valid URL. Double-check it and try again, "
|
||||
"or clear the field to use the default source."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeCustomUrl(text);
|
||||
setState(State::Running);
|
||||
emit updateRequested();
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onRestoreDefaultUrl()
|
||||
{
|
||||
urlLineEdit->clear();
|
||||
writeCustomUrl(QString());
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::setState(State newState)
|
||||
{
|
||||
state = newState;
|
||||
|
||||
progressBar->setVisible(state == State::Running);
|
||||
retryButton->setVisible(state == State::Failed);
|
||||
manualButton->setVisible(state == State::Failed);
|
||||
applyAndRetryButton->setEnabled(state != State::Running);
|
||||
|
||||
switch (state) {
|
||||
case State::NotStarted:
|
||||
statusLabel->setText(tr("Press Download to fetch the card database, or Skip to do it later."));
|
||||
break;
|
||||
case State::Running:
|
||||
statusLabel->setText(tr("Downloading the latest card database…"));
|
||||
break;
|
||||
case State::Succeeded:
|
||||
statusLabel->setText(tr("Card database ready ✓"));
|
||||
break;
|
||||
case State::Failed:
|
||||
statusLabel->setText(
|
||||
tr("Couldn't download the card database automatically. Check your connection and retry, "
|
||||
"set it up manually, or skip this for now — you can do it later from the Card Database menu."));
|
||||
break;
|
||||
}
|
||||
|
||||
emit completeChanged();
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::isComplete() const
|
||||
{
|
||||
return state != State::Running;
|
||||
}
|
||||
|
||||
bool CardDatabaseSetupPage::isSkippable() const
|
||||
{
|
||||
return state != State::Succeeded;
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("Card Database");
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("Cockatrice needs card data to know what you're playing with.");
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::retranslateUi()
|
||||
{
|
||||
retryButton->setText(tr("Retry"));
|
||||
manualButton->setText(tr("Set up manually…"));
|
||||
|
||||
onToggleAdvanced(advancedToggleButton->isChecked());
|
||||
urlLineEdit->setPlaceholderText(tr("Leave blank to use the default source"));
|
||||
urlHintLabel->setText(tr("Only change this if you know you need a mirror or a custom card data source."));
|
||||
restoreDefaultUrlButton->setText(tr("Restore default"));
|
||||
applyAndRetryButton->setText(tr("Apply && retry"));
|
||||
|
||||
startupBehaviorLabel->setText(tr("Check for card database updates on startup"));
|
||||
startupBehaviorCombo->setItemText(0, tr("Don't check"));
|
||||
startupBehaviorCombo->setItemText(1, tr("Prompt for update"));
|
||||
startupBehaviorCombo->setItemText(2, tr("Always update in the background"));
|
||||
|
||||
checkIntervalLabel->setText(tr("Check for card database updates every"));
|
||||
checkIntervalSpinBox->setSuffix(tr(" days"));
|
||||
|
||||
setState(state);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
#ifndef CARD_DATABASE_SETUP_PAGE_H
|
||||
#define CARD_DATABASE_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QProgressBar;
|
||||
class QPushButton;
|
||||
class QSpinBox;
|
||||
class QWidget;
|
||||
|
||||
class CardDatabaseSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CardDatabaseSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
QString nextButtonText() const override;
|
||||
bool handleNextClick() override;
|
||||
void retranslateUi() override;
|
||||
|
||||
void onUpdateFinished(bool success);
|
||||
|
||||
signals:
|
||||
void updateRequested();
|
||||
void manualSetupRequested();
|
||||
|
||||
private:
|
||||
enum class State
|
||||
{
|
||||
NotStarted,
|
||||
Running,
|
||||
Succeeded,
|
||||
Failed,
|
||||
};
|
||||
|
||||
void setState(State newState);
|
||||
bool alreadyHaveDatabase() const;
|
||||
|
||||
QString oracleSettingsFilePath() const;
|
||||
QString readCustomUrl() const;
|
||||
void writeCustomUrl(const QString &url);
|
||||
|
||||
void onToggleAdvanced(bool open);
|
||||
void onApplyCustomUrl();
|
||||
void onRestoreDefaultUrl();
|
||||
|
||||
QLabel *statusLabel;
|
||||
QProgressBar *progressBar;
|
||||
QPushButton *retryButton;
|
||||
QPushButton *manualButton;
|
||||
|
||||
QPushButton *advancedToggleButton;
|
||||
QWidget *advancedPanel;
|
||||
QLineEdit *urlLineEdit;
|
||||
QLabel *urlHintLabel;
|
||||
QPushButton *restoreDefaultUrlButton;
|
||||
QPushButton *applyAndRetryButton;
|
||||
|
||||
QLabel *startupBehaviorLabel;
|
||||
QComboBox *startupBehaviorCombo;
|
||||
QLabel *checkIntervalLabel;
|
||||
QSpinBox *checkIntervalSpinBox;
|
||||
|
||||
State state = State::NotStarted;
|
||||
};
|
||||
|
||||
#endif // CARD_DATABASE_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#include "finish_page.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
FinishPage::FinishPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
QString FinishPage::stepTitle() const
|
||||
{
|
||||
return tr("You're All Set");
|
||||
}
|
||||
|
||||
void FinishPage::retranslateUi()
|
||||
{
|
||||
bodyLabel->setText(
|
||||
tr("That's everything for now. Jump into Settings any time to change your mind about any of this.\n\n"
|
||||
"Have fun!"));
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef FINISH_PAGE_H
|
||||
#define FINISH_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QLabel;
|
||||
|
||||
class FinishPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FinishPage(QWidget *parent = nullptr);
|
||||
|
||||
QString stepTitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
};
|
||||
|
||||
#endif // FINISH_PAGE_H
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
#include "preferences_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QGroupBox>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/sound_settings.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
QGroupBox *makeGroup(QWidget *parent, QVBoxLayout *outerLayout)
|
||||
{
|
||||
auto *group = new QGroupBox(parent);
|
||||
new QVBoxLayout(group);
|
||||
outerLayout->addWidget(group);
|
||||
return group;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PreferencesSetupPage::PreferencesSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
auto *content = new QWidget;
|
||||
auto *contentLayout = new QVBoxLayout(content);
|
||||
|
||||
appearanceGroup = makeGroup(content, contentLayout);
|
||||
styleUserListCheckBox = new QCheckBox(appearanceGroup);
|
||||
cardScalingCheckBox = new QCheckBox(appearanceGroup);
|
||||
roundCardCornersCheckBox = new QCheckBox(appearanceGroup);
|
||||
displayCardNamesCheckBox = new QCheckBox(appearanceGroup);
|
||||
autoRotateCardsCheckBox = new QCheckBox(appearanceGroup);
|
||||
tapAnimationCheckBox = new QCheckBox(appearanceGroup);
|
||||
appearanceGroup->layout()->addWidget(styleUserListCheckBox);
|
||||
appearanceGroup->layout()->addWidget(cardScalingCheckBox);
|
||||
appearanceGroup->layout()->addWidget(roundCardCornersCheckBox);
|
||||
appearanceGroup->layout()->addWidget(displayCardNamesCheckBox);
|
||||
appearanceGroup->layout()->addWidget(autoRotateCardsCheckBox);
|
||||
appearanceGroup->layout()->addWidget(tapAnimationCheckBox);
|
||||
|
||||
notificationsGroup = makeGroup(content, contentLayout);
|
||||
notificationsEnabledCheckBox = new QCheckBox(notificationsGroup);
|
||||
soundEnabledCheckBox = new QCheckBox(notificationsGroup);
|
||||
notificationsGroup->layout()->addWidget(notificationsEnabledCheckBox);
|
||||
notificationsGroup->layout()->addWidget(soundEnabledCheckBox);
|
||||
|
||||
gameplayGroup = makeGroup(content, contentLayout);
|
||||
doubleClickToPlayCheckBox = new QCheckBox(gameplayGroup);
|
||||
horizontalHandCheckBox = new QCheckBox(gameplayGroup);
|
||||
playToStackCheckBox = new QCheckBox(gameplayGroup);
|
||||
gameplayGroup->layout()->addWidget(doubleClickToPlayCheckBox);
|
||||
gameplayGroup->layout()->addWidget(horizontalHandCheckBox);
|
||||
gameplayGroup->layout()->addWidget(playToStackCheckBox);
|
||||
|
||||
menuGroup = makeGroup(content, contentLayout);
|
||||
showShortcutsCheckBox = new QCheckBox(menuGroup);
|
||||
menuGroup->layout()->addWidget(showShortcutsCheckBox);
|
||||
|
||||
dataGroup = makeGroup(content, contentLayout);
|
||||
picDownloadCheckBox = new QCheckBox(dataGroup);
|
||||
checkUpdatesOnStartupCheckBox = new QCheckBox(dataGroup);
|
||||
showTipsOnStartupCheckBox = new QCheckBox(dataGroup);
|
||||
dataGroup->layout()->addWidget(picDownloadCheckBox);
|
||||
dataGroup->layout()->addWidget(checkUpdatesOnStartupCheckBox);
|
||||
dataGroup->layout()->addWidget(showTipsOnStartupCheckBox);
|
||||
|
||||
contentLayout->addStretch();
|
||||
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidget(content);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(scrollArea);
|
||||
|
||||
SettingsCache &settings = SettingsCache::instance();
|
||||
|
||||
connect(styleUserListCheckBox, &QCheckBox::toggled, &settings.interface(), &InterfaceSettings::setStyleUserList);
|
||||
connect(cardScalingCheckBox, &QCheckBox::toggled, &settings.cardsDisplay(), &CardsDisplaySettings::setCardScaling);
|
||||
connect(roundCardCornersCheckBox, &QCheckBox::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setRoundCardCorners);
|
||||
connect(displayCardNamesCheckBox, &QCheckBox::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setDisplayCardNames);
|
||||
connect(autoRotateCardsCheckBox, &QCheckBox::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setAutoRotateSidewaysLayoutCards);
|
||||
connect(tapAnimationCheckBox, &QCheckBox::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setTapAnimation);
|
||||
|
||||
connect(notificationsEnabledCheckBox, &QCheckBox::toggled, &settings.interface(),
|
||||
&InterfaceSettings::setNotificationsEnabled);
|
||||
connect(soundEnabledCheckBox, &QCheckBox::toggled, &settings.sound(), &SoundSettings::setSoundEnabled);
|
||||
|
||||
connect(doubleClickToPlayCheckBox, &QCheckBox::toggled, &settings.interface(),
|
||||
&InterfaceSettings::setDoubleClickToPlay);
|
||||
connect(horizontalHandCheckBox, &QCheckBox::toggled, &settings.interface(), &InterfaceSettings::setHorizontalHand);
|
||||
connect(playToStackCheckBox, &QCheckBox::toggled, &settings.interface(), &InterfaceSettings::setPlayToStack);
|
||||
|
||||
connect(showShortcutsCheckBox, &QCheckBox::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setShowShortcuts);
|
||||
|
||||
connect(picDownloadCheckBox, &QCheckBox::toggled, &settings.personal(), &PersonalSettings::setPicDownload);
|
||||
connect(checkUpdatesOnStartupCheckBox, &QCheckBox::toggled, &settings.updates(),
|
||||
&UpdatesSettings::setCheckUpdatesOnStartup);
|
||||
connect(showTipsOnStartupCheckBox, &QCheckBox::toggled, &settings.personal(),
|
||||
&PersonalSettings::setShowTipsOnStartup);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PreferencesSetupPage::initializePage()
|
||||
{
|
||||
SettingsCache &settings = SettingsCache::instance();
|
||||
|
||||
styleUserListCheckBox->setChecked(settings.interface().getStyleUserList());
|
||||
cardScalingCheckBox->setChecked(settings.cardsDisplay().getScaleCards());
|
||||
roundCardCornersCheckBox->setChecked(settings.cardsDisplay().getRoundCardCorners());
|
||||
displayCardNamesCheckBox->setChecked(settings.cardsDisplay().getDisplayCardNames());
|
||||
autoRotateCardsCheckBox->setChecked(settings.cardsDisplay().getAutoRotateSidewaysLayoutCards());
|
||||
tapAnimationCheckBox->setChecked(settings.cardsDisplay().getTapAnimation());
|
||||
|
||||
notificationsEnabledCheckBox->setChecked(settings.interface().getNotificationsEnabled());
|
||||
soundEnabledCheckBox->setChecked(settings.sound().getSoundEnabled());
|
||||
|
||||
doubleClickToPlayCheckBox->setChecked(settings.interface().getDoubleClickToPlay());
|
||||
horizontalHandCheckBox->setChecked(settings.interface().getHorizontalHand());
|
||||
playToStackCheckBox->setChecked(settings.interface().getPlayToStack());
|
||||
|
||||
showShortcutsCheckBox->setChecked(settings.cardsDisplay().getShowShortcuts());
|
||||
|
||||
picDownloadCheckBox->setChecked(settings.personal().getPicDownload());
|
||||
checkUpdatesOnStartupCheckBox->setChecked(settings.updates().getCheckUpdatesOnStartup());
|
||||
showTipsOnStartupCheckBox->setChecked(settings.personal().getShowTipsOnStartup());
|
||||
}
|
||||
|
||||
bool PreferencesSetupPage::isSkippable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString PreferencesSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("A Few Preferences");
|
||||
}
|
||||
|
||||
QString PreferencesSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("Defaults are fine — tweak these now or from Settings anytime.");
|
||||
}
|
||||
|
||||
void PreferencesSetupPage::retranslateUi()
|
||||
{
|
||||
appearanceGroup->setTitle(tr("Appearance"));
|
||||
styleUserListCheckBox->setText(tr("Use the styled user list (avatars, role colours)"));
|
||||
cardScalingCheckBox->setText(tr("Scale cards to fit the window"));
|
||||
roundCardCornersCheckBox->setText(tr("Round card corners"));
|
||||
displayCardNamesCheckBox->setText(tr("Display card names on pictured cards"));
|
||||
autoRotateCardsCheckBox->setText(tr("Auto-rotate sideways layout cards"));
|
||||
tapAnimationCheckBox->setText(tr("Animate tapping cards"));
|
||||
|
||||
notificationsGroup->setTitle(tr("Notifications && Sound"));
|
||||
notificationsEnabledCheckBox->setText(tr("Show desktop notifications"));
|
||||
soundEnabledCheckBox->setText(tr("Play sound effects"));
|
||||
|
||||
gameplayGroup->setTitle(tr("Gameplay"));
|
||||
doubleClickToPlayCheckBox->setText(tr("Double-click a card to play it"));
|
||||
horizontalHandCheckBox->setText(tr("Display hand horizontally"));
|
||||
playToStackCheckBox->setText(tr("Play cards to top of stack"));
|
||||
|
||||
menuGroup->setTitle(tr("Menus"));
|
||||
showShortcutsCheckBox->setText(tr("Show keyboard shortcuts in menus"));
|
||||
|
||||
dataGroup->setTitle(tr("Updates && Data"));
|
||||
picDownloadCheckBox->setText(tr("Automatically download card images"));
|
||||
picDownloadCheckBox->setToolTip(tr("Turn this off if you're on a limited connection — "
|
||||
"card art just won't load until you turn it back on."));
|
||||
checkUpdatesOnStartupCheckBox->setText(tr("Check for client updates on startup"));
|
||||
showTipsOnStartupCheckBox->setText(tr("Show tip of the day on startup"));
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
#ifndef PREFERENCES_SETUP_PAGE_H
|
||||
#define PREFERENCES_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QGroupBox;
|
||||
|
||||
/** @brief A curated subset of settings for the user to adjust.
|
||||
**/
|
||||
class PreferencesSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PreferencesSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
void initializePage() override;
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private:
|
||||
QGroupBox *appearanceGroup;
|
||||
QCheckBox *styleUserListCheckBox;
|
||||
QCheckBox *cardScalingCheckBox;
|
||||
QCheckBox *roundCardCornersCheckBox;
|
||||
QCheckBox *displayCardNamesCheckBox;
|
||||
QCheckBox *autoRotateCardsCheckBox;
|
||||
QCheckBox *tapAnimationCheckBox;
|
||||
|
||||
QGroupBox *notificationsGroup;
|
||||
QCheckBox *notificationsEnabledCheckBox;
|
||||
QCheckBox *soundEnabledCheckBox;
|
||||
|
||||
QGroupBox *gameplayGroup;
|
||||
QCheckBox *doubleClickToPlayCheckBox;
|
||||
QCheckBox *horizontalHandCheckBox;
|
||||
QCheckBox *playToStackCheckBox;
|
||||
|
||||
QGroupBox *menuGroup;
|
||||
QCheckBox *showShortcutsCheckBox;
|
||||
|
||||
QGroupBox *dataGroup;
|
||||
QCheckBox *picDownloadCheckBox;
|
||||
QCheckBox *checkUpdatesOnStartupCheckBox;
|
||||
QCheckBox *showTipsOnStartupCheckBox;
|
||||
};
|
||||
|
||||
#endif // PREFERENCES_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
#include "theme_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../interface/palette_editor/palette_generator.h"
|
||||
#include "../../interface/palette_editor/quick_setup_panel.h"
|
||||
#include "../../interface/theme_manager.h"
|
||||
#include "../../interface/widgets/general/background_sources.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
themeCombo = new QComboBox(this);
|
||||
schemeCombo = new QComboBox(this);
|
||||
schemeCombo->addItem(tr("Light"), QStringLiteral("Light"));
|
||||
schemeCombo->addItem(tr("Dark"), QStringLiteral("Dark"));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
schemeCombo->addItem(tr("Match system"), QStringLiteral("System"));
|
||||
#endif
|
||||
|
||||
quickSetupPanel = new QuickSetupPanel(this);
|
||||
|
||||
connect(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
|
||||
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
|
||||
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
|
||||
|
||||
homeTabBackgroundCombo = new QComboBox(this);
|
||||
for (const auto &entry : BackgroundSources::all()) {
|
||||
homeTabBackgroundCombo->addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type));
|
||||
}
|
||||
connect(homeTabBackgroundCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&ThemeSetupPage::onHomeTabBackgroundChanged);
|
||||
|
||||
// Keep the scheme combo honest when the *theme* changes underneath it
|
||||
// (switching theme reloads that theme's own stored colorScheme), and
|
||||
// opportunistically seed a palette for themes that ship none at all.
|
||||
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
|
||||
// half of this.
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
|
||||
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
||||
const QString current = cfg.colorScheme;
|
||||
|
||||
schemeCombo->blockSignals(true);
|
||||
const int idx = schemeCombo->findData(current);
|
||||
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
schemeCombo->blockSignals(false);
|
||||
|
||||
maybeAutoGeneratePalette();
|
||||
});
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
form->addRow(tr("Theme:"), themeCombo);
|
||||
form->addRow(tr("Appearance:"), schemeCombo);
|
||||
form->addRow(tr("Home screen background:"), homeTabBackgroundCombo);
|
||||
|
||||
accentGroup = new QGroupBox(this);
|
||||
auto *accentLayout = new QVBoxLayout(accentGroup);
|
||||
accentLayout->addWidget(quickSetupPanel);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(accentGroup);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void ThemeSetupPage::initializePage()
|
||||
{
|
||||
themeCombo->blockSignals(true);
|
||||
themeCombo->clear();
|
||||
const QString currentTheme = SettingsCache::instance().getThemeName();
|
||||
for (const QString &name : themeManager->getAvailableThemes().keys()) {
|
||||
themeCombo->addItem(name);
|
||||
}
|
||||
const int idx = themeCombo->findText(currentTheme);
|
||||
themeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||
themeCombo->blockSignals(false);
|
||||
|
||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
||||
schemeCombo->blockSignals(true);
|
||||
const int schemeIdx = schemeCombo->findData(cfg.colorScheme);
|
||||
schemeCombo->setCurrentIndex(schemeIdx >= 0 ? schemeIdx : 0);
|
||||
schemeCombo->blockSignals(false);
|
||||
|
||||
homeTabBackgroundCombo->blockSignals(true);
|
||||
QString homeTabSource = SettingsCache::instance().personal().getHomeTabBackgroundSource();
|
||||
int homeTabIdx = homeTabBackgroundCombo->findData(BackgroundSources::fromId(homeTabSource));
|
||||
homeTabBackgroundCombo->setCurrentIndex(homeTabIdx >= 0 ? homeTabIdx : 0);
|
||||
homeTabBackgroundCombo->blockSignals(false);
|
||||
|
||||
// Opening the page must not touch the running application's palette:
|
||||
// previews and auto-generation only happen in response to the user
|
||||
// actually changing a control, never on mere page visibility.
|
||||
paletteDirty = false;
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::currentScheme() const
|
||||
{
|
||||
return schemeCombo->currentData().toString();
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::resolvedScheme() const
|
||||
{
|
||||
const QString scheme = currentScheme();
|
||||
if (scheme.isEmpty() || scheme == QStringLiteral("System")) {
|
||||
return themeManager->isDarkMode(themeManager->getCurrentThemePath()) ? "Dark" : "Light";
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onThemeChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
paletteDirty = false;
|
||||
SettingsCache::instance().setThemeName(themeCombo->itemText(index));
|
||||
// Scheme-combo sync and auto-generation both happen via the
|
||||
// ThemeManager::themeChanged listener above, triggered by setThemeName.
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onSchemeChanged()
|
||||
{
|
||||
themeManager->setColorScheme(currentScheme());
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onHomeTabBackgroundChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
auto type = homeTabBackgroundCombo->currentData().value<BackgroundSources::Type>();
|
||||
SettingsCache::instance().personal().setHomeTabBackgroundSource(BackgroundSources::toId(type));
|
||||
}
|
||||
|
||||
void ThemeSetupPage::onGenerateFromAccent(const QColor &accent, int intensity)
|
||||
{
|
||||
PaletteConfig cfg = PaletteGenerator::fromAccent(accent, intensity, resolvedScheme());
|
||||
themeManager->previewPalette(cfg, resolvedScheme());
|
||||
paletteDirty = true;
|
||||
}
|
||||
|
||||
void ThemeSetupPage::maybeAutoGeneratePalette()
|
||||
{
|
||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
const QString scheme = resolvedScheme();
|
||||
|
||||
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
|
||||
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
|
||||
return; // theme already has something real to show -- leave it alone
|
||||
}
|
||||
|
||||
// The theme+scheme combination has nothing saved and nothing shipped, and
|
||||
// the user just switched to it. Rather than leaving a flat, unstyled look,
|
||||
// seed one from whatever accent QuickSetupPanel currently holds and mark
|
||||
// it dirty so it's written to disk if the user moves on. Only ever reached
|
||||
// through user interaction (theme/scheme change, accent drag) -- never on
|
||||
// page open.
|
||||
PaletteConfig generated =
|
||||
PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme);
|
||||
themeManager->previewPalette(generated, scheme);
|
||||
paletteDirty = true;
|
||||
}
|
||||
|
||||
bool ThemeSetupPage::validatePage()
|
||||
{
|
||||
if (paletteDirty) {
|
||||
const QString scheme = resolvedScheme();
|
||||
PaletteConfig cfg =
|
||||
PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme);
|
||||
if (!ThemeManager::commitPalette(writableThemeDir(), scheme, cfg)) {
|
||||
QMessageBox::warning(this, tr("Save failed"),
|
||||
tr("Could not write the theme palette to:\n%1").arg(writableThemeDir()));
|
||||
return false;
|
||||
}
|
||||
themeManager->reloadCurrentTheme();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::writableThemeDir() const
|
||||
{
|
||||
// Built-in themes resolve to the read-only system themes directory;
|
||||
// palette edits must go to the user themes directory instead, exactly
|
||||
// as PaletteEditorDialog does.
|
||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||
if (!dirPath.isEmpty()) {
|
||||
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
|
||||
QFile f(probe);
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.close();
|
||||
f.remove();
|
||||
return dirPath;
|
||||
}
|
||||
}
|
||||
return QDir(SettingsCache::instance().paths().getThemesPath())
|
||||
.absoluteFilePath(SettingsCache::instance().getThemeName());
|
||||
}
|
||||
|
||||
bool ThemeSetupPage::isSkippable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::stepTitle() const
|
||||
{
|
||||
return tr("Pick a Look");
|
||||
}
|
||||
|
||||
QString ThemeSetupPage::stepSubtitle() const
|
||||
{
|
||||
return tr("You can fine-tune every colour later from Settings → Appearance.");
|
||||
}
|
||||
|
||||
void ThemeSetupPage::retranslateUi()
|
||||
{
|
||||
accentGroup->setTitle(tr("Accent colour (optional)"));
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#ifndef THEME_SETUP_PAGE_H
|
||||
#define THEME_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QuickSetupPanel;
|
||||
|
||||
/** @brief First-run theme step. Reuses the same building blocks as Appearance
|
||||
* settings and the Palette Editor (ThemeManager, PaletteConfig,
|
||||
* PaletteGenerator, and the QuickSetupPanel widget itself) rather than
|
||||
* reimplementing palette generation or preview here.
|
||||
*
|
||||
* Behavior specific to this page (deliberately not pushed down into
|
||||
* ThemeManager, to avoid changing app-wide behaviour for existing installs):
|
||||
* - Opening the page never changes the running palette; previews and
|
||||
* auto-generation only happen when the user actually changes a control.
|
||||
* - If a theme+scheme the user selects has no saved palette and no shipped
|
||||
* default, one is generated from the QuickSetupPanel's current accent so
|
||||
* the preview doesn't fall back to a flat, unstyled look. */
|
||||
class ThemeSetupPage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ThemeSetupPage(QWidget *parent = nullptr);
|
||||
|
||||
void initializePage() override;
|
||||
bool validatePage() override;
|
||||
bool isSkippable() const override;
|
||||
QString stepTitle() const override;
|
||||
QString stepSubtitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private slots:
|
||||
void onThemeChanged(int index);
|
||||
void onSchemeChanged();
|
||||
void onGenerateFromAccent(const QColor &accent, int intensity);
|
||||
void onHomeTabBackgroundChanged(int index);
|
||||
|
||||
private:
|
||||
QString currentScheme() const;
|
||||
QString resolvedScheme() const; // "System" -> actual Light/Dark
|
||||
void maybeAutoGeneratePalette();
|
||||
QString writableThemeDir() const;
|
||||
|
||||
QComboBox *themeCombo;
|
||||
QComboBox *schemeCombo;
|
||||
QGroupBox *accentGroup;
|
||||
QuickSetupPanel *quickSetupPanel;
|
||||
|
||||
QComboBox *homeTabBackgroundCombo;
|
||||
|
||||
bool paletteDirty = false;
|
||||
};
|
||||
|
||||
#endif // THEME_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#include "welcome_page.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
WelcomePage::WelcomePage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addStretch();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
QString WelcomePage::stepTitle() const
|
||||
{
|
||||
return tr("Welcome!");
|
||||
}
|
||||
|
||||
void WelcomePage::retranslateUi()
|
||||
{
|
||||
bodyLabel->setText(tr("Let's get you set up. This will only take a minute — "
|
||||
"we'll grab the card database, pick a look you like, "
|
||||
"and get you ready to connect to a server.\n\n"
|
||||
"You can change any of this later from Settings."));
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef WELCOME_PAGE_H
|
||||
#define WELCOME_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QLabel;
|
||||
|
||||
class WelcomePage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WelcomePage(QWidget *parent = nullptr);
|
||||
|
||||
QString stepTitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
};
|
||||
|
||||
#endif // WELCOME_PAGE_H
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ShaderEffect {
|
||||
id: effectA
|
||||
anchors.fill: parent
|
||||
opacity: bannerConfig.frontIsA ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 450; easing.type: Easing.InOutCubic } }
|
||||
property real iTime: bannerConfig.time
|
||||
property real uAspect: bannerConfig.aspect
|
||||
property real uMode: bannerConfig.modeA
|
||||
property real uSpeed: bannerConfig.speedA
|
||||
property real uSeed: bannerConfig.seedA
|
||||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||
property real uLogoGlow: bannerConfig.logoGlow
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
id: effectB
|
||||
anchors.fill: parent
|
||||
opacity: bannerConfig.frontIsA ? 0.0 : 1.0
|
||||
Behavior on opacity { NumberAnimation { duration: 450; easing.type: Easing.InOutCubic } }
|
||||
property real iTime: bannerConfig.time
|
||||
property real uAspect: bannerConfig.aspect
|
||||
property real uMode: bannerConfig.modeB
|
||||
property real uSpeed: bannerConfig.speedB
|
||||
property real uSeed: bannerConfig.seedB
|
||||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||
property real uLogoGlow: bannerConfig.logoGlow
|
||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||
}
|
||||
|
||||
// The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range
|
||||
Image {
|
||||
id: logo
|
||||
anchors.centerIn: parent
|
||||
visible: bannerConfig.logoVisible
|
||||
source: "qrc:/resources/cockatrice-logo-white.svg"
|
||||
width: root.height * 0.6
|
||||
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
|
||||
sourceSize: Qt.size(256, 256)
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } }
|
||||
|
||||
transform: Scale {
|
||||
origin.x: logo.width / 2
|
||||
origin.y: logo.height / 2
|
||||
xScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
||||
yScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
#include "shader_banner_widget.h"
|
||||
|
||||
#include "banner_shader_config.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickWidget>
|
||||
#include <QResizeEvent>
|
||||
#include <QStackedLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Near-black base palette -- the background is dark and quiet so the green
|
||||
// accent stands out.
|
||||
constexpr QRgb kColorA = 0x1A1A20;
|
||||
constexpr QRgb kColorB = 0x0E0E12;
|
||||
constexpr QRgb kAccent = 0x8BDD6B;
|
||||
} // namespace
|
||||
|
||||
class GradientFallbackWidget : public QWidget
|
||||
{
|
||||
public:
|
||||
using QWidget::QWidget;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
QLinearGradient gradient(0, 0, width(), height());
|
||||
gradient.setColorAt(0.0, QColor(kColorA));
|
||||
gradient.setColorAt(1.0, QColor(kColorB));
|
||||
painter.fillRect(rect(), gradient);
|
||||
}
|
||||
};
|
||||
|
||||
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedHeight(150);
|
||||
|
||||
stack = new QStackedLayout(this);
|
||||
stack->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
fallback = new GradientFallbackWidget(this);
|
||||
stack->addWidget(fallback);
|
||||
|
||||
quickWidget = new QQuickWidget(this);
|
||||
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
config = new BannerShaderConfig(quickWidget->engine());
|
||||
quickWidget->rootContext()->setContextProperty("bannerConfig", config);
|
||||
quickWidget->setSource(QUrl("qrc:/onboarding/qml/BrandBanner.qml"));
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
activateFallback();
|
||||
} else {
|
||||
connect(quickWidget, &QQuickWidget::sceneGraphError, this, &BannerHost::onSceneGraphFailed);
|
||||
stack->addWidget(quickWidget);
|
||||
stack->setCurrentWidget(quickWidget);
|
||||
}
|
||||
|
||||
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
|
||||
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
|
||||
|
||||
applyMotifPreset(currentMotif);
|
||||
updateAspect();
|
||||
}
|
||||
|
||||
void BannerHost::activateFallback()
|
||||
{
|
||||
if (usingFallback) {
|
||||
return;
|
||||
}
|
||||
usingFallback = true;
|
||||
clock.stop();
|
||||
stack->setCurrentWidget(fallback);
|
||||
|
||||
if (quickWidget) {
|
||||
quickWidget->deleteLater(); // takes BannerShaderConfig (parented to its engine) with it
|
||||
quickWidget = nullptr;
|
||||
config = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::onSceneGraphFailed()
|
||||
{
|
||||
activateFallback();
|
||||
}
|
||||
|
||||
void BannerHost::setMotif(Motif motif)
|
||||
{
|
||||
currentMotif = motif;
|
||||
applyMotifPreset(motif);
|
||||
}
|
||||
|
||||
BannerHost::Preset BannerHost::presetFor(Motif motif)
|
||||
{
|
||||
// speed/seed tuned per motif so e.g. the network "pulse" (Account) reads
|
||||
// at a deliberately calmer cadence than the data "scan" lines
|
||||
// (Preferences), even though both come from the same shader.
|
||||
switch (motif) {
|
||||
case Motif::Welcome:
|
||||
return {0.0, 0.6, 0.15};
|
||||
case Motif::CardDatabase:
|
||||
return {1.0, 1.3, 0.42};
|
||||
case Motif::Theming:
|
||||
return {2.0, 1.2, 0.73};
|
||||
case Motif::Account:
|
||||
return {3.0, 0.8, 0.28};
|
||||
case Motif::Preferences:
|
||||
return {4.0, 1.0, 0.61};
|
||||
case Motif::Finish:
|
||||
return {5.0, 1.0, 0.91};
|
||||
}
|
||||
return {0.0, 0.6, 0.15};
|
||||
}
|
||||
|
||||
void BannerHost::applyMotifPreset(Motif motif)
|
||||
{
|
||||
if (usingFallback || !config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Preset p = presetFor(motif);
|
||||
|
||||
config->setColorA(QColor(kColorA));
|
||||
config->setColorB(QColor(kColorB));
|
||||
config->setAccent(QColor(kAccent));
|
||||
config->setLogoVisible(motif == Motif::Welcome);
|
||||
|
||||
if (isFirstApply) {
|
||||
// Nothing on screen yet -- write straight into the front bank, no
|
||||
// crossfade needed for the very first paint.
|
||||
config->setModeA(p.mode);
|
||||
config->setSpeedA(p.speed);
|
||||
config->setSeedA(p.seed);
|
||||
config->setFrontIsA(true);
|
||||
isFirstApply = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the new preset into whichever bank is currently hidden, then
|
||||
// flip which one is front. QML's opacity Behavior does the actual
|
||||
// crossfade -- BannerHost never animates anything itself.
|
||||
if (config->frontIsA()) {
|
||||
config->setModeB(p.mode);
|
||||
config->setSpeedB(p.speed);
|
||||
config->setSeedB(p.seed);
|
||||
config->setFrontIsA(false);
|
||||
} else {
|
||||
config->setModeA(p.mode);
|
||||
config->setSpeedA(p.speed);
|
||||
config->setSeedA(p.seed);
|
||||
config->setFrontIsA(true);
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::updateAspect()
|
||||
{
|
||||
if (config && height() > 0) {
|
||||
config->setAspect(qreal(width()) / qreal(height()));
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateAspect();
|
||||
}
|
||||
|
||||
void BannerHost::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
if (!usingFallback) {
|
||||
elapsed.restart();
|
||||
clock.start();
|
||||
}
|
||||
}
|
||||
|
||||
void BannerHost::hideEvent(QHideEvent *event)
|
||||
{
|
||||
QWidget::hideEvent(event);
|
||||
clock.stop();
|
||||
}
|
||||
|
||||
void BannerHost::tick()
|
||||
{
|
||||
if (config) {
|
||||
qreal t = elapsed.elapsed() / 1000.0;
|
||||
config->setTime(t);
|
||||
// Visible breathing for the logo: oscillates between 0.0 and 1.0
|
||||
qreal glow = 0.5 + 0.5 * qSin(t * 0.4);
|
||||
config->setLogoGlow(glow);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
#ifndef SHADER_BANNER_WIDGET_H
|
||||
#define SHADER_BANNER_WIDGET_H
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
class BannerShaderConfig;
|
||||
class QQuickWidget;
|
||||
class GradientFallbackWidget;
|
||||
class QStackedLayout;
|
||||
|
||||
/** @brief Onboarding banner: a subtle, looping brand-shader animation, one of six
|
||||
* per-page "motifs" driving the same prebaked fragment shader
|
||||
* (onboarding/shaders/brand_banner.frag) with different uniform values, so
|
||||
* every page feels distinct but unmistakably part of the same family.
|
||||
*
|
||||
* Motif switches crossfade smoothly (see BrandBanner.qml's two stacked
|
||||
* ShaderEffect layers + Behavior on opacity) rather than cutting instantly
|
||||
* -- BannerHost just writes the new preset into whichever layer is
|
||||
* currently hidden and flips BannerShaderConfig::frontIsA; QML handles the
|
||||
* actual animation declaratively.
|
||||
*
|
||||
* Falls back to a static two-stop gradient (no shader, no QQuickWidget) if
|
||||
* the platform's Qt Quick scenegraph can't initialize -- e.g. software
|
||||
* rendering only, or a CI/VM environment with no GPU -- so onboarding
|
||||
* never blocks or blanks out over a graphics driver problem. The fallback
|
||||
* is permanent for the lifetime of this widget once triggered. */
|
||||
class BannerHost : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Motif
|
||||
{
|
||||
Welcome,
|
||||
CardDatabase,
|
||||
Theming,
|
||||
Account,
|
||||
Preferences,
|
||||
Finish,
|
||||
};
|
||||
|
||||
explicit BannerHost(QWidget *parent = nullptr);
|
||||
|
||||
void setMotif(Motif motif);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void tick();
|
||||
void onSceneGraphFailed();
|
||||
|
||||
private:
|
||||
struct Preset
|
||||
{
|
||||
qreal mode;
|
||||
qreal speed;
|
||||
qreal seed;
|
||||
};
|
||||
|
||||
static Preset presetFor(Motif motif);
|
||||
|
||||
void applyMotifPreset(Motif motif);
|
||||
void updateAspect();
|
||||
void activateFallback();
|
||||
|
||||
QStackedLayout *stack;
|
||||
QQuickWidget *quickWidget = nullptr;
|
||||
BannerShaderConfig *config = nullptr;
|
||||
GradientFallbackWidget *fallback = nullptr;
|
||||
|
||||
QTimer clock;
|
||||
QElapsedTimer elapsed;
|
||||
Motif currentMotif = Motif::Welcome;
|
||||
bool usingFallback = false;
|
||||
bool isFirstApply = true;
|
||||
};
|
||||
|
||||
#endif // SHADER_BANNER_WIDGET_H
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
#version 440
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// brand_banner.frag
|
||||
//
|
||||
// One shader, six motifs (uMode 0..5). All motifs composite over a shared
|
||||
// backgroundField() whose colour is flow-noise-modulated blend of uColorA
|
||||
// and uColorB. SDFs operate in aspect-corrected space (ac.x = uv.x *
|
||||
// uAspect) to preserve shape proportions on the wide banner.
|
||||
//
|
||||
// IMPORTANT: the uniform block below must list custom uniforms in EXACTLY
|
||||
// the order they're declared as properties on each ShaderEffect instance in
|
||||
// BrandBanner.qml (after the two Qt-supplied members, qt_Matrix/qt_Opacity).
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf
|
||||
{
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float iTime;
|
||||
float uAspect;
|
||||
float uMode;
|
||||
float uSpeed;
|
||||
float uSeed;
|
||||
vec4 uColorA;
|
||||
vec4 uColorB;
|
||||
vec4 uAccent;
|
||||
float uLogoGlow;
|
||||
};
|
||||
|
||||
// ── Primitives ──────────────────────────────────────────────────────────
|
||||
|
||||
float hash21(vec2 p)
|
||||
{
|
||||
p = fract(p * vec2(123.34, 456.21));
|
||||
p += dot(p, p + 45.32);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
float valueNoise(vec2 p)
|
||||
{
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
float a = hash21(i);
|
||||
float b = hash21(i + vec2(1.0, 0.0));
|
||||
float c = hash21(i + vec2(0.0, 1.0));
|
||||
float d = hash21(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p)
|
||||
{
|
||||
float v = 0.0;
|
||||
float amp = 0.5;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
v += amp * valueNoise(p);
|
||||
p *= 2.03;
|
||||
amp *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
float flowNoise(vec2 p, float t)
|
||||
{
|
||||
vec2 warp1 = vec2(fbm(p + vec2(0.0, 0.0)), fbm(p + vec2(5.2, 1.3)));
|
||||
vec2 warp2 = vec2(fbm(p + 4.0 * warp1 + vec2(1.7, 9.2) + t * 0.6),
|
||||
fbm(p + 4.0 * warp1 + vec2(8.3, 2.8) - t * 0.5));
|
||||
return fbm(p + 4.0 * warp2 + t * 0.15);
|
||||
}
|
||||
|
||||
float bloom(float d, float coreRadius, float haloRadius)
|
||||
{
|
||||
float core = exp(-(d * d) / (coreRadius * coreRadius));
|
||||
float halo = exp(-d / haloRadius) * 0.35;
|
||||
return core + halo;
|
||||
}
|
||||
|
||||
float roundedBoxSDF(vec2 p, vec2 halfSize, float radius)
|
||||
{
|
||||
vec2 d = abs(p) - halfSize + radius;
|
||||
return length(max(d, 0.0)) - radius + min(max(d.x, d.y), 0.0);
|
||||
}
|
||||
|
||||
// Rotated box SDF -- applies 2D rotation to p before evaluating roundedBoxSDF.
|
||||
float rotatedBoxSDF(vec2 p, vec2 halfSize, float radius, float angle)
|
||||
{
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
vec2 rp = vec2(p.x * c - p.y * s, p.x * s + p.y * c);
|
||||
return roundedBoxSDF(rp, halfSize, radius);
|
||||
}
|
||||
|
||||
float vignette(vec2 uv)
|
||||
{
|
||||
vec2 c = uv - 0.5;
|
||||
c.x *= max(uAspect, 0.0001);
|
||||
return smoothstep(1.0, 0.25, length(c));
|
||||
}
|
||||
|
||||
// ── Shared background ───────────────────────────────────────────────────
|
||||
|
||||
vec3 backgroundField(vec2 uv, float time)
|
||||
{
|
||||
// Diagonal luminance gradient from (0,0) to (1,1) used as blend factor
|
||||
// between uColorA and uColorB; modulated by flowNoise.
|
||||
float baseD = smoothstep(0.0, 1.0, uv.y * 0.5 + uv.x * 0.2);
|
||||
float painted = flowNoise(uv * 1.5, time * 0.04) - 0.5;
|
||||
baseD = clamp(baseD + painted * 0.12, 0.0, 1.0);
|
||||
|
||||
vec3 col = mix(uColorA.rgb, uColorB.rgb, baseD);
|
||||
|
||||
// Low-frequency fBM noise pushes local colour toward uColorB for depth
|
||||
float deep = fbm(uv * 1.0 + vec2(37.1, 12.4) + time * 0.015);
|
||||
col = mix(col, uColorB.rgb, (deep - 0.5) * 0.08);
|
||||
|
||||
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
|
||||
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
|
||||
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// ── Motifs ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Centre bloom, flow-noise shimmer gated to centre, and 48 orbiting ember
|
||||
// particles that deflect into a tight ring near the centre.
|
||||
vec3 motifWelcome(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
float cDist = length(ac - center);
|
||||
|
||||
// Centre bloom at logo position; intensity scales with uLogoGlow
|
||||
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
|
||||
col += centreLight * 0.20 * uLogoGlow;
|
||||
|
||||
// Flow-noise shimmer gated by Gaussian mask at centre
|
||||
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
|
||||
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
|
||||
col += shimmer * shimmerMask * 0.04 * uLogoGlow;
|
||||
|
||||
// 48 ember particles: hash-seeded position, speed, size, brightness.
|
||||
// Embers within a distance threshold of centre are deflected into an
|
||||
// orbital ring via tangent displacement perpendicular to the centre vector.
|
||||
const int EMBERS = 48;
|
||||
for (int i = 0; i < EMBERS; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
float baseX = hash21(vec2(fi * 7.31 + uSeed, fi * 3.17));
|
||||
float baseY = hash21(vec2(fi * 11.9 + uSeed * 1.4, fi * 5.53));
|
||||
|
||||
float riseSpeed = 0.025 + hash21(vec2(fi * 1.7, uSeed * 2.1)) * 0.035;
|
||||
float driftAmp = 0.04 + hash21(vec2(fi * 9.3, uSeed)) * 0.06;
|
||||
float driftFreq = 0.3 + hash21(vec2(fi * 4.1, uSeed * 3.3)) * 0.5;
|
||||
|
||||
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
|
||||
float pY = fract(baseY + t * riseSpeed);
|
||||
|
||||
float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
|
||||
float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
|
||||
|
||||
// Fade out near top/bottom edges
|
||||
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
|
||||
float twinkle = 0.6 + 0.4 * sin(t * (1.2 + fi * 0.37) + fi * 2.9);
|
||||
|
||||
vec2 ePos = vec2(pX, pY);
|
||||
|
||||
// Embers near centre: deflect into orbital ring via tangent displacement
|
||||
vec2 toCenter = ePos - center;
|
||||
float distToCenter = length(toCenter);
|
||||
float ringWeight = smoothstep(0.38 * asp, 0.06 * asp, distToCenter);
|
||||
|
||||
float orbitPhase = t * (0.15 + fi * 0.020) + fi * 2.3;
|
||||
float orbitAmount = 0.020 + hash21(vec2(fi * 12.3, uSeed * 2.7)) * 0.020;
|
||||
vec2 tangent = vec2(-toCenter.y, toCenter.x);
|
||||
vec2 deflected = ePos + tangent * ringWeight * orbitAmount * asp * sin(orbitPhase);
|
||||
|
||||
float pushOut = ringWeight * (0.008 + hash21(vec2(fi * 6.7, uSeed * 1.1)) * 0.012) * asp;
|
||||
deflected += normalize(toCenter + 0.001) * pushOut;
|
||||
|
||||
float dist = length(ac - deflected);
|
||||
float intensity = bright * edgeFade * twinkle;
|
||||
col += uAccent.rgb * bloom(dist, size, size * 4.0) * intensity;
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// 25 card-shaped box SDFs at parallax depths drifting horizontally across
|
||||
// the banner; each card has a semi-transparent fill, accent outline, and
|
||||
// card-back diamond pattern.
|
||||
vec3 motifCardDatabase(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
|
||||
const int CARDS = 25;
|
||||
for (int i = 0; i < CARDS; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
// Parallax depth via hash; used to scale size, speed, brightness
|
||||
float depth = hash21(vec2(fi * 1.37 + uSeed, fi * 0.91));
|
||||
|
||||
// Card dimensions in corrected space (portrait: height > width)
|
||||
float cardH = mix(0.055, 0.15, depth);
|
||||
cardH *= 0.85 + 0.30 * hash21(vec2(fi * 3.14, uSeed * 2.71));
|
||||
float cardW = cardH * 0.71; // 5:7 ratio
|
||||
|
||||
// Horizontal drift; nearer cards (higher depth) move faster
|
||||
float speed = mix(0.06, 0.18, depth);
|
||||
float xPhase = hash21(vec2(fi * 7.13, uSeed * 4.37));
|
||||
xPhase = fract(xPhase + t * speed);
|
||||
float x = mix(-1.5, asp + 1.5, xPhase);
|
||||
|
||||
// Vertical position: hash distribution with sinusoidal oscillation
|
||||
float yBase = hash21(vec2(fi * 2.91, uSeed * 1.63));
|
||||
float y = yBase + sin(t * 0.6 + fi * 1.9) * 0.035;
|
||||
y = clamp(y, cardH + 0.02, 1.0 - cardH - 0.02);
|
||||
|
||||
// Random rotation angle ±4 degrees
|
||||
float tilt = (hash21(vec2(fi * 5.71, uSeed * 8.29)) - 0.5) * 0.14;
|
||||
|
||||
vec2 p = ac - vec2(x, y);
|
||||
float d = rotatedBoxSDF(p, vec2(cardW, cardH), cardW * 0.14, tilt);
|
||||
|
||||
// Semi-transparent dark fill
|
||||
float fill = smoothstep(0.015, -0.005, d);
|
||||
col = mix(col, uColorB.rgb * 0.55, fill * 0.50);
|
||||
|
||||
// Accent outline
|
||||
float edge = smoothstep(0.035, 0.0, abs(d));
|
||||
col += uAccent.rgb * edge * mix(0.18, 0.50, 1.0 - depth);
|
||||
|
||||
// Card-back diamond: smaller rotated box inset from card edges
|
||||
float innerD = rotatedBoxSDF(p, vec2(cardW * 0.45, cardH * 0.55), cardW * 0.08, tilt);
|
||||
float innerEdge = smoothstep(0.012, 0.0, abs(innerD));
|
||||
col += uAccent.rgb * innerEdge * fill * 0.12 * (1.0 - depth);
|
||||
|
||||
// Centre dot
|
||||
float dotDist = length(p);
|
||||
col += uAccent.rgb * bloom(dotDist, 0.008, 0.02) * fill * 0.15 * (1.0 - depth);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
// 4 horizontal bands with multi-frequency sinusoidal warp and pulsing width.
|
||||
vec3 motifTheming(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
|
||||
const int BANDS = 4;
|
||||
for (int i = 0; i < BANDS; i++) {
|
||||
float fi = float(i);
|
||||
float yCenter = 0.18 + fi * 0.22;
|
||||
|
||||
// Three summed sinusoids for horizontal undulation
|
||||
float wave = sin(uv.x * 3.2 + t * 0.5 + fi * 2.1) * 0.08;
|
||||
wave += sin(uv.x * 7.0 - t * 0.3 + fi * 1.3) * 0.035;
|
||||
wave += sin(uv.x * 1.6 + t * 0.18 + fi * 3.7) * 0.05;
|
||||
|
||||
float bandDist = abs(uv.y - yCenter - wave);
|
||||
float bandWidth = 0.04 + sin(t * 0.2 + fi * 0.8) * 0.012;
|
||||
float band = smoothstep(bandWidth, 0.0, bandDist);
|
||||
|
||||
// Upper bands have higher intensity
|
||||
float intensity = mix(0.15, 0.38, 1.0 - fi / float(BANDS));
|
||||
col += uAccent.rgb * band * intensity;
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// 14 nodes at pseudo-random positions with sinusoidal pulse; edges drawn
|
||||
// between nodes within a threshold distance; central glow + periodic ring.
|
||||
vec3 motifAccount(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
|
||||
const int NODES = 14;
|
||||
vec2 nodePos[14];
|
||||
float nodePulse[14];
|
||||
|
||||
for (int i = 0; i < NODES; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
// Hash-seeded position with gentle sinusoidal drift
|
||||
float nx = hash21(vec2(fi * 3.17 + uSeed, fi * 1.93)) * asp;
|
||||
float ny = hash21(vec2(fi * 5.41 + uSeed * 1.7, fi * 2.79));
|
||||
|
||||
float dx = sin(t * 0.12 + fi * 1.7) * 0.08;
|
||||
float dy = cos(t * 0.09 + fi * 2.3) * 0.04;
|
||||
vec2 pos = vec2(nx + dx, ny + dy);
|
||||
nodePos[i] = pos;
|
||||
|
||||
// Per-node pulse phase, normalised to [0, 1]
|
||||
float pulsePhase = hash21(vec2(fi * 4.31, uSeed * 6.17));
|
||||
float pulse = sin(t * 0.8 + pulsePhase * 6.283) * 0.5 + 0.5;
|
||||
nodePulse[i] = pulse;
|
||||
|
||||
// Node glow via bloom; intensity modulated by pulse
|
||||
float dist = length(ac - pos);
|
||||
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse);
|
||||
}
|
||||
|
||||
// Edges: connect nodes within a radius threshold
|
||||
float connectDist = asp * 0.22;
|
||||
for (int i = 0; i < NODES; i++) {
|
||||
for (int j = i + 1; j < NODES; j++) {
|
||||
float pairDist = length(nodePos[i] - nodePos[j]);
|
||||
if (pairDist < connectDist) {
|
||||
float strength = 1.0 - pairDist / connectDist;
|
||||
vec2 pa = ac - nodePos[i];
|
||||
vec2 ba = nodePos[j] - nodePos[i];
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
float lineDist = length(pa - ba * h);
|
||||
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Central bloom at banner centre
|
||||
float cDist = length(ac - center);
|
||||
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12;
|
||||
|
||||
// Periodic expanding ring from centre
|
||||
float ripplePhase = t * 0.4;
|
||||
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
|
||||
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// 18x5 toggle-grid of rounded boxes with hash-driven on/off per cell;
|
||||
// a scanning highlight sweeps L-to-R, brightening cells near the scan line.
|
||||
vec3 motifPreferences(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
|
||||
float cols = 18.0;
|
||||
float rows = 5.0;
|
||||
vec2 gridUV = uv * vec2(cols, rows);
|
||||
vec2 cell = fract(gridUV) - 0.5;
|
||||
vec2 cellId = floor(gridUV);
|
||||
|
||||
// On/off state per cell, hash-seeded for pseudo-randomness
|
||||
float on = step(0.55, hash21(cellId + uSeed * 10.0));
|
||||
|
||||
float d = roundedBoxSDF(cell, vec2(0.28, 0.32), 0.06);
|
||||
|
||||
// Filled "on" cells
|
||||
float cellFill = smoothstep(0.04, -0.02, d);
|
||||
col += uAccent.rgb * cellFill * on * 0.18;
|
||||
|
||||
// Cell borders (drawn on all cells)
|
||||
float border = smoothstep(0.025, 0.0, abs(d));
|
||||
col += uAccent.rgb * border * 0.06;
|
||||
|
||||
// Scanning highlight: thin line + soft glow sweeping L-to-R
|
||||
float scanX = fract(t * 0.15);
|
||||
float scanDist = abs(uv.x - scanX);
|
||||
float scanLine = smoothstep(0.015, 0.0, scanDist);
|
||||
col += uAccent.rgb * scanLine * 0.40;
|
||||
|
||||
float scanGlow = smoothstep(0.08, 0.0, scanDist);
|
||||
col += uAccent.rgb * scanGlow * 0.08;
|
||||
|
||||
// "On" cells near the scan line get extra brightness
|
||||
float scanProximity = smoothstep(0.12, 0.0, scanDist);
|
||||
col += uAccent.rgb * cellFill * on * scanProximity * 0.15;
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// Centre radial bloom with sinusoidal pulse, 4 expanding ring halos with
|
||||
// outer glow falloff, and 35 rising particles.
|
||||
vec3 motifFinish(vec2 uv, vec3 bg, float t)
|
||||
{
|
||||
vec3 col = bg;
|
||||
float asp = max(uAspect, 0.001);
|
||||
vec2 ac = vec2(uv.x * asp, uv.y);
|
||||
vec2 center = vec2(asp * 0.5, 0.5);
|
||||
float cDist = length(ac - center);
|
||||
|
||||
// Centre bloom with sinusoidal pulse modulation
|
||||
float pulse = 0.65 + 0.35 * sin(t * 0.4);
|
||||
col += uAccent.rgb * bloom(cDist, 0.12, 0.55) * 0.10 * pulse;
|
||||
|
||||
// 4 expanding rings: radius increases via phase; ring width grows with
|
||||
// expansion; combined with exponential outer glow falloff
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float fi = float(i);
|
||||
float phase = fract(t * 0.06 + fi * 0.25);
|
||||
float ringRadius = phase * asp * 0.7;
|
||||
float ringDist = abs(cDist - ringRadius);
|
||||
float ringWidth = 0.025 + phase * 0.025;
|
||||
float ring = smoothstep(ringWidth, 0.0, ringDist);
|
||||
float outerGlow = exp(-ringDist / (0.03 + phase * 0.02)) * 0.3;
|
||||
float combined = ring + outerGlow;
|
||||
float fade = 1.0 - phase * 0.5;
|
||||
col += uAccent.rgb * combined * fade * 0.15;
|
||||
}
|
||||
|
||||
// 35 particles rising vertically with sinusoidal horizontal drift;
|
||||
// each particle uses bloom with edge fade and twinkle animation
|
||||
const int PARTICLES = 35;
|
||||
for (int i = 0; i < PARTICLES; i++) {
|
||||
float fi = float(i);
|
||||
float baseX = hash21(vec2(fi * 13.7 + uSeed, fi * 7.31));
|
||||
float baseY = hash21(vec2(fi * 23.1 + uSeed * 1.9, fi * 11.3));
|
||||
|
||||
float riseSpeed = 0.04 + hash21(vec2(fi * 3.1, uSeed * 2.7)) * 0.06;
|
||||
float driftAmp = 0.03 + hash21(vec2(fi * 8.9, uSeed)) * 0.05;
|
||||
float driftFreq = 0.4 + hash21(vec2(fi * 5.3, uSeed * 4.1)) * 0.6;
|
||||
|
||||
float pX = baseX * asp + sin(t * driftFreq + fi * 2.3) * driftAmp * asp;
|
||||
float pY = fract(baseY + t * riseSpeed);
|
||||
|
||||
float size = 0.005 + hash21(vec2(fi * 4.7, uSeed * 3.9)) * 0.010;
|
||||
float bright = 0.12 + hash21(vec2(fi * 7.1, uSeed * 1.3)) * 0.25;
|
||||
|
||||
float edgeFade = smoothstep(0.0, 0.1, pY) * smoothstep(1.0, 0.9, pY);
|
||||
float twinkle = 0.5 + 0.5 * sin(t * (1.8 + fi * 0.43) + fi * 3.1);
|
||||
|
||||
vec2 pPos = vec2(pX, pY);
|
||||
float dist = length(ac - pPos);
|
||||
col += uAccent.rgb * bloom(dist, size, size * 3.5) * bright * edgeFade * twinkle;
|
||||
}
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
// ── Main ────────────────────────────────────────────────────────────────
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 uv = qt_TexCoord0;
|
||||
float t = iTime * uSpeed;
|
||||
|
||||
vec3 bg = backgroundField(uv, iTime);
|
||||
|
||||
vec3 col;
|
||||
if (uMode < 0.5) col = motifWelcome(uv, bg, t);
|
||||
else if (uMode < 1.5) col = motifCardDatabase(uv, bg, t);
|
||||
else if (uMode < 2.5) col = motifTheming(uv, bg, t);
|
||||
else if (uMode < 3.5) col = motifAccount(uv, bg, t);
|
||||
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
|
||||
else col = motifFinish(uv, bg, t);
|
||||
|
||||
col *= mix(0.62, 1.0, vignette(uv));
|
||||
fragColor = vec4(col, 1.0) * qt_Opacity;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
#include "step_indicator_widget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
StepIndicatorWidget::StepIndicatorWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setFixedHeight(kDotDiameter + 2 * kVerticalMargin);
|
||||
}
|
||||
|
||||
void StepIndicatorWidget::setStepCount(int count)
|
||||
{
|
||||
stepCount = qMax(0, count);
|
||||
currentStep = qBound(0, currentStep, qMax(0, stepCount - 1));
|
||||
updateGeometry();
|
||||
update();
|
||||
}
|
||||
|
||||
void StepIndicatorWidget::setCurrentStep(int index)
|
||||
{
|
||||
if (stepCount == 0) {
|
||||
return;
|
||||
}
|
||||
currentStep = qBound(0, index, stepCount - 1);
|
||||
update();
|
||||
}
|
||||
|
||||
QSize StepIndicatorWidget::sizeHint() const
|
||||
{
|
||||
return minimumSizeHint();
|
||||
}
|
||||
|
||||
QSize StepIndicatorWidget::minimumSizeHint() const
|
||||
{
|
||||
if (stepCount == 0) {
|
||||
return QSize(0, height());
|
||||
}
|
||||
int width = kActiveDotWidth + (stepCount - 1) * kDotDiameter + (stepCount - 1) * kDotSpacing;
|
||||
return QSize(width, height());
|
||||
}
|
||||
|
||||
void StepIndicatorWidget::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
if (stepCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const QColor activeColor = palette().color(QPalette::Highlight);
|
||||
|
||||
// QPalette::Mid alpha-blended against a dark Window background reads as
|
||||
// near-invisible (Mid is itself a dark grey in dark palettes -- see
|
||||
// PaletteGenerator's satShadeLo/Dark roles). WindowText is guaranteed to
|
||||
// contrast against Window in any theme by definition, so alpha-blending
|
||||
// *that* instead keeps the dots visibly dim-but-present in both light and
|
||||
// dark schemes. Same trick PaletteGenerator uses for placeholder text.
|
||||
QColor inactiveColor = palette().color(QPalette::WindowText);
|
||||
inactiveColor.setAlpha(100);
|
||||
|
||||
int totalWidth = 0;
|
||||
for (int i = 0; i < stepCount; ++i) {
|
||||
totalWidth += (i == currentStep) ? kActiveDotWidth : kDotDiameter;
|
||||
if (i > 0) {
|
||||
totalWidth += kDotSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
int x = (width() - totalWidth) / 2;
|
||||
const int y = height() / 2;
|
||||
|
||||
for (int i = 0; i < stepCount; ++i) {
|
||||
const bool active = (i == currentStep);
|
||||
const int dotWidth = active ? kActiveDotWidth : kDotDiameter;
|
||||
|
||||
QPainterPath path;
|
||||
QRectF rect(x, y - kDotDiameter / 2.0, dotWidth, kDotDiameter);
|
||||
path.addRoundedRect(rect, kDotDiameter / 2.0, kDotDiameter / 2.0);
|
||||
painter.fillPath(path, active ? activeColor : inactiveColor);
|
||||
|
||||
x += dotWidth + kDotSpacing;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef STEP_INDICATOR_WIDGET_H
|
||||
#define STEP_INDICATOR_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
/** @brief Row of dots showing progress through a fixed-length sequence of steps,
|
||||
* in the style of a mobile/OS setup flow. Purely presentational. */
|
||||
class StepIndicatorWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StepIndicatorWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setStepCount(int count);
|
||||
void setCurrentStep(int index);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
int stepCount = 0;
|
||||
int currentStep = 0;
|
||||
|
||||
static constexpr int kDotDiameter = 8;
|
||||
static constexpr int kActiveDotWidth = 22;
|
||||
static constexpr int kDotSpacing = 10;
|
||||
static constexpr int kVerticalMargin = 6;
|
||||
};
|
||||
|
||||
#endif // STEP_INDICATOR_WIDGET_H
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
#include "../interface/widgets/dialogs/dlg_tip_of_the_day.h"
|
||||
#include "../interface/widgets/dialogs/dlg_update.h"
|
||||
#include "../interface/widgets/dialogs/dlg_view_log.h"
|
||||
#include "../interface/widgets/onboarding/first_run_wizard.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../main.h"
|
||||
|
|
@ -337,6 +338,7 @@ void MainWindow::retranslateUi()
|
|||
aStatusBar->setText(tr("Show Status Bar"));
|
||||
aViewLog->setText(tr("View &Debug Log"));
|
||||
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
|
||||
aFirstRunWizard->setText(tr("Re-run Onboarding Wizard..."));
|
||||
|
||||
aShow->setText(tr("Show/Hide"));
|
||||
|
||||
|
|
@ -398,6 +400,8 @@ void MainWindow::createActions()
|
|||
connect(aViewLog, &QAction::triggered, this, &MainWindow::actViewLog);
|
||||
aOpenSettingsFolder = new QAction(this);
|
||||
connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder);
|
||||
aFirstRunWizard = new QAction(this);
|
||||
connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); });
|
||||
|
||||
aShow = new QAction(this);
|
||||
connect(aShow, &QAction::triggered, this, &MainWindow::actShow);
|
||||
|
|
@ -476,6 +480,8 @@ void MainWindow::createMenus()
|
|||
helpMenu->addAction(aStatusBar);
|
||||
helpMenu->addAction(aViewLog);
|
||||
helpMenu->addAction(aOpenSettingsFolder);
|
||||
helpMenu->addSeparator();
|
||||
helpMenu->addAction(aFirstRunWizard);
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
|
|
@ -561,9 +567,10 @@ void MainWindow::startupConfigCheck()
|
|||
// no config found, 99% new clean install
|
||||
qCInfo(WindowMainStartupVersionLog)
|
||||
<< "Startup: old client version empty, assuming first start after clean install";
|
||||
alertForcedOracleRun(VERSION_STRING, false);
|
||||
SettingsCache::instance().downloads().resetToDefaultURLs(); // populate the download urls
|
||||
SettingsCache::instance().personal().setClientVersion(VERSION_STRING);
|
||||
actCheckServerUpdates();
|
||||
runFirstRunWizard();
|
||||
|
||||
if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
|
||||
SettingsCache::instance().updates().setCheckUpdatesOnStartup(false);
|
||||
|
|
@ -643,6 +650,21 @@ void MainWindow::startupConfigCheck()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::runFirstRunWizard()
|
||||
{
|
||||
auto *wizard = new FirstRunWizard(this);
|
||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground);
|
||||
connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates);
|
||||
connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished);
|
||||
connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer);
|
||||
connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer);
|
||||
|
||||
wizard->setModal(true);
|
||||
wizard->show();
|
||||
}
|
||||
|
||||
void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
|
||||
{
|
||||
if (isUpdate) {
|
||||
|
|
@ -934,6 +956,9 @@ void MainWindow::createCardUpdateProcess(bool background)
|
|||
|
||||
void MainWindow::exitCardDatabaseUpdate()
|
||||
{
|
||||
if (!cardUpdateProcess) {
|
||||
return;
|
||||
}
|
||||
cardUpdateProcess->deleteLater();
|
||||
cardUpdateProcess = nullptr;
|
||||
statusBar()->clearMessage();
|
||||
|
|
@ -971,14 +996,17 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err)
|
|||
|
||||
exitCardDatabaseUpdate();
|
||||
QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error:\n%1").arg(error));
|
||||
emit cardDatabaseUpdateFinished(false);
|
||||
}
|
||||
|
||||
void MainWindow::cardUpdateFinished(int, QProcess::ExitStatus exitStatus)
|
||||
void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
|
||||
if (exitStatus == QProcess::NormalExit) {
|
||||
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());
|
||||
}
|
||||
exitCardDatabaseUpdate();
|
||||
emit cardDatabaseUpdateFinished(success);
|
||||
}
|
||||
|
||||
void MainWindow::actCheckServerUpdates()
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ class DlgTipOfTheDay;
|
|||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
signals:
|
||||
/** @brief Emitted after the background card-database update subprocess exits. */
|
||||
void cardDatabaseUpdateFinished(bool success);
|
||||
|
||||
public slots:
|
||||
void actCheckCardUpdates();
|
||||
void actCheckCardUpdatesBackground();
|
||||
|
|
@ -113,6 +117,9 @@ private:
|
|||
|
||||
void createTrayIcon();
|
||||
int getNextCustomSetPrefix(QDir dataDir);
|
||||
|
||||
void runFirstRunWizard();
|
||||
|
||||
inline QString getCardUpdaterBinaryName()
|
||||
{
|
||||
return "oracle";
|
||||
|
|
@ -128,8 +135,8 @@ private:
|
|||
QAction *aConnect, *aDisconnect, *aRegister, *aForgotPassword, *aSinglePlayer, *aWatchReplay, *aFullScreen;
|
||||
QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet,
|
||||
*aReloadCardDatabase;
|
||||
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aStatusBar, *aViewLog,
|
||||
*aOpenSettingsFolder;
|
||||
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar,
|
||||
*aViewLog, *aOpenSettingsFolder;
|
||||
|
||||
TabSupervisor *tabSupervisor;
|
||||
WndSets *wndSets;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user