mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge branch 'master' into tooomm-cache_mgmt
This commit is contained in:
commit
77563586c9
191 changed files with 10775 additions and 3052 deletions
|
|
@ -8,10 +8,13 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
|||
gtest \
|
||||
mariadb-libs \
|
||||
ninja \
|
||||
openssl \
|
||||
protobuf \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
qt6-imageformats \
|
||||
qt6-multimedia \
|
||||
qt6-shadertools \
|
||||
qt6-svg \
|
||||
qt6-tools \
|
||||
qt6-translations \
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
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 \
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
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 \
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ RUN dnf install -y \
|
|||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
rpm-build \
|
||||
xz-devel \
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ RUN dnf install -y \
|
|||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
rpm-build \
|
||||
xz-devel \
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ RUN apt-get update && \
|
|||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-tools-dev \
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
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 \
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
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 \
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ if [[ $MAKE_TEST ]]; then
|
|||
fi
|
||||
if [[ $USE_CCACHE ]]; then
|
||||
flags+=("-DUSE_CCACHE=1")
|
||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||
# consumes a precompiled header, silently recompiling everything on every run.
|
||||
ccache --set-config sloppiness=pch_defines,time_macros
|
||||
if [[ $CCACHE_SIZE ]]; then
|
||||
# note, this setting persists after running the script
|
||||
ccache --max-size "$CCACHE_SIZE"
|
||||
|
|
|
|||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout repository"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: "Initialize CodeQL"
|
||||
uses: github/codeql-action/init@v4
|
||||
|
|
@ -68,7 +68,9 @@ jobs:
|
|||
libprotobuf-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-declarative-dev \
|
||||
qt6-multimedia-dev \
|
||||
qt6-shadertools-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
|
|
|
|||
18
.github/workflows/desktop-build.yml
vendored
18
.github/workflows/desktop-build.yml
vendored
|
|
@ -176,8 +176,12 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
RUN --server --debug --test --ccache "$CCACHE_SIZE" \
|
||||
--cmake-generator "$CMAKE_GENERATOR"
|
||||
args=()
|
||||
[[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE")
|
||||
args+=(--ccache "$CCACHE_SIZE")
|
||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||
|
||||
RUN --server --debug --test "${args[@]}"
|
||||
|
||||
- name: "Build release package"
|
||||
id: build
|
||||
|
|
@ -269,7 +273,7 @@ jobs:
|
|||
override_target: 13
|
||||
package_suffix: "-macOS13_Intel"
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Intel
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
|
|
@ -285,7 +289,7 @@ jobs:
|
|||
override_target: 14
|
||||
package_suffix: "-macOS14"
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
|
|
@ -301,7 +305,7 @@ jobs:
|
|||
override_target: 15
|
||||
package_suffix: "-macOS15"
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Release
|
||||
use_ccache: 1
|
||||
|
|
@ -314,7 +318,7 @@ jobs:
|
|||
ccache_eviction_age: 7d
|
||||
cmake_generator: Ninja
|
||||
qt_version: 6.11.1
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||
soc: Apple
|
||||
type: Debug
|
||||
use_ccache: 1
|
||||
|
|
@ -329,7 +333,7 @@ jobs:
|
|||
make_package: 1
|
||||
package_suffix: "-Win10"
|
||||
qt_version: 6.11.1
|
||||
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' || '' }}
|
||||
|
|
|
|||
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -127,7 +127,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Download digests"
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digest-*
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@
|
|||
# This file sets all the variables shared between the projects
|
||||
# like the installation path, compilation flags etc..
|
||||
|
||||
# cmake 3.16 is required if using qt6
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
# 3.16 required for Qt6 and target_precompile_headers()
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Early detect ccache
|
||||
# Use compiler cache (ccache)
|
||||
option(USE_CCACHE "Cache the build results with ccache" ON)
|
||||
# Treat warnings as errors (Debug builds only)
|
||||
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
|
||||
# Check for translation updates
|
||||
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
|
||||
# Compile servatrice
|
||||
option(WITH_SERVER "build servatrice" OFF)
|
||||
# Compile cockatrice
|
||||
option(WITH_CLIENT "build cockatrice" ON)
|
||||
# Compile oracle
|
||||
option(WITH_ORACLE "build oracle" ON)
|
||||
# Compile Cockatrice
|
||||
option(WITH_CLIENT "Build Cockatrice client" ON)
|
||||
# Compile Oracle
|
||||
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
||||
# Compile Servatrice
|
||||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||
# Compile tests
|
||||
option(TEST "build tests" OFF)
|
||||
option(TEST "Build tests" OFF)
|
||||
# Use vcpkg regardless of OS
|
||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||
|
||||
|
|
@ -39,13 +39,24 @@ else()
|
|||
)
|
||||
endif()
|
||||
|
||||
if(USE_CCACHE)
|
||||
# ccache does not support MSVC and must not auto-engage on Windows
|
||||
# (it is installed unintentionally on the Windows CI runner).
|
||||
# NOTE: this keys off the target OS, so a mingw/Ninja configuration on Windows
|
||||
# also opts out of ccache even though the GNUCXX branch below supports it.
|
||||
if(USE_CCACHE AND NOT WIN32)
|
||||
find_program(CCACHE_PROGRAM ccache)
|
||||
if(CCACHE_PROGRAM)
|
||||
# Support Unix Makefiles and Ninja
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
|
||||
# PCH-aware caching, matching .ci/compile.sh: without this ccache refuses
|
||||
# to cache any TU that consumes a precompiled header, so every PCH-backed
|
||||
# target recompiles from scratch on each build.
|
||||
execute_process(COMMAND ${CCACHE_PROGRAM} --set-config sloppiness=pch_defines,time_macros)
|
||||
message(STATUS "Found CCache ${CCACHE_PROGRAM}")
|
||||
endif()
|
||||
elseif(USE_CCACHE AND WIN32)
|
||||
# An explicit opt-in must not disappear silently on Windows.
|
||||
message(STATUS "ccache disabled: not supported for the MSVC toolchain on Windows")
|
||||
endif()
|
||||
|
||||
if(WIN32 OR USE_VCPKG)
|
||||
|
|
@ -184,6 +195,9 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
|||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
else()
|
||||
# other: osx/llvm, bsd/llvm
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
||||
|
|
@ -192,6 +206,9 @@ else()
|
|||
else()
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra")
|
||||
endif()
|
||||
|
||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
endif()
|
||||
|
||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||
|
|
@ -239,11 +256,6 @@ if(WIN32)
|
|||
find_package(OpenSSL REQUIRED)
|
||||
if(OPENSSL_FOUND)
|
||||
include_directories(${OPENSSL_INCLUDE_DIRS})
|
||||
else()
|
||||
message(
|
||||
WARNING
|
||||
"Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ RUN apt-get update \
|
|||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
qt6-websockets-dev \
|
||||
protobuf-compiler \
|
||||
qt6-tools-dev \
|
||||
|
|
@ -42,6 +43,7 @@ RUN apt-get update \
|
|||
libprotobuf32t64 \
|
||||
libqt6sql6-mysql \
|
||||
libqt6websockets6 \
|
||||
libssl3 \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@ if(WITH_CLIENT)
|
|||
Multimedia
|
||||
Network
|
||||
PrintSupport
|
||||
ShaderTools
|
||||
Svg
|
||||
WebSockets
|
||||
Widgets
|
||||
Xml
|
||||
Quick
|
||||
QuickWidgets
|
||||
)
|
||||
endif()
|
||||
if(WITH_ORACLE)
|
||||
|
|
|
|||
24
cmake/pch/qtcore_pch.h
Normal file
24
cmake/pch/qtcore_pch.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/** @file qtcore_pch.h
|
||||
* @brief Precompiled header for all Qt targets (Qt Core only).
|
||||
*
|
||||
* Safe for every target that links Qt Core, including the headless
|
||||
* Servatrice binary. Keep this header free of any widget/gui types.
|
||||
*/
|
||||
|
||||
#include <QBasicTimer>
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMetaObject>
|
||||
#include <QObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <QVariant>
|
||||
30
cmake/pch/qtwidgets_pch.h
Normal file
30
cmake/pch/qtwidgets_pch.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/** @file qtwidgets_pch.h
|
||||
* @brief Precompiled header for GUI targets (Cockatrice client, Oracle).
|
||||
*
|
||||
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
||||
* Network layers that virtually every client translation unit re-parses.
|
||||
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
||||
*/
|
||||
|
||||
#include "qtcore_pch.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QTabWidget>
|
||||
#include <QToolBar>
|
||||
#include <QTreeWidget>
|
||||
#include <QWidget>
|
||||
|
|
@ -214,6 +214,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
|
||||
src/interface/widgets/deck_editor/deck_state_manager.cpp
|
||||
src/interface/widgets/deck_editor/deck_zone_dialog.cpp
|
||||
src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp
|
||||
src/interface/widgets/general/background_sources.cpp
|
||||
src/interface/widgets/general/display/background_plate_widget.cpp
|
||||
|
|
@ -230,6 +231,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp
|
||||
src/interface/widgets/general/display/charts/pies/color_pie.cpp
|
||||
src/interface/widgets/general/home_styled_button.cpp
|
||||
src/interface/widgets/general/home_tab_button_color.h
|
||||
src/interface/widgets/general/home_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/flow_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/overlap_control_widget.cpp
|
||||
|
|
@ -270,6 +272,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/server/user/user_context_menu.cpp
|
||||
src/interface/widgets/server/user/user_info_box.cpp
|
||||
src/interface/widgets/server/user/user_info_connection.cpp
|
||||
src/interface/widgets/server/user/user_list_dialog.cpp
|
||||
src/interface/widgets/server/user/user_list_manager.cpp
|
||||
src/interface/widgets/server/user/user_list_painter.cpp
|
||||
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
||||
|
|
@ -397,6 +400,27 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.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/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
|
||||
|
|
@ -493,6 +517,30 @@ qt6_add_executable(
|
|||
MANUAL_FINALIZATION
|
||||
)
|
||||
|
||||
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
cockatrice
|
||||
PUBLIC libcockatrice_card
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -52,6 +52,7 @@ In this list of examples below, each entry has an explanation and can be clicked
|
|||
<dt><u>E</u>dition:</dt>
|
||||
<dd>[set:lea](#set:lea) <small>(Cards that appear in Alpha, which has the set code LEA)</small></dd>
|
||||
<dd>[e:lea OR e:leb](#e:lea OR e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
|
||||
<dd>[e<8ED](#e<8ED) <small>(Cards that appear before 8th edition)</small></dd>
|
||||
|
||||
<dt>Negate:</dt>
|
||||
<dd>[c:wu -c:m](#c:wu -c:m) <small>(Any card that is white or blue, but not multicolored)</small></dd>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
#include <algorithm>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
||||
|
|
@ -381,12 +380,10 @@ void DeckViewScene::rebuildTree()
|
|||
addItem(container);
|
||||
}
|
||||
|
||||
for (int j = 0; j < currentZone->size(); j++) {
|
||||
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||
if (!currentCard) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cards in custom zones nested under a board are regular board cards in-game.
|
||||
// They are collected recursively (like every other consumer) and reported with
|
||||
// the top-level board zone as their origin, so that sideboard plans keep working.
|
||||
for (auto *currentCard : deck->getCardNodes({currentZone->getName()})) {
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName());
|
||||
container->addCard(newCard);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
||||
#include "../../interface/widgets/dialogs/dlg_load_remote_deck.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "deck_view.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
|
|
|||
|
|
@ -44,11 +44,16 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
|
||||
GameScene::~GameScene()
|
||||
{
|
||||
// Sever all incoming connections (animated item destroy-tracking) before the
|
||||
// members below are destroyed: the base QGraphicsScene destructor destroys the
|
||||
// remaining items, and their destroyed() signals must not reach slots that
|
||||
// reference members that no longer exist.
|
||||
disconnect(this);
|
||||
// Sever all destroyed->removeAnimatedItem connections before the members below
|
||||
// are destroyed: the base QGraphicsScene destructor destroys the remaining items,
|
||||
// and their destroyed() signals must not reach slots that reference members that
|
||||
// no longer exist. The connection handle overload is used because the string-based
|
||||
// disconnect(nullptr, nullptr, this, nullptr) is invalid (the sender must never be
|
||||
// nullptr) and would otherwise fail to sever these pointer-to-member connections.
|
||||
for (auto it = animationItemConnections.constBegin(); it != animationItemConnections.constEnd(); ++it) {
|
||||
QObject::disconnect(*it);
|
||||
}
|
||||
animationItemConnections.clear();
|
||||
|
||||
delete animationTimer;
|
||||
animationTimer = nullptr;
|
||||
|
|
@ -777,8 +782,15 @@ void GameScene::registerAnimationItem(IAnimatedItem *item)
|
|||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if (!animatedItems.contains(object)) {
|
||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
|
||||
// Guard against duplicate connections using the connection map, not
|
||||
// animatedItems: the animation timer removes entries from animatedItems when an
|
||||
// animation completes, but the destroyed->removeAnimatedItem connection must
|
||||
// persist until the object is destroyed. Relying on animatedItems here would let
|
||||
// a re-registered item (e.g. a life counter that flashes repeatedly) accumulate
|
||||
// duplicate destroyed connections, the older ones of which would survive teardown.
|
||||
if (!animationItemConnections.contains(object)) {
|
||||
animationItemConnections.insert(object,
|
||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem));
|
||||
}
|
||||
animatedItems.insert(object, item);
|
||||
if (animationTimer && !animationTimer->isActive()) {
|
||||
|
|
@ -797,6 +809,7 @@ void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
|||
void GameScene::removeAnimatedItem(QObject *item)
|
||||
{
|
||||
animatedItems.remove(item);
|
||||
animationItemConnections.remove(item);
|
||||
if (animationTimer && animatedItems.isEmpty()) {
|
||||
animationTimer->stop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,9 +54,11 @@ private:
|
|||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
QHash<QObject *, QMetaObject::Connection>
|
||||
animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
|
||||
/**
|
||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ TallyMenu::TallyMenu()
|
|||
aTallyNone = createTallyAction(TallyType::None);
|
||||
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
||||
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
||||
aTallyTotalToughness = createTallyAction(TallyType::TotalToughness);
|
||||
|
||||
addAction(aTallyNone);
|
||||
addSeparator();
|
||||
addAction(aTallySubtypes);
|
||||
addAction(aTallyTotalPower);
|
||||
addAction(aTallyTotalToughness);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
|
@ -54,4 +56,5 @@ void TallyMenu::retranslateUi()
|
|||
aTallyNone->setText(tr("None"));
|
||||
aTallySubtypes->setText(tr("Subtypes"));
|
||||
aTallyTotalPower->setText(tr("Total Power"));
|
||||
aTallyTotalToughness->setText(tr("Total Toughness"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ private:
|
|||
QAction *aTallyNone = nullptr;
|
||||
QAction *aTallySubtypes = nullptr;
|
||||
QAction *aTallyTotalPower = nullptr;
|
||||
QAction *aTallyTotalToughness = nullptr;
|
||||
|
||||
QAction *createTallyAction(TallyType tallyType);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,3 +34,31 @@ QList<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &cards)
|
|||
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
||||
return {TallyRow{name, QString::number(total)}};
|
||||
}
|
||||
|
||||
static int sumToughness(const QList<CardItem *> &cards)
|
||||
{
|
||||
int total = 0;
|
||||
for (auto card : cards) {
|
||||
QVariantList parsed = CardItem::parsePT(card->getPT());
|
||||
if (parsed.size() == 2) {
|
||||
int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int
|
||||
total += qMax(toughness, 0);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
QList<TallyRow> StatsTally::computeTotalToughness(const QList<CardItem *> &cards)
|
||||
{
|
||||
// don't bother if none of the cards have pt
|
||||
bool hasPT =
|
||||
std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); });
|
||||
if (!hasPT) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int total = sumToughness(cards);
|
||||
|
||||
QString name = QCoreApplication::translate("StatsTally", "Total Toughness");
|
||||
return {TallyRow{name, QString::number(total)}};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ namespace StatsTally
|
|||
*/
|
||||
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
||||
|
||||
/**
|
||||
* @brief Sums the toughness of all selected cards
|
||||
*
|
||||
* @param cards The list of selected card items to analyze.
|
||||
* @return A single row containing the total, or an empty list if none of the cards have pt
|
||||
*/
|
||||
QList<TallyRow> computeTotalToughness(const QList<CardItem *> &cards);
|
||||
|
||||
} // namespace StatsTally
|
||||
|
||||
#endif // COCKATRICE_STATS_TALLY_H
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType t
|
|||
return SubtypeTally::countSubtypes(cards);
|
||||
case TallyType::TotalPower:
|
||||
return StatsTally::computeTotalPower(cards);
|
||||
case TallyType::TotalToughness:
|
||||
return StatsTally::computeTotalToughness(cards);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ enum class TallyType
|
|||
None,
|
||||
Subtypes,
|
||||
TotalPower,
|
||||
MaxValue = TotalPower // sentinel value
|
||||
TotalToughness,
|
||||
MaxValue = TotalToughness // sentinel value
|
||||
};
|
||||
|
||||
namespace Tally
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
}
|
||||
}
|
||||
} else {
|
||||
x = calcDropIndexFromY(dropPoint.y());
|
||||
bool sameZone = startZone == getLogic();
|
||||
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
|
||||
}
|
||||
|
||||
Command_MoveCard cmd;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons
|
|||
return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
|
||||
}
|
||||
|
||||
int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
||||
int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const
|
||||
{
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (cards.isEmpty()) {
|
||||
|
|
@ -94,7 +94,8 @@ int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
|||
if (effectiveOffset <= 0.0) {
|
||||
return 0;
|
||||
}
|
||||
return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1);
|
||||
int max = allowCountExpand ? params.cardCount : params.cardCount - 1;
|
||||
return qBound(0, qRound((dropY - start) / effectiveOffset), max);
|
||||
}
|
||||
|
||||
void SelectZone::restoreStaleEscapedCards()
|
||||
|
|
|
|||
|
|
@ -104,8 +104,12 @@ protected:
|
|||
/**
|
||||
* @brief Computes the card index at a given y-coordinate within the zone's vertical layout.
|
||||
* Returns 0 if the zone has no cards or the offset is zero.
|
||||
*
|
||||
* @param dropY The y-coordinate that the card was dropped at
|
||||
* @param allowCountExpand If false, clamps the index at the number of cards minus 1
|
||||
* @param minOffset Minimum offset to preserve
|
||||
*/
|
||||
int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const;
|
||||
int calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset = 0.0) const;
|
||||
|
||||
/**
|
||||
* @brief Positions cards vertically with alternating left/right x-offsets.
|
||||
|
|
|
|||
|
|
@ -57,18 +57,14 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
|||
return;
|
||||
}
|
||||
|
||||
const auto &cards = getLogic()->getCards();
|
||||
int index;
|
||||
if (startZone == getLogic()) {
|
||||
// Reordering within the zone: use drop position
|
||||
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||
bool sameZone = startZone == getLogic();
|
||||
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
|
||||
if (sameZone) {
|
||||
// Same-zone no-op: don't move a card onto itself
|
||||
const auto &cards = getLogic()->getCards();
|
||||
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Coming from another zone: append at end (top of stack, rendered on top)
|
||||
index = static_cast<int>(cards.size());
|
||||
}
|
||||
|
||||
Command_MoveCard cmd;
|
||||
|
|
|
|||
|
|
@ -375,15 +375,32 @@ void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckL
|
|||
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
bool addComments,
|
||||
bool addSetNameAndNumber)
|
||||
bool addSetNameAndNumber,
|
||||
const QString &boardZoneName)
|
||||
{
|
||||
// Nested sub-zones keep their owning board's identity: the top-level call
|
||||
// passes no board, so the zone's own name is used; recursive calls carry the
|
||||
// owning board down so the sideboard marker survives sub-zone nesting.
|
||||
const QString owningBoardZoneName = boardZoneName.isEmpty() ? zoneNode->getName() : boardZoneName;
|
||||
|
||||
// group cards by card type and count the subtotals
|
||||
QMultiMap<QString, DecklistCardNode *> cardsByType;
|
||||
QMap<QString, int> cardTotalByType;
|
||||
int cardTotal = 0;
|
||||
QList<const InnerDecklistNode *> subZones;
|
||||
|
||||
for (int j = 0; j < zoneNode->size(); j++) {
|
||||
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
|
||||
if (!card) {
|
||||
// Cards collected in nested sub-zones are exported by recursion so
|
||||
// they don't end up invisible in the plain text output. They are
|
||||
// deferred until after this zone's own header and cards so they read
|
||||
// as part of this zone's block.
|
||||
if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zoneNode->at(j))) {
|
||||
subZones.append(subZone);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
QString cardType = info ? info->getMainCardType() : "unknown";
|
||||
|
|
@ -411,25 +428,30 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
|||
|
||||
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
|
||||
|
||||
saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber);
|
||||
saveToStream_DeckZoneCards(out, cards, addComments, addSetNameAndNumber, owningBoardZoneName);
|
||||
|
||||
if (addComments) {
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Nested sub-zones come last, after the parent's own header and cards.
|
||||
for (const auto *subZone : subZones) {
|
||||
saveToStream_DeckZone(out, subZone, addComments, addSetNameAndNumber, owningBoardZoneName);
|
||||
}
|
||||
}
|
||||
|
||||
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
QList<DecklistCardNode *> cards,
|
||||
bool addComments,
|
||||
bool addSetNameAndNumber)
|
||||
bool addSetNameAndNumber,
|
||||
const QString &boardZoneName)
|
||||
{
|
||||
// QMultiMap sorts values in reverse order
|
||||
for (int i = cards.size() - 1; i >= 0; --i) {
|
||||
DecklistCardNode *card = cards[i];
|
||||
|
||||
if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) {
|
||||
if (boardZoneName == DECK_ZONE_SIDE && addComments) {
|
||||
out << "SB: ";
|
||||
}
|
||||
|
||||
|
|
@ -510,9 +532,26 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
|
|||
|
||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
||||
{
|
||||
if (!node || node->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int totalColumns = 2;
|
||||
|
||||
if (node->height() == 1) {
|
||||
// Dispatch children by type instead of trusting a whole-node height: a deck
|
||||
// node may hold direct cards and nested zones side by side (custom zones),
|
||||
// and an empty node would previously crash on at(0).
|
||||
QVector<const AbstractDecklistCardNode *> cards;
|
||||
QVector<const InnerDecklistNode *> subZones;
|
||||
for (int i = 0; i < node->size(); i++) {
|
||||
if (auto *card = dynamic_cast<const AbstractDecklistCardNode *>(node->at(i))) {
|
||||
cards.append(card);
|
||||
} else if (auto *zone = dynamic_cast<const InnerDecklistNode *>(node->at(i))) {
|
||||
subZones.append(zone);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cards.isEmpty()) {
|
||||
QTextBlockFormat blockFormat;
|
||||
QTextCharFormat charFormat;
|
||||
charFormat.setFontPointSize(11);
|
||||
|
|
@ -523,9 +562,9 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
|||
tableFormat.setCellPadding(0);
|
||||
tableFormat.setCellSpacing(0);
|
||||
tableFormat.setBorder(0);
|
||||
QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat);
|
||||
for (int i = 0; i < node->size(); i++) {
|
||||
auto *card = dynamic_cast<AbstractDecklistCardNode *>(node->at(i));
|
||||
QTextTable *table = cursor->insertTable(cards.size() + 1, totalColumns, tableFormat);
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
const AbstractDecklistCardNode *card = cards[i];
|
||||
|
||||
QTextCharFormat cellCharFormat;
|
||||
cellCharFormat.setFontPointSize(9);
|
||||
|
|
@ -540,7 +579,13 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
|||
cellCursor = cell.firstCursorPosition();
|
||||
cellCursor.insertText(card->getName());
|
||||
}
|
||||
} else if (node->height() == 2) {
|
||||
}
|
||||
|
||||
for (const InnerDecklistNode *subZone : subZones) {
|
||||
if (subZone->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QTextBlockFormat blockFormat;
|
||||
QTextCharFormat charFormat;
|
||||
charFormat.setFontPointSize(14);
|
||||
|
|
@ -559,10 +604,8 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
|||
tableFormat.setColumnWidthConstraints(constraints);
|
||||
|
||||
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
|
||||
for (int i = 0; i < node->size(); i++) {
|
||||
QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition();
|
||||
printDeckListNode(&cellCursor, dynamic_cast<InnerDecklistNode *>(node->at(i)));
|
||||
}
|
||||
QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition();
|
||||
printDeckListNode(&cellCursor, subZone);
|
||||
}
|
||||
|
||||
cursor->movePosition(QTextCursor::End);
|
||||
|
|
|
|||
|
|
@ -159,12 +159,13 @@ private:
|
|||
static void saveToStream_DeckZone(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
bool addSetNameAndNumber = true,
|
||||
const QString &boardZoneName = QString());
|
||||
static void saveToStream_DeckZoneCards(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
QList<DecklistCardNode *> cards,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
bool addSetNameAndNumber = true,
|
||||
const QString &boardZoneName = QString());
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <QApplication>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QImageReader>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QSvgRenderer>
|
||||
|
|
@ -418,6 +419,57 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
|
|||
|
||||
QMap<QString, QPixmap> DropdownIconPixmapGenerator::pmCache;
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Longest side mana symbols are rendered at before being scaled to their final size.
|
||||
constexpr int MASTER_ICON_SIZE = 128;
|
||||
|
||||
QString manaSymbolCacheKey(const QString &symbol, const QSize &size)
|
||||
{
|
||||
return symbol + QLatin1Char('|') + QString::number(size.width()) + QLatin1Char('x') +
|
||||
QString::number(size.height());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const QPixmap &ManaSymbolPixmapGenerator::masterIcon(const QString &symbol)
|
||||
{
|
||||
auto it = masterCache.constFind(symbol);
|
||||
if (it != masterCache.constEnd()) {
|
||||
return it.value();
|
||||
}
|
||||
|
||||
QImageReader reader("theme:icons/mana/" + symbol);
|
||||
QSize sourceSize = reader.size();
|
||||
if (!sourceSize.isEmpty()) {
|
||||
sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio);
|
||||
reader.setScaledSize(sourceSize);
|
||||
}
|
||||
const QPixmap rendered = QPixmap::fromImageReader(&reader);
|
||||
|
||||
return masterCache.insert(symbol, rendered).value();
|
||||
}
|
||||
|
||||
QPixmap ManaSymbolPixmapGenerator::generatePixmap(const QString &symbol, const QSize &size)
|
||||
{
|
||||
const QString key = manaSymbolCacheKey(symbol, size);
|
||||
auto it = scaledCache.constFind(key);
|
||||
if (it != scaledCache.constEnd()) {
|
||||
return it.value();
|
||||
}
|
||||
|
||||
const QPixmap &icon = masterIcon(symbol);
|
||||
if (icon.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QPixmap scaled = icon.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
scaledCache.insert(key, scaled);
|
||||
return scaled;
|
||||
}
|
||||
|
||||
QHash<QString, QPixmap> ManaSymbolPixmapGenerator::masterCache;
|
||||
QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
|
||||
|
||||
QPixmap loadColorAdjustedPixmap(const QString &name)
|
||||
{
|
||||
if (qApp->palette().windowText().color().lightness() > 200) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#ifndef PIXMAPGENERATOR_H
|
||||
#define PIXMAPGENERATOR_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QIcon>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
|
|
@ -125,6 +126,34 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
class ManaSymbolPixmapGenerator
|
||||
{
|
||||
private:
|
||||
static QHash<QString, QPixmap> masterCache;
|
||||
static QHash<QString, QPixmap> scaledCache;
|
||||
|
||||
/**
|
||||
* @brief Renders \a symbol once at a fixed moderate size, so repeated scalings never
|
||||
* re-rasterize the source file (SVG sources can be very expensive to rasterize).
|
||||
*/
|
||||
static const QPixmap &masterIcon(const QString &symbol);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Returns a smooth-scaled rendering of the given mana symbol icon.
|
||||
*
|
||||
* Results are shared between all callers via a process-wide cache keyed by symbol
|
||||
* and size, so scaling work is done once per distinct combination instead of once
|
||||
* per widget creation or resize.
|
||||
*/
|
||||
static QPixmap generatePixmap(const QString &symbol, const QSize &size);
|
||||
static void clear()
|
||||
{
|
||||
masterCache.clear();
|
||||
scaledCache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
QPixmap loadColorAdjustedPixmap(const QString &name);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -272,6 +272,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());
|
||||
|
|
|
|||
|
|
@ -91,6 +91,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);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
|
|||
// clear old layout
|
||||
QtUtils::clearLayoutRec(layout);
|
||||
|
||||
// The freshly created symbols haven't been sized yet, so force the next resize pass
|
||||
// to apply the symbol size again.
|
||||
lastIconSize = -1;
|
||||
lastWidth = -1;
|
||||
|
||||
// populate mana symbols
|
||||
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) {
|
||||
for (const QString symbol : fullColorIdentity) {
|
||||
|
|
@ -73,20 +78,33 @@ void ColorIdentityWidget::toggleUnusedVisibility()
|
|||
void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
|
||||
|
||||
if (!manaSymbols.isEmpty()) {
|
||||
int totalWidth = event->size().width();
|
||||
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
const int totalWidth = event->size().width();
|
||||
if (totalWidth == lastWidth && lastIconSize != -1) {
|
||||
return;
|
||||
}
|
||||
lastWidth = totalWidth;
|
||||
|
||||
int spacing = layout->spacing();
|
||||
int count = manaSymbols.size();
|
||||
int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
|
||||
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
|
||||
manaSymbol->setFixedSize(iconSize, iconSize);
|
||||
const int count = layout->count();
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int spacing = layout->spacing();
|
||||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
|
||||
if (iconSize == lastIconSize) {
|
||||
return;
|
||||
}
|
||||
lastIconSize = iconSize;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (auto *w = qobject_cast<ManaSymbolWidget *>(layout->itemAt(i)->widget())) {
|
||||
w->setFixedSize(iconSize, iconSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public slots:
|
|||
private:
|
||||
QString colorIdentity;
|
||||
QHBoxLayout *layout;
|
||||
int lastIconSize = -1; ///< The symbol size last applied, to skip redundant resize passes.
|
||||
int lastWidth = -1; ///< The width last processed, to skip redundant resize passes.
|
||||
};
|
||||
|
||||
#endif // COLOR_IDENTITY_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
#include "mana_symbol_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../../pixel_map_generator.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled)
|
||||
: QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled)
|
||||
: QLabel(parent), symbol(std::move(_symbol)), isActive(_isActive), mayBeToggled(_mayBeToggled)
|
||||
{
|
||||
loadManaIcon();
|
||||
setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(50, 50)));
|
||||
setMaximumWidth(50);
|
||||
|
||||
// Initialize opacity effect
|
||||
|
|
@ -64,16 +64,13 @@ void ManaSymbolWidget::mousePressEvent(QMouseEvent *event)
|
|||
void ManaSymbolWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QLabel::resizeEvent(event);
|
||||
setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
const QSize newSize = event->size();
|
||||
|
||||
void ManaSymbolWidget::loadManaIcon()
|
||||
{
|
||||
QString filename = "theme:icons/mana/";
|
||||
|
||||
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
|
||||
filename += symbol;
|
||||
// Skip the rescale when the size didn't actually change: layout passes resize these
|
||||
// widgets repeatedly with identical sizes.
|
||||
if (newSize.isEmpty() || pixmap().size() == newSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
manaIcon = QPixmap(filename);
|
||||
setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, newSize));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ public:
|
|||
return symbol[0];
|
||||
}
|
||||
|
||||
void loadManaIcon();
|
||||
|
||||
public slots:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
|
@ -44,7 +42,6 @@ signals:
|
|||
|
||||
private:
|
||||
QString symbol;
|
||||
QPixmap manaIcon;
|
||||
bool isActive;
|
||||
bool mayBeToggled;
|
||||
QGraphicsOpacityEffect *opacityEffect;
|
||||
|
|
|
|||
|
|
@ -174,16 +174,18 @@ void CardGroupDisplayWidget::updateCardDisplays()
|
|||
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
|
||||
|
||||
// 4. persist the source index
|
||||
QPersistentModelIndex persistent(sourceIndex);
|
||||
addCardWidgets(QPersistentModelIndex(sourceIndex));
|
||||
}
|
||||
}
|
||||
|
||||
// Get the card amount
|
||||
int cardAmount =
|
||||
sourceIndex.sibling(sourceIndex.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
|
||||
void CardGroupDisplayWidget::addCardWidgets(const QPersistentModelIndex &persistent)
|
||||
{
|
||||
// Get the card amount
|
||||
int cardAmount = persistent.sibling(persistent.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
|
||||
|
||||
// Create multiple widgets for the card count
|
||||
for (int copy = 0; copy < cardAmount; ++copy) {
|
||||
addToLayout(constructWidgetForIndex(persistent));
|
||||
}
|
||||
// Create multiple widgets for the card count
|
||||
for (int copy = 0; copy < cardAmount; ++copy) {
|
||||
addToLayout(constructWidgetForIndex(persistent));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public:
|
|||
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
|
||||
void refreshSelectionForIndex(const QPersistentModelIndex &persistent);
|
||||
void clearAllDisplayWidgets();
|
||||
void addCardWidgets(const QPersistentModelIndex &persistent);
|
||||
|
||||
DeckListModel *deckListModel;
|
||||
QItemSelectionModel *selectionModel;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "libcockatrice/card/database/card_database_manager.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
|
||||
DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
||||
|
|
@ -51,11 +52,6 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
|||
// User Interaction
|
||||
// =====================================================================================================================
|
||||
|
||||
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
|
||||
{
|
||||
emit cardClicked(event, card, zoneName);
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::onHover(const ExactCard &card)
|
||||
{
|
||||
emit cardHovered(card);
|
||||
|
|
@ -95,12 +91,18 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
|
|||
}
|
||||
|
||||
auto categoryName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
// Cards in a custom zone belong to that zone, not the board zone, so that
|
||||
// increment/decrement/swap actions target the custom zone.
|
||||
const bool isCustomZone = index.data(DeckRoles::IsCustomZoneRole).toBool();
|
||||
const QString effectiveZoneName = isCustomZone ? categoryName : zoneName;
|
||||
const auto routeCardClick = [this, effectiveZoneName](QMouseEvent *event, const ExactCard &card) {
|
||||
emit cardClicked(event, card, effectiveZoneName);
|
||||
};
|
||||
if (displayType == DisplayType::Overlap) {
|
||||
auto *displayWidget = new OverlappedCardGroupDisplayWidget(
|
||||
cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria,
|
||||
activeSortCriteria, subBannerOpacity, cardSizeWidget);
|
||||
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this,
|
||||
&DeckCardZoneDisplayWidget::onClick);
|
||||
cardGroupContainer, deckListModel, selectionModel, index, effectiveZoneName, categoryName,
|
||||
activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget);
|
||||
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this, routeCardClick);
|
||||
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardHovered, this,
|
||||
&DeckCardZoneDisplayWidget::onHover);
|
||||
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
|
||||
|
|
@ -111,9 +113,9 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
|
|||
indexToWidgetMap.insert(index, displayWidget);
|
||||
} else if (displayType == DisplayType::Flat) {
|
||||
auto *displayWidget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, selectionModel, index,
|
||||
zoneName, categoryName, activeGroupCriteria,
|
||||
effectiveZoneName, categoryName, activeGroupCriteria,
|
||||
activeSortCriteria, subBannerOpacity, cardSizeWidget);
|
||||
connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, &DeckCardZoneDisplayWidget::onClick);
|
||||
connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, routeCardClick);
|
||||
connect(displayWidget, &FlatCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover);
|
||||
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
|
||||
&DeckCardZoneDisplayWidget::cleanupInvalidCardGroup);
|
||||
|
|
@ -126,24 +128,18 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
|
|||
|
||||
void DeckCardZoneDisplayWidget::displayCards()
|
||||
{
|
||||
QSortFilterProxyModel proxy;
|
||||
proxy.setSourceModel(deckListModel);
|
||||
proxy.setSortRole(Qt::EditRole);
|
||||
proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
|
||||
if (!trackedIndex.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. trackedIndex is a source index → map it to proxy space
|
||||
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
|
||||
|
||||
// 2. iterate children under the proxy parent
|
||||
for (int i = 0; i < proxy.rowCount(proxyParent); ++i) {
|
||||
QModelIndex proxyIndex = proxy.index(i, 0, proxyParent);
|
||||
|
||||
// 3. map back to source
|
||||
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
|
||||
|
||||
// 4. persist the source index
|
||||
QPersistentModelIndex persistent(sourceIndex);
|
||||
// Iterate the direct children of the tracked zone, keeping the tree view's row
|
||||
// order (criteria groups first, then custom zones, both in the model's sort order).
|
||||
QList<QPersistentModelIndex> rows;
|
||||
for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) {
|
||||
rows.append(QPersistentModelIndex(deckListModel->index(i, 0, trackedIndex)));
|
||||
}
|
||||
|
||||
for (const QPersistentModelIndex &persistent : rows) {
|
||||
constructAppropriateWidget(persistent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ public:
|
|||
void addCardsToOverlapWidget();
|
||||
|
||||
public slots:
|
||||
void onClick(QMouseEvent *event, const ExactCard &card);
|
||||
void onHover(const ExactCard &card);
|
||||
void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget);
|
||||
void constructAppropriateWidget(QPersistentModelIndex index);
|
||||
|
|
|
|||
|
|
@ -90,6 +90,13 @@ void CardDatabaseView::decrementCard(const QString &zoneName)
|
|||
emit cardDecremented(currentCardName(), zoneName);
|
||||
}
|
||||
|
||||
void CardDatabaseView::setZoneMenuProvider(const std::function<QList<QPair<QString, QStringList>>()> &provider,
|
||||
const std::function<QString()> &newZoneHandler)
|
||||
{
|
||||
zoneMenuProvider = provider;
|
||||
this->newZoneHandler = newZoneHandler;
|
||||
}
|
||||
|
||||
void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/)
|
||||
{
|
||||
if (!current.isValid()) {
|
||||
|
|
@ -142,6 +149,50 @@ void CardDatabaseView::openCustomMenu(QPoint point)
|
|||
[this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); });
|
||||
connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked);
|
||||
|
||||
if (zoneMenuProvider) {
|
||||
QMenu *addToZoneMenu = menu.addMenu(tr("Add to Zone"));
|
||||
const auto zoneBoards = zoneMenuProvider();
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
// Boards with zones nest their children so no two menu entries
|
||||
// share a visible name: "Maindeck ▸ { Maindeck (whole board), … }".
|
||||
const QStringList customZones = [&zoneBoards, boardName] {
|
||||
for (const auto &zoneBoard : zoneBoards) {
|
||||
if (zoneBoard.first == boardName) {
|
||||
return zoneBoard.second;
|
||||
}
|
||||
}
|
||||
return QStringList();
|
||||
}();
|
||||
if (customZones.isEmpty()) {
|
||||
QAction *action = addToZoneMenu->addAction(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
connect(action, &QAction::triggered, this,
|
||||
[this, card, boardName] { emit cardAdded(card->getName(), boardName); });
|
||||
} else {
|
||||
QMenu *boardSubmenu = addToZoneMenu->addMenu(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
QAction *wholeBoardAction = boardSubmenu->addAction(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
connect(wholeBoardAction, &QAction::triggered, this,
|
||||
[this, card, boardName] { emit cardAdded(card->getName(), boardName); });
|
||||
for (const QString &zoneName : customZones) {
|
||||
QAction *action = boardSubmenu->addAction(zoneName);
|
||||
connect(action, &QAction::triggered, this,
|
||||
[this, card, zoneName] { emit cardAdded(card->getName(), zoneName); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newZoneHandler) {
|
||||
addToZoneMenu->addSeparator();
|
||||
|
||||
QAction *newZoneAction = addToZoneMenu->addAction(tr("Create &new zone..."));
|
||||
connect(newZoneAction, &QAction::triggered, this, [this, card] {
|
||||
const QString zoneName = newZoneHandler();
|
||||
if (!zoneName.isEmpty()) {
|
||||
emit cardAdded(card->getName(), zoneName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (canBeCommander(*card)) {
|
||||
QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)"));
|
||||
connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); });
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../key_signals.h"
|
||||
|
||||
#include <QTreeView>
|
||||
#include <functional>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
||||
class CardDatabaseModel;
|
||||
|
|
@ -19,6 +20,13 @@ class CardDatabaseView : public QTreeView
|
|||
KeySignals searchKeySignals;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
|
||||
/// Provides the custom zones available in the current deck, grouped by board zone.
|
||||
/// The list contains (board zone name, custom zone names) pairs for every board.
|
||||
std::function<QList<QPair<QString, QStringList>>()> zoneMenuProvider;
|
||||
/// Handler invoked when the user picks "New zone..." from the add-to-zone menu.
|
||||
/// Returns the name of the created zone, or an empty string if creation was cancelled.
|
||||
std::function<QString()> newZoneHandler;
|
||||
|
||||
public:
|
||||
explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model);
|
||||
|
||||
|
|
@ -33,6 +41,17 @@ public:
|
|||
return &searchKeySignals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the provider used to populate the "Add to zone" submenu of the context menu.
|
||||
* If no provider is set, the submenu is not shown.
|
||||
*
|
||||
* @param provider Returns the custom zones of the current deck, grouped by board zone
|
||||
* @param newZoneHandler Creates a new custom zone and returns its name, or an empty string
|
||||
* if creation was cancelled. The menu entry is hidden when not provided.
|
||||
*/
|
||||
void setZoneMenuProvider(const std::function<QList<QPair<QString, QStringList>>()> &provider,
|
||||
const std::function<QString()> &newZoneHandler);
|
||||
|
||||
signals:
|
||||
void cardChanged(const QString &cardName);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
#include "deck_editor_card_database_dock_widget.h"
|
||||
|
||||
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
|
||||
#include "card_database_view.h"
|
||||
#include "deck_state_manager.h"
|
||||
#include "deck_zone_dialog.h"
|
||||
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
|
||||
DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent)
|
||||
{
|
||||
setObjectName("databaseDisplayDock");
|
||||
|
|
@ -15,6 +22,27 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck
|
|||
{
|
||||
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel);
|
||||
|
||||
databaseDisplayWidget->getDatabaseView()->setZoneMenuProvider(
|
||||
[deckEditor]() -> QList<QPair<QString, QStringList>> {
|
||||
QList<QPair<QString, QStringList>> result;
|
||||
auto *deckListModel = deckEditor->deckStateManager->getModel();
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
result.append({boardName, deckListModel->getCustomZoneNames(boardName)});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[this, deckEditor]() -> QString {
|
||||
QString boardName;
|
||||
const QString zoneName =
|
||||
DeckZoneDialog::promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) {
|
||||
return deckEditor->deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!zoneName.isEmpty()) {
|
||||
deckEditor->deckStateManager->createCustomZone(boardName, zoneName);
|
||||
}
|
||||
return zoneName;
|
||||
});
|
||||
|
||||
auto *frame = new QVBoxLayout;
|
||||
frame->setObjectName("databaseDisplayFrame");
|
||||
frame->addWidget(databaseDisplayWidget);
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@
|
|||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
#include "deck_state_manager.h"
|
||||
#include "deck_zone_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
|
@ -772,14 +775,213 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i
|
|||
|
||||
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)
|
||||
{
|
||||
const QModelIndex sourceIndex = proxy->mapToSource(deckView->indexAt(point));
|
||||
|
||||
QMenu menu;
|
||||
|
||||
const bool isCustomZoneRow = sourceIndex.isValid() && sourceIndex.data(DeckRoles::IsCustomZoneRole).toBool();
|
||||
const bool isBoardZoneRow = sourceIndex.isValid() && !isCustomZoneRow && !sourceIndex.parent().isValid();
|
||||
const bool isCardRow =
|
||||
sourceIndex.isValid() && !isCustomZoneRow && !isBoardZoneRow && !getModel()->hasChildren(sourceIndex);
|
||||
|
||||
// Walk the row up to its top-level node to find the hosting board. Cards in
|
||||
// the tokens board cannot be moved (moveCardToZone bails for it), so the
|
||||
// move menu is skipped for them.
|
||||
QString currentBoardName;
|
||||
QModelIndex board = sourceIndex.parent();
|
||||
while (board.isValid() && board.parent().isValid()) {
|
||||
board = board.parent();
|
||||
}
|
||||
if (board.isValid()) {
|
||||
currentBoardName = board.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
}
|
||||
|
||||
if (isCardRow) {
|
||||
if (currentBoardName != DECK_ZONE_TOKENS) {
|
||||
addMoveToZoneMenu(&menu, sourceIndex, currentBoardName);
|
||||
menu.addSeparator();
|
||||
}
|
||||
} else if (isCustomZoneRow) {
|
||||
const QString zoneName =
|
||||
sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
|
||||
QAction *renameAction = menu.addAction(tr("&Rename zone..."));
|
||||
connect(renameAction, &QAction::triggered, this, [this, zoneName] {
|
||||
// The unchanged name must not validate as a duplicate.
|
||||
const QString newName =
|
||||
DeckZoneDialog::promptForRename(this, zoneName, [this, zoneName](const QString &candidate) {
|
||||
return candidate == zoneName ? QString() : deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!newName.isEmpty() && newName != zoneName) {
|
||||
deckStateManager->renameCustomZone(zoneName, newName);
|
||||
}
|
||||
});
|
||||
|
||||
QMenu *boardMenu = menu.addMenu(tr("Change &board"));
|
||||
addChangeBoardMenu(boardMenu, zoneName);
|
||||
|
||||
QAction *deleteAction = menu.addAction(tr("&Delete zone"));
|
||||
const bool zoneHasCards = getModel()->hasChildren(sourceIndex);
|
||||
deleteAction->setEnabled(!zoneHasCards);
|
||||
if (zoneHasCards) {
|
||||
deleteAction->setToolTip(tr("Move or remove all cards first."));
|
||||
menu.setToolTipsVisible(true);
|
||||
}
|
||||
connect(deleteAction, &QAction::triggered, this, [this, zoneName] {
|
||||
const auto result =
|
||||
QMessageBox::warning(this, tr("Delete zone"), tr("Delete the zone \"%1\"?").arg(zoneName),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (result == QMessageBox::Yes) {
|
||||
deckStateManager->removeCustomZone(zoneName);
|
||||
}
|
||||
});
|
||||
menu.addSeparator();
|
||||
} else if (isBoardZoneRow) {
|
||||
const QString boardName =
|
||||
sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
// Tokens cannot host custom zones, so only offer the action on real boards.
|
||||
const bool canHostCustomZones =
|
||||
boardName == DECK_ZONE_MAIN || boardName == DECK_ZONE_SIDE || boardName == DECK_ZONE_MAYBEBOARD;
|
||||
if (canHostCustomZones) {
|
||||
addNewZoneAction(&menu, boardName);
|
||||
menu.addSeparator();
|
||||
}
|
||||
} else if (!sourceIndex.isValid()) {
|
||||
addNewZoneAction(&menu);
|
||||
menu.addSeparator();
|
||||
}
|
||||
|
||||
QAction *selectPrinting = menu.addAction(tr("Select Printing"));
|
||||
connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector);
|
||||
|
||||
menu.exec(deckView->mapToGlobal(point));
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::addMoveToZoneMenu(QMenu *menu,
|
||||
const QModelIndex &sourceCardIndex,
|
||||
const QString ¤tBoardName)
|
||||
{
|
||||
// The card's current *zone*, derived with the same ancestor walk as
|
||||
// DeckStateManager::moveCardToZone (nearest custom-zone ancestor, else the
|
||||
// top-level board/zone): a card inside "Removal" under the maindeck lives in
|
||||
// "Removal", not "main". Comparing against that instead of the board keeps
|
||||
// the enabled state and the same-zone no-op consistent with the move logic.
|
||||
QString currentZoneName;
|
||||
for (QModelIndex ancestor = sourceCardIndex.parent(); ancestor.isValid(); ancestor = ancestor.parent()) {
|
||||
if (ancestor.data(DeckRoles::IsCustomZoneRole).toBool() || !ancestor.parent().isValid()) {
|
||||
currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto addMoveAction = [this, sourceCardIndex](QMenu *targetMenu, const QString &targetZoneName,
|
||||
const QString &label, bool enabled) {
|
||||
QAction *action = targetMenu->addAction(label);
|
||||
action->setEnabled(enabled);
|
||||
if (enabled) {
|
||||
connect(action, &QAction::triggered, this, [this, sourceCardIndex, targetZoneName] {
|
||||
deckStateManager->moveCardToZone(sourceCardIndex, targetZoneName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const auto tree = deckStateManager->getDeckListShared()->getTree();
|
||||
|
||||
QMenu *moveMenu = menu->addMenu(tr("Move to &zone"));
|
||||
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
const QString boardLabel = InnerDecklistNode::visibleNameFromName(boardName);
|
||||
const auto customZones = tree->getCustomZones(boardName);
|
||||
|
||||
// Boards with zones nest their children so no two menu entries share a
|
||||
// visible name: "Maindeck ▸ { Maindeck (whole board), Removal, … }".
|
||||
// The board the card already lives on is marked instead of offered.
|
||||
if (!customZones.isEmpty()) {
|
||||
QMenu *boardSubmenu = moveMenu->addMenu(boardLabel);
|
||||
addMoveAction(boardSubmenu, boardName, boardLabel, boardName != currentZoneName);
|
||||
for (const auto *customZone : customZones) {
|
||||
addMoveAction(boardSubmenu, customZone->getName(), customZone->getName(),
|
||||
customZone->getName() != currentZoneName);
|
||||
}
|
||||
} else {
|
||||
addMoveAction(moveMenu, boardName, boardLabel, boardName != currentZoneName);
|
||||
}
|
||||
}
|
||||
|
||||
moveMenu->addSeparator();
|
||||
|
||||
QAction *newZoneAction = moveMenu->addAction(tr("Create new zone and move &here..."));
|
||||
connect(newZoneAction, &QAction::triggered, this, [this, sourceCardIndex, currentBoardName, currentZoneName] {
|
||||
// Resolve the card's identity before creating the zone:
|
||||
// createNewCustomZone rebuilds the model tree, so sourceCardIndex's
|
||||
// internal pointer is freed by the time it would be used.
|
||||
const QString cardName =
|
||||
sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
const QString providerId =
|
||||
sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString();
|
||||
const QString collectorNumber = sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_COLLECTOR_NUMBER)
|
||||
.data(Qt::DisplayRole)
|
||||
.toString();
|
||||
|
||||
const QString zoneName = createNewCustomZone(currentBoardName);
|
||||
if (!zoneName.isEmpty()) {
|
||||
// Re-find the card: the old index is no longer safe since rows were
|
||||
// rebuilt. Mirror DeckStateManager::decrementCard's re-find pattern.
|
||||
const QModelIndex refreshed = getModel()->findCard(cardName, currentZoneName, providerId, collectorNumber);
|
||||
if (refreshed.isValid()) {
|
||||
deckStateManager->moveCardToZone(refreshed, zoneName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::addChangeBoardMenu(QMenu *menu, const QString &zoneName)
|
||||
{
|
||||
const auto tree = deckStateManager->getDeckListShared()->getTree();
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
QAction *action = menu->addAction(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
|
||||
// The board currently holding the zone is marked instead of offered.
|
||||
// Duplicate names cannot come up through the editor, so this doubles as
|
||||
// the uniqueness guard for imported decks.
|
||||
bool holdsTheZone = false;
|
||||
for (const auto *customZone : tree->getCustomZones(boardName)) {
|
||||
if (customZone->getName() == zoneName) {
|
||||
holdsTheZone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (holdsTheZone) {
|
||||
action->setCheckable(true);
|
||||
action->setChecked(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
connect(action, &QAction::triggered, this,
|
||||
[this, zoneName, boardName] { deckStateManager->moveCustomZone(zoneName, boardName); });
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::addNewZoneAction(QMenu *menu, const QString &initialBoardName)
|
||||
{
|
||||
QAction *newZoneAction = menu->addAction(tr("Create &new zone..."));
|
||||
connect(newZoneAction, &QAction::triggered, this,
|
||||
[this, initialBoardName] { createNewCustomZone(initialBoardName); });
|
||||
}
|
||||
|
||||
QString DeckEditorDeckDockWidget::createNewCustomZone(const QString &initialBoardName)
|
||||
{
|
||||
QString boardName;
|
||||
const QString zoneName =
|
||||
DeckZoneDialog::promptForNewZone(this, initialBoardName, &boardName, [this](const QString &candidate) {
|
||||
return deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!zoneName.isEmpty()) {
|
||||
deckStateManager->createCustomZone(boardName, zoneName);
|
||||
}
|
||||
return zoneName;
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::refreshShortcuts()
|
||||
{
|
||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@
|
|||
#include "deck_list_history_manager_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
|
@ -99,6 +103,11 @@ private:
|
|||
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
|
||||
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
|
||||
|
||||
void addMoveToZoneMenu(QMenu *menu, const QModelIndex &sourceCardIndex, const QString ¤tBoardName);
|
||||
void addChangeBoardMenu(QMenu *menu, const QString &zoneName);
|
||||
QString createNewCustomZone(const QString &initialBoardName = {});
|
||||
void addNewZoneAction(QMenu *menu, const QString &initialBoardName = {});
|
||||
|
||||
private slots:
|
||||
void decklistCustomMenu(QPoint point);
|
||||
void updateCard(QModelIndex, const QModelIndex ¤t);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
DeckStateManager::DeckStateManager(QObject *parent)
|
||||
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
||||
|
|
@ -307,6 +308,170 @@ bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx)
|
|||
return offsetCountAtIndex(idx, -1);
|
||||
}
|
||||
|
||||
bool DeckStateManager::moveCardToZone(const QModelIndex &idx, const QString &targetZoneName)
|
||||
{
|
||||
if (!idx.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only actual card rows can be moved. Group or zone rows report an
|
||||
// aggregate amount and must never be deleted by this operation.
|
||||
if (!idx.data(DeckRoles::IsCardRole).toBool()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString();
|
||||
int copies = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
|
||||
|
||||
if (copies <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tokens only live in the tokens zone and cannot be moved into decks.
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
if (info && info->getIsToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the zone the card currently lives in: the enclosing custom
|
||||
// zone, or the nearest top-level zone (board zone or legacy zone).
|
||||
QString currentZoneName;
|
||||
for (QModelIndex ancestor = idx.parent(); ancestor.isValid(); ancestor = ancestor.parent()) {
|
||||
bool isCustomZone = ancestor.data(DeckRoles::IsCustomZoneRole).toBool();
|
||||
if (isCustomZone || !ancestor.parent().isValid()) {
|
||||
currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentZoneName == targetZoneName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString reason = tr("Moved %1 × \"%2\" (%3) to %4")
|
||||
.arg(copies)
|
||||
.arg(cardName)
|
||||
.arg(providerId)
|
||||
.arg(InnerDecklistNode::visibleNameFromName(targetZoneName));
|
||||
|
||||
return modifyDeck(reason, [&idx, &cardName, &providerId, &targetZoneName, copies](auto model) {
|
||||
if (!model->removeRow(idx.row(), idx.parent())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId})) {
|
||||
for (int i = 0; i < copies; ++i) {
|
||||
model->addCard(card, targetZoneName);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < copies; ++i) {
|
||||
model->addPreferredPrintingCard(cardName, targetZoneName, true);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::createCustomZone(const QString &boardZoneName, const QString &zoneName)
|
||||
{
|
||||
const QString trimmedZoneName = zoneName.trimmed();
|
||||
if (trimmedZoneName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString reason =
|
||||
tr("Created zone \"%1\" in %2").arg(trimmedZoneName, InnerDecklistNode::visibleNameFromName(boardZoneName));
|
||||
|
||||
return modifyTree(reason, [&boardZoneName, &trimmedZoneName](DecklistNodeTree *tree) {
|
||||
return tree->addCustomZone(boardZoneName, trimmedZoneName) != nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::renameCustomZone(const QString &oldZoneName, const QString &newZoneName)
|
||||
{
|
||||
const QString trimmedNewZoneName = newZoneName.trimmed();
|
||||
if (trimmedNewZoneName.isEmpty() || oldZoneName == trimmedNewZoneName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString reason = tr("Renamed zone \"%1\" to \"%2\"").arg(oldZoneName, trimmedNewZoneName);
|
||||
|
||||
return modifyTree(reason, [&oldZoneName, &trimmedNewZoneName](DecklistNodeTree *tree) {
|
||||
return tree->renameCustomZone(oldZoneName, trimmedNewZoneName);
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::moveCustomZone(const QString &zoneName, const QString &newBoardZoneName)
|
||||
{
|
||||
const auto *tree = deckList->getTree();
|
||||
|
||||
// Locate the zone through the tree's own lookup, which walks every top-level
|
||||
// zone (not just the standard boards) and covers the same-board no-op below.
|
||||
const auto *zone = tree->findCustomZoneByName(zoneName);
|
||||
if (!zone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same-board moves are no-ops and must not pollute the history.
|
||||
const QString currentBoardName = zone->getParent() ? zone->getParent()->getName() : QString();
|
||||
if (currentBoardName == newBoardZoneName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Zone names are deck-unique among zones created through this manager, so a
|
||||
// same-named zone on the target board can only come from an imported deck.
|
||||
// Refuse the move instead of silently stacking same-named zones.
|
||||
for (const auto *targetZone : tree->getCustomZones(newBoardZoneName)) {
|
||||
if (targetZone->getName() == zoneName) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString reason =
|
||||
tr("Moved zone \"%1\" to %2").arg(zoneName, InnerDecklistNode::visibleNameFromName(newBoardZoneName));
|
||||
|
||||
return modifyTree(reason, [&zoneName, &newBoardZoneName](DecklistNodeTree *tree) {
|
||||
return tree->moveCustomZone(zoneName, newBoardZoneName);
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::removeCustomZone(const QString &zoneName)
|
||||
{
|
||||
QString reason = tr("Deleted zone \"%1\"").arg(zoneName);
|
||||
|
||||
return modifyTree(reason, [&zoneName](DecklistNodeTree *tree) { return tree->removeCustomZone(zoneName); });
|
||||
}
|
||||
|
||||
QString DeckStateManager::validateNewZoneName(const QString &zoneName) const
|
||||
{
|
||||
if (zoneName.trimmed().isEmpty()) {
|
||||
return tr("Enter a zone name.");
|
||||
}
|
||||
|
||||
const QString trimmedZoneName = zoneName.trimmed();
|
||||
|
||||
// The standard zone names are reserved even before they exist.
|
||||
if (trimmedZoneName == DECK_ZONE_MAIN || trimmedZoneName == DECK_ZONE_SIDE ||
|
||||
trimmedZoneName == DECK_ZONE_MAYBEBOARD || trimmedZoneName == DECK_ZONE_TOKENS) {
|
||||
return tr("This name is reserved.");
|
||||
}
|
||||
|
||||
const auto *tree = deckList->getTree();
|
||||
|
||||
// Reuse the tree's own uniqueness contract: any top-level zone and any
|
||||
// custom zone on *every* board claims the name (hasZoneName also reserves
|
||||
// the standard board names, which we already rejected with a dedicated
|
||||
// message above). Scanning only the standard boards here would miss a
|
||||
// custom zone an imported deck carries under `tokens`.
|
||||
if (tree->hasZoneName(trimmedZoneName)) {
|
||||
return tr("A zone with this name already exists.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset)
|
||||
{
|
||||
if (!idx.isValid()) {
|
||||
|
|
@ -367,6 +532,25 @@ void DeckStateManager::requestHistorySave(const QString &reason)
|
|||
historyManager->save(deckList->createMemento(reason));
|
||||
}
|
||||
|
||||
bool DeckStateManager::modifyTree(const QString &reason, const std::function<bool(DecklistNodeTree *)> &operation)
|
||||
{
|
||||
DeckListMemento memento = deckList->createMemento(reason);
|
||||
bool success = operation(deckList->getTree());
|
||||
|
||||
if (success) {
|
||||
historyManager->save(memento);
|
||||
deckListModel->rebuildTree();
|
||||
deckList->refreshDeckHash();
|
||||
emit deckListModel->deckHashChanged();
|
||||
// removeCustomZone can drop whole card sets the model never notified
|
||||
// about (rebuildTree emits no cardNodesChanged), so tell the consumers.
|
||||
emit deckListModel->cardNodesChanged();
|
||||
doCardModified();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles updating state and emitting signals whenever the cards are modified
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "deck_list_model.h"
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <functional>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class DeckListHistoryManager;
|
||||
|
|
@ -236,6 +237,68 @@ public:
|
|||
*/
|
||||
bool decrementCountAtIndex(const QModelIndex &idx);
|
||||
|
||||
/**
|
||||
* @brief Moves all copies of the card at the given index to the given zone.
|
||||
* No-ops if the index is invalid, not a card node, the card is a token, or the
|
||||
* card is already in the target zone.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param idx The model index of the card to move
|
||||
* @param targetZoneName The zone to move the card to (board zone or custom zone name)
|
||||
* @return Whether the operation was successfully performed
|
||||
*/
|
||||
bool moveCardToZone(const QModelIndex &idx, const QString &targetZoneName);
|
||||
|
||||
/**
|
||||
* @brief Creates a new custom zone nested under a board zone.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param boardZoneName The board zone to nest the custom zone under
|
||||
* @param zoneName The name of the new custom zone. Gets trimmed and must be
|
||||
* unique across the deck.
|
||||
* @return Whether the zone was created
|
||||
*/
|
||||
bool createCustomZone(const QString &boardZoneName, const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Renames a custom zone.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param oldZoneName The current name of the custom zone
|
||||
* @param newZoneName The new name. Gets trimmed and must be unique across the deck.
|
||||
* @return Whether the rename succeeded
|
||||
*/
|
||||
bool renameCustomZone(const QString &oldZoneName, const QString &newZoneName);
|
||||
|
||||
/**
|
||||
* @brief Moves a custom zone (and its cards) to a different board zone.
|
||||
* Same-board moves succeed without creating a history entry.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param zoneName The custom zone to move
|
||||
* @param newBoardZoneName The board zone to move the custom zone under
|
||||
* @return Whether the move succeeded
|
||||
*/
|
||||
bool moveCustomZone(const QString &zoneName, const QString &newBoardZoneName);
|
||||
|
||||
/**
|
||||
* @brief Removes a custom zone and all its cards.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param zoneName The custom zone to remove
|
||||
* @return Whether the zone was removed
|
||||
*/
|
||||
bool removeCustomZone(const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Checks whether a candidate name is usable for a new custom zone.
|
||||
*
|
||||
* @param zoneName The candidate name
|
||||
* @return An empty string when the name is usable, otherwise a user-facing
|
||||
* error message describing the problem
|
||||
*/
|
||||
[[nodiscard]] QString validateNewZoneName(const QString &zoneName) const;
|
||||
|
||||
/**
|
||||
* Undoes n steps of the history, setting the decklist state and updating the current step in the historyManager.
|
||||
* @param steps Number of steps to undo.
|
||||
|
|
@ -257,6 +320,7 @@ public slots:
|
|||
|
||||
private:
|
||||
bool offsetCountAtIndex(const QModelIndex &idx, int offset);
|
||||
bool modifyTree(const QString &reason, const std::function<bool(DecklistNodeTree *)> &operation);
|
||||
void doCardModified();
|
||||
void doMetadataModified();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
#include "deck_zone_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
DeckZoneDialog::DeckZoneDialog(QWidget *parent,
|
||||
const QString &initialBoardName,
|
||||
const std::function<QString(const QString &)> &_nameValidator,
|
||||
bool _allowBoardSelection)
|
||||
: QDialog(parent), nameValidator(_nameValidator), allowBoardSelection(_allowBoardSelection)
|
||||
{
|
||||
nameLabel = new QLabel(this);
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
|
||||
errorLabel = new QLabel(this);
|
||||
errorLabel->hide();
|
||||
|
||||
boardLabel = new QLabel(this);
|
||||
boardCombo = new QComboBox(this);
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
// Use the icon overload explicitly so `boardName` lands in the user data role
|
||||
// (visible text is applied below in retranslateUi). The two-argument form
|
||||
// addItem({}, boardName) would be ambiguous and resolve to the icon overload
|
||||
// with empty user data, yielding empty entries and an empty getBoardName().
|
||||
boardCombo->addItem({}, {}, boardName);
|
||||
}
|
||||
if (!initialBoardName.isEmpty()) {
|
||||
int idx = boardCombo->findData(initialBoardName);
|
||||
if (idx != -1) {
|
||||
boardCombo->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(nameLabel);
|
||||
layout->addWidget(nameEdit);
|
||||
layout->addWidget(errorLabel);
|
||||
if (allowBoardSelection) {
|
||||
layout->addWidget(boardLabel);
|
||||
layout->addWidget(boardCombo);
|
||||
} else {
|
||||
boardLabel->hide();
|
||||
boardCombo->hide();
|
||||
}
|
||||
layout->addWidget(buttonBox);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
connect(nameEdit, &QLineEdit::textChanged, this, [this] { validateName(); });
|
||||
validateName();
|
||||
|
||||
nameEdit->setFocus();
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::getZoneName() const
|
||||
{
|
||||
return nameEdit->text().trimmed();
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::getBoardName() const
|
||||
{
|
||||
return boardCombo->currentData().toString();
|
||||
}
|
||||
|
||||
void DeckZoneDialog::setZoneName(const QString &zoneName)
|
||||
{
|
||||
nameEdit->setText(zoneName);
|
||||
nameEdit->selectAll();
|
||||
}
|
||||
|
||||
void DeckZoneDialog::changeEvent(QEvent *event)
|
||||
{
|
||||
QDialog::changeEvent(event);
|
||||
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
retranslateUi();
|
||||
}
|
||||
}
|
||||
|
||||
void DeckZoneDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(allowBoardSelection ? tr("New zone") : tr("Rename zone"));
|
||||
|
||||
nameLabel->setText(tr("Zone &name:"));
|
||||
nameLabel->setBuddy(nameEdit);
|
||||
|
||||
boardLabel->setText(tr("&Parent zone:"));
|
||||
boardLabel->setBuddy(boardCombo);
|
||||
|
||||
for (int i = 0; i < boardCombo->count(); i++) {
|
||||
boardCombo->setItemText(i, InnerDecklistNode::visibleNameFromName(boardCombo->itemData(i).toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckZoneDialog::validateName()
|
||||
{
|
||||
const QString zoneName = nameEdit->text().trimmed();
|
||||
QString error;
|
||||
if (zoneName.isEmpty()) {
|
||||
error = tr("Enter a zone name.");
|
||||
} else if (nameValidator) {
|
||||
error = nameValidator(zoneName);
|
||||
}
|
||||
|
||||
errorLabel->setText(error);
|
||||
errorLabel->setVisible(!error.isEmpty());
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty());
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::promptForNewZone(QWidget *parent,
|
||||
const QString &initialBoardName,
|
||||
QString *chosenBoardName,
|
||||
const std::function<QString(const QString &)> &nameValidator)
|
||||
{
|
||||
DeckZoneDialog dialog(parent, initialBoardName, nameValidator);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (chosenBoardName) {
|
||||
*chosenBoardName = dialog.getBoardName();
|
||||
}
|
||||
return dialog.getZoneName();
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::promptForRename(QWidget *parent,
|
||||
const QString ¤tZoneName,
|
||||
const std::function<QString(const QString &)> &nameValidator)
|
||||
{
|
||||
DeckZoneDialog dialog(parent, {}, nameValidator, false);
|
||||
dialog.setZoneName(currentZoneName);
|
||||
return dialog.exec() == QDialog::Accepted ? dialog.getZoneName() : QString();
|
||||
}
|
||||
123
cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
Normal file
123
cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* @file deck_zone_dialog.h
|
||||
* @ingroup DeckEditorWidgets
|
||||
* @brief Shared dialog for creating custom deck zones.
|
||||
*/
|
||||
|
||||
#ifndef DECK_ZONE_DIALOG_H
|
||||
#define DECK_ZONE_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QEvent>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
class QComboBox;
|
||||
class QDialogButtonBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QWidget;
|
||||
|
||||
/**
|
||||
* @brief Modal dialog asking for the name and parent zone of a new custom deck zone.
|
||||
*
|
||||
* Menus construct the dialog transiently around exec(), so validation state only
|
||||
* ever reflects the name currently typed.
|
||||
*/
|
||||
class DeckZoneDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the dialog and runs the initial validation pass.
|
||||
*
|
||||
* @param parent The parent widget for the dialog
|
||||
* @param initialBoardName The board zone to preselect in the combo. Unknown names
|
||||
* fall back to main.
|
||||
* @param _nameValidator Given the trimmed candidate name, returns an empty string
|
||||
* when it is usable, otherwise a user-facing error message. May be empty.
|
||||
* @param _allowBoardSelection When false the parent-zone combo is hidden and the
|
||||
* dialog acts as a rename prompt for an existing zone.
|
||||
*/
|
||||
explicit DeckZoneDialog(QWidget *parent = nullptr,
|
||||
const QString &initialBoardName = {},
|
||||
const std::function<QString(const QString &)> &_nameValidator = {},
|
||||
bool _allowBoardSelection = true);
|
||||
|
||||
/**
|
||||
* @brief The trimmed zone name entered by the user.
|
||||
*/
|
||||
[[nodiscard]] QString getZoneName() const;
|
||||
|
||||
/**
|
||||
* @brief The internal name of the board zone selected in the combo.
|
||||
*/
|
||||
[[nodiscard]] QString getBoardName() const;
|
||||
|
||||
/**
|
||||
* @brief Prefills the name field, e.g. with the current name when renaming.
|
||||
*
|
||||
* @param zoneName The text to put into the name field, selected for quick editing
|
||||
*/
|
||||
void setZoneName(const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Prompts the user for a new custom zone name and the board zone to nest it under.
|
||||
*
|
||||
* Convenience wrapper that runs DeckZoneDialog modally.
|
||||
*
|
||||
* @param parent The parent widget for the dialog
|
||||
* @param initialBoardName The board zone to preselect in the dialog. Unknown names fall
|
||||
* back to main.
|
||||
* @param chosenBoardName (out) The internal name of the board zone the user chose
|
||||
* @param nameValidator Optional validator forwarded to the dialog
|
||||
* @return The trimmed zone name, or an empty string if the user cancelled
|
||||
*/
|
||||
static QString promptForNewZone(QWidget *parent,
|
||||
const QString &initialBoardName,
|
||||
QString *chosenBoardName,
|
||||
const std::function<QString(const QString &)> &nameValidator = {});
|
||||
|
||||
/**
|
||||
* @brief Prompts the user for a new name for an existing custom zone.
|
||||
*
|
||||
* Same inline validation as promptForNewZone, but without a parent-zone picker.
|
||||
*
|
||||
* @param parent The parent widget for the dialog
|
||||
* @param currentZoneName The current name, prefilled for editing
|
||||
* @param nameValidator Validator deciding whether a candidate name is usable. It sees
|
||||
* the current name too, so callers wanting to allow unchanged names must
|
||||
* special-case that themselves.
|
||||
* @return The trimmed new name, or an empty string if the user cancelled
|
||||
*/
|
||||
static QString promptForRename(QWidget *parent,
|
||||
const QString ¤tZoneName,
|
||||
const std::function<QString(const QString &)> &nameValidator = {});
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Sets every user-visible string. Runs on construction and on runtime
|
||||
* language changes.
|
||||
*/
|
||||
void retranslateUi();
|
||||
|
||||
/**
|
||||
* @brief Validates the current input, toggling Ok and the inline error label.
|
||||
*/
|
||||
void validateName();
|
||||
|
||||
QLabel *nameLabel;
|
||||
QLineEdit *nameEdit;
|
||||
QLabel *errorLabel;
|
||||
QLabel *boardLabel;
|
||||
QComboBox *boardCombo;
|
||||
QDialogButtonBox *buttonBox;
|
||||
std::function<QString(const QString &)> nameValidator;
|
||||
bool allowBoardSelection;
|
||||
};
|
||||
|
||||
#endif // DECK_ZONE_DIALOG_H
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
#include "dlg_convert_deck_to_cod_format.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
|
|
@ -38,3 +46,71 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const
|
|||
{
|
||||
return dontAskAgainCheckbox->isChecked();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
|
||||
if (QFile::exists(newFileName)) {
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
return reply == QMessageBox::Yes;
|
||||
}
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent,
|
||||
const QString &filePath,
|
||||
const std::function<bool()> &convert)
|
||||
{
|
||||
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirmOverwriteIfExists(parent, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return convert();
|
||||
}
|
||||
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parent);
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
||||
!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(parent, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!convert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <functional>
|
||||
|
||||
class QWidget;
|
||||
|
||||
class DialogConvertDeckToCodFormat : public QDialog
|
||||
{
|
||||
|
|
@ -24,6 +27,21 @@ public:
|
|||
|
||||
[[nodiscard]] bool dontAskAgain() const;
|
||||
|
||||
/**
|
||||
* @brief Checks whether the deck file at \a filePath can store tags.
|
||||
*
|
||||
* If the file is not a .cod deck, prompts the user for conversion to the
|
||||
* Cockatrice format, honoring the saved "always convert / don't ask again"
|
||||
* preference. On acceptance \a convert is called to perform the conversion.
|
||||
*
|
||||
* @param parent The widget to parent the prompt to.
|
||||
* @param filePath The path of the deck file to check.
|
||||
* @param convert Called to convert the deck once the user agrees.
|
||||
* @return true if tags can be stored (no conversion needed, or the conversion
|
||||
* was performed), false if the user declined to convert.
|
||||
*/
|
||||
static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function<bool()> &convert);
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
QLabel *label;
|
||||
|
|
|
|||
|
|
@ -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 = tr("Downloading...");
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif // DLG_REGISTER_H
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation
|
|||
|
||||
// Set minimum height for the widget
|
||||
setMinimumHeight(50);
|
||||
setMaximumHeight(100);
|
||||
connect(this, &BannerWidget::buddyVisibilityChanged, this, &BannerWidget::toggleBuddyVisibility);
|
||||
|
||||
updateDropdownIconState();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
#ifndef COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
#define COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
|
||||
#include <QList>
|
||||
|
||||
namespace HomeTabButtonColor
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Where to get the colors for the home tab buttons from
|
||||
*/
|
||||
enum Source
|
||||
{
|
||||
Automatic, ///< Extract color from background, or use theme color if no background
|
||||
FromBackground, ///< Always extract color from background
|
||||
};
|
||||
|
||||
struct Entry
|
||||
{
|
||||
Source source;
|
||||
const char *trKey; ///< key for translation
|
||||
};
|
||||
|
||||
inline QList<Entry> all()
|
||||
{
|
||||
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
|
||||
{FromBackground, QT_TR_NOOP("Extract from background")}};
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts an int into the corresponding Source.
|
||||
*
|
||||
* @param value The int value
|
||||
* @return The Source. Returns Source::Automatic if the value is not within range
|
||||
*/
|
||||
inline Source intToSource(int value)
|
||||
{
|
||||
if (value > FromBackground) {
|
||||
return Automatic; // default
|
||||
}
|
||||
|
||||
return static_cast<Source>(value);
|
||||
}
|
||||
|
||||
} // namespace HomeTabButtonColor
|
||||
|
||||
#endif // COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "../cards/art_crop_attribution.h"
|
||||
#include "background_sources.h"
|
||||
#include "home_styled_button.h"
|
||||
#include "home_tab_button_color.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QPainter>
|
||||
|
|
@ -25,7 +26,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
|
||||
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
||||
|
||||
gradientColors = extractDominantColors(background);
|
||||
gradientColors = determineButtonColor();
|
||||
|
||||
layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter);
|
||||
|
||||
|
|
@ -55,6 +56,8 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
}
|
||||
|
||||
void HomeWidget::initializeBackgroundFromSource()
|
||||
|
|
@ -97,6 +100,34 @@ void HomeWidget::loadBackgroundSourceDeck()
|
|||
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
||||
}
|
||||
|
||||
static bool isDefaultBackgroundAndTheme()
|
||||
{
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
|
||||
}
|
||||
|
||||
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
||||
{
|
||||
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
|
||||
|
||||
auto colorSource =
|
||||
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
||||
|
||||
switch (colorSource) {
|
||||
case HomeTabButtonColor::Automatic: {
|
||||
if (isDefaultBackgroundAndTheme()) {
|
||||
return defaultColor;
|
||||
} else {
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
}
|
||||
case HomeTabButtonColor::FromBackground:
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
void HomeWidget::setRandomCard(ExactCard &newCard)
|
||||
{
|
||||
static constexpr int ATTEMPTS = 10;
|
||||
|
|
@ -171,7 +202,7 @@ void HomeWidget::updateBackgroundProperties()
|
|||
|
||||
void HomeWidget::updateButtonsToBackgroundColor()
|
||||
{
|
||||
gradientColors = extractDominantColors(background);
|
||||
gradientColors = determineButtonColor();
|
||||
for (HomeStyledButton *button : findChildren<HomeStyledButton *>()) {
|
||||
button->updateStylesheet(gradientColors);
|
||||
button->update();
|
||||
|
|
@ -266,11 +297,6 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
|
|||
|
||||
QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)
|
||||
{
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
if (themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme) {
|
||||
return QPair<QColor, QColor>(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80));
|
||||
}
|
||||
|
||||
// Step 1: Downscale image for performance
|
||||
QImage image = pixmap.toImage()
|
||||
.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class HomeWidget : public QWidget
|
|||
public:
|
||||
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
|
||||
void updateRandomCard();
|
||||
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
static QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
|
||||
public slots:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
|
@ -47,6 +47,7 @@ private:
|
|||
|
||||
void setRandomCard(ExactCard &newCard);
|
||||
void loadBackgroundSourceDeck();
|
||||
QPair<QColor, QColor> determineButtonColor() const;
|
||||
};
|
||||
|
||||
#endif // HOME_WIDGET_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,314 @@
|
|||
#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 <QTimer>
|
||||
#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);
|
||||
|
||||
QWidget *wizardWindow = window();
|
||||
if (!wizardWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
windowSizeBeforeExpansion = wizardWindow->size();
|
||||
QTimer::singleShot(0, this, [wizardWindow] {
|
||||
wizardWindow->resize(wizardWindow->size().expandedTo(wizardWindow->sizeHint()));
|
||||
});
|
||||
} else {
|
||||
QTimer::singleShot(0, this, [this, wizardWindow] {
|
||||
wizardWindow->resize(wizardWindow->size().boundedTo(windowSizeBeforeExpansion));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,79 @@
|
|||
#ifndef CARD_DATABASE_SETUP_PAGE_H
|
||||
#define CARD_DATABASE_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
#include <QSize>
|
||||
|
||||
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;
|
||||
QSize windowSizeBeforeExpansion;
|
||||
};
|
||||
|
||||
#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,173 @@
|
|||
#include "preferences_setup_page.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../client/sound_engine.h"
|
||||
#include "libcockatrice/settings/interface_settings.h"
|
||||
#include "libcockatrice/settings/sound_settings.h"
|
||||
#include "libcockatrice/settings/tabs_settings.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
// The server destinations are omitted: during first run their tabs are not
|
||||
// open yet, and the wizard offers no way to fill in the server/room details.
|
||||
QList<StartupTab> wizardStartupTabOrder()
|
||||
{
|
||||
return {StartupTabHome, StartupTabVisualDeckStorage, StartupTabDeckStorage,
|
||||
StartupTabReplays, StartupTabDeckEditor, StartupTabVisualDeckEditor};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PreferencesSetupPage::PreferencesSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
auto *content = new QWidget;
|
||||
auto *contentLayout = new QVBoxLayout(content);
|
||||
|
||||
gameplayGroup = new QGroupBox(content);
|
||||
auto *gameplayLayout = new QVBoxLayout(gameplayGroup);
|
||||
contentLayout->addWidget(gameplayGroup);
|
||||
doubleClickToPlayCheckBox = new QCheckBox(gameplayGroup);
|
||||
horizontalHandCheckBox = new QCheckBox(gameplayGroup);
|
||||
playToStackCheckBox = new QCheckBox(gameplayGroup);
|
||||
gameplayLayout->addWidget(doubleClickToPlayCheckBox);
|
||||
gameplayLayout->addWidget(horizontalHandCheckBox);
|
||||
gameplayLayout->addWidget(playToStackCheckBox);
|
||||
|
||||
notificationsGroup = new QGroupBox(content);
|
||||
auto *notificationsLayout = new QVBoxLayout(notificationsGroup);
|
||||
contentLayout->addWidget(notificationsGroup);
|
||||
notificationsEnabledCheckBox = new QCheckBox(notificationsGroup);
|
||||
soundEnabledCheckBox = new QCheckBox(notificationsGroup);
|
||||
notificationsLayout->addWidget(notificationsEnabledCheckBox);
|
||||
notificationsLayout->addWidget(soundEnabledCheckBox);
|
||||
|
||||
startupGroup = new QGroupBox(content);
|
||||
auto *startupForm = new QFormLayout(startupGroup);
|
||||
contentLayout->addWidget(startupGroup);
|
||||
startupTabLabel = new QLabel(startupGroup);
|
||||
startupTabSelector = new QComboBox(startupGroup);
|
||||
startupTabSelector->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
for (StartupTab tab : wizardStartupTabOrder()) {
|
||||
startupTabSelector->addItem(QString(), tab); // texts set in retranslateUi
|
||||
}
|
||||
startupForm->addRow(startupTabLabel, startupTabSelector);
|
||||
|
||||
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(doubleClickToPlayCheckBox, &QCheckBox::toggled, &settings.userInterface(),
|
||||
&InterfaceSettings::setDoubleClickToPlay);
|
||||
connect(horizontalHandCheckBox, &QCheckBox::toggled, &settings.userInterface(),
|
||||
&InterfaceSettings::setHorizontalHand);
|
||||
connect(playToStackCheckBox, &QCheckBox::toggled, &settings.userInterface(), &InterfaceSettings::setPlayToStack);
|
||||
|
||||
connect(notificationsEnabledCheckBox, &QCheckBox::toggled, &settings.userInterface(),
|
||||
&InterfaceSettings::setNotificationsEnabled);
|
||||
connect(soundEnabledCheckBox, &QCheckBox::toggled, &settings.sound(), &SoundSettings::setSoundEnabled);
|
||||
connect(soundEnabledCheckBox, &QCheckBox::toggled, soundEngine, &SoundEngine::testSound);
|
||||
|
||||
connect(startupTabSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
SettingsCache::instance().tabs().setStartupTabIndex(startupTabSelector->itemData(index).toInt());
|
||||
});
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PreferencesSetupPage::initializePage()
|
||||
{
|
||||
SettingsCache &settings = SettingsCache::instance();
|
||||
|
||||
doubleClickToPlayCheckBox->setChecked(settings.userInterface().getDoubleClickToPlay());
|
||||
horizontalHandCheckBox->setChecked(settings.userInterface().getHorizontalHand());
|
||||
playToStackCheckBox->setChecked(settings.userInterface().getPlayToStack());
|
||||
|
||||
notificationsEnabledCheckBox->setChecked(settings.userInterface().getNotificationsEnabled());
|
||||
soundEnabledCheckBox->setChecked(settings.sound().getSoundEnabled());
|
||||
|
||||
startupTabSelector->setCurrentIndex(startupTabSelector->findData(settings.tabs().getStartupTabIndex()));
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
gameplayGroup->setTitle(tr("Gameplay"));
|
||||
doubleClickToPlayCheckBox->setText(tr("Double-click cards to play them"));
|
||||
doubleClickToPlayCheckBox->setToolTip(tr("When disabled, a single click plays the selected card onto the table."));
|
||||
horizontalHandCheckBox->setText(tr("Display hand horizontally"));
|
||||
horizontalHandCheckBox->setToolTip(
|
||||
tr("Shows your hand as a row along the bottom of the table instead of a column beside it."));
|
||||
playToStackCheckBox->setText(tr("Play all nonlands onto the stack by default"));
|
||||
playToStackCheckBox->setToolTip(
|
||||
tr("Cards you play appear on the stack so other players can respond to them, as in a tabletop game."));
|
||||
|
||||
notificationsGroup->setTitle(tr("Notifications && Sound"));
|
||||
notificationsEnabledCheckBox->setText(tr("Show desktop notifications"));
|
||||
soundEnabledCheckBox->setText(tr("Play sound effects"));
|
||||
|
||||
startupGroup->setTitle(tr("Startup"));
|
||||
startupTabLabel->setText(tr("Startup tab:"));
|
||||
const QList<StartupTab> tabs = wizardStartupTabOrder();
|
||||
for (int i = 0; i < tabs.size(); ++i) {
|
||||
QString name;
|
||||
switch (tabs[i]) {
|
||||
case StartupTabHome:
|
||||
name = tr("Home");
|
||||
break;
|
||||
case StartupTabVisualDeckStorage:
|
||||
name = tr("Visual Deck Storage");
|
||||
break;
|
||||
case StartupTabDeckStorage:
|
||||
name = tr("Deck Storage");
|
||||
break;
|
||||
case StartupTabReplays:
|
||||
name = tr("Game Replays");
|
||||
break;
|
||||
case StartupTabDeckEditor:
|
||||
name = tr("Deck Editor");
|
||||
break;
|
||||
case StartupTabVisualDeckEditor:
|
||||
name = tr("Visual Deck Editor");
|
||||
break;
|
||||
case StartupTabServer:
|
||||
name = tr("Server");
|
||||
break;
|
||||
case StartupTabServerRoom:
|
||||
name = tr("Server Room");
|
||||
break;
|
||||
}
|
||||
startupTabSelector->setItemText(i, name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef PREFERENCES_SETUP_PAGE_H
|
||||
#define PREFERENCES_SETUP_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
|
||||
/** @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 *gameplayGroup;
|
||||
QCheckBox *doubleClickToPlayCheckBox;
|
||||
QCheckBox *horizontalHandCheckBox;
|
||||
QCheckBox *playToStackCheckBox;
|
||||
|
||||
QGroupBox *notificationsGroup;
|
||||
QCheckBox *notificationsEnabledCheckBox;
|
||||
QCheckBox *soundEnabledCheckBox;
|
||||
|
||||
QGroupBox *startupGroup;
|
||||
QLabel *startupTabLabel;
|
||||
QComboBox *startupTabSelector;
|
||||
};
|
||||
|
||||
#endif // PREFERENCES_SETUP_PAGE_H
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
#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 "libcockatrice/settings/appearance_settings.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().appearance().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().appearance().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,79 @@
|
|||
#include "welcome_page.h"
|
||||
|
||||
#include "../../../../main.h"
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../settings_page/general_settings_page.h"
|
||||
#include "libcockatrice/settings/personal_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLocale>
|
||||
#include <QTranslator>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
WelcomePage::WelcomePage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||
{
|
||||
bodyLabel = new QLabel(this);
|
||||
bodyLabel->setWordWrap(true);
|
||||
bodyLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
languageLabel = new QLabel(this);
|
||||
langCombo = new QComboBox(this);
|
||||
for (const QString &code : GeneralSettingsPage::findQmFiles()) {
|
||||
langCombo->addItem(GeneralSettingsPage::languageName(code), code);
|
||||
}
|
||||
|
||||
QString current = SettingsCache::instance().personal().getLang();
|
||||
if (current.isEmpty()) {
|
||||
current = QLocale::system().name();
|
||||
}
|
||||
int index = langCombo->findData(current);
|
||||
if (index < 0) {
|
||||
index = langCombo->findData(current.section('_', 0, 0));
|
||||
}
|
||||
if (index >= 0) {
|
||||
langCombo->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
connect(langCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &WelcomePage::languageChanged);
|
||||
|
||||
auto *languageRow = new QHBoxLayout;
|
||||
languageRow->addStretch();
|
||||
languageRow->addWidget(languageLabel);
|
||||
languageRow->addWidget(langCombo);
|
||||
languageRow->addStretch();
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addStretch();
|
||||
layout->addWidget(bodyLabel);
|
||||
layout->addStretch();
|
||||
layout->addLayout(languageRow);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void WelcomePage::languageChanged(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
SettingsCache::instance().personal().setLang(langCombo->itemData(index).toString());
|
||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
installNewTranslator();
|
||||
}
|
||||
|
||||
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."));
|
||||
languageLabel->setText(tr("Language:"));
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef WELCOME_PAGE_H
|
||||
#define WELCOME_PAGE_H
|
||||
|
||||
#include "../first_run_wizard_page.h"
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
|
||||
class WelcomePage : public FirstRunWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WelcomePage(QWidget *parent = nullptr);
|
||||
|
||||
QString stepTitle() const override;
|
||||
void retranslateUi() override;
|
||||
|
||||
private slots:
|
||||
void languageChanged(int index);
|
||||
|
||||
private:
|
||||
QLabel *bodyLabel;
|
||||
QLabel *languageLabel;
|
||||
QComboBox *langCombo;
|
||||
};
|
||||
|
||||
#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
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#include "printing_selector_card_overlay_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "printing_selector_card_display_widget.h"
|
||||
|
||||
#include <QImageReader>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
#include "../chat_view/chat_view.h"
|
||||
#include "../game_selector.h"
|
||||
#include "user_info_box.h"
|
||||
#include "user_list_dialog.h"
|
||||
#include "user_list_manager.h"
|
||||
#include "user_list_proxy.h"
|
||||
#include "user_list_widget.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QMenu>
|
||||
|
|
|
|||
|
|
@ -525,6 +525,13 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
||||
add(games);
|
||||
|
||||
// ── Invite (only while the inviter has a joinable game for this user) ────
|
||||
if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) {
|
||||
auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme);
|
||||
connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); });
|
||||
add(invite);
|
||||
}
|
||||
|
||||
// ── Buddy / ignore (registered users only) ────────────────────────────────
|
||||
if (!isSelf && isReg) {
|
||||
if (isBuddy) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <QMap>
|
||||
#include <QPixmap>
|
||||
#include <QStandardItemModel>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||
|
|
@ -149,6 +150,17 @@ public:
|
|||
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
|
||||
void refreshHeader();
|
||||
|
||||
/**
|
||||
* Sets a predicate evaluated on every action-button rebuild. It receives
|
||||
* the name of the user the popup currently shows; when it returns true an
|
||||
* "Invite" button is shown. The popup itself never resolves the invite
|
||||
* link, it just forwards the request.
|
||||
*/
|
||||
void setGameInviteAvailable(std::function<bool(const QString &userName)> available)
|
||||
{
|
||||
gameInviteAvailable = std::move(available);
|
||||
}
|
||||
|
||||
signals:
|
||||
void mouseEnteredPopup();
|
||||
void mouseLeftPopup();
|
||||
|
|
@ -159,6 +171,7 @@ signals:
|
|||
|
||||
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
|
||||
void chatRequested(const QString &userName);
|
||||
void inviteRequested(const QString &userName);
|
||||
void detailsRequested(const QString &userName);
|
||||
void showGamesRequested(const QString &userName);
|
||||
void addBuddyRequested(const QString &userName);
|
||||
|
|
@ -200,6 +213,7 @@ private:
|
|||
QString currentUser;
|
||||
ServerInfo_User currentUserInfo;
|
||||
bool currentOnline = false;
|
||||
std::function<bool(const QString &userName)> gameInviteAvailable;
|
||||
|
||||
UserInfoHeaderWidget *header;
|
||||
QWidget *actionArea; ///< rebuilt per user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,302 @@
|
|||
#include "user_list_dialog.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
nameBanCheckBox = new QCheckBox(tr("ban &user name"));
|
||||
nameBanCheckBox->setChecked(true);
|
||||
nameBanEdit = new QLineEdit(QString::fromStdString(info.name()));
|
||||
nameBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
ipBanCheckBox = new QCheckBox(tr("ban &IP address"));
|
||||
ipBanCheckBox->setChecked(true);
|
||||
ipBanEdit = new QLineEdit(QString::fromStdString(info.address()));
|
||||
ipBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
idBanCheckBox = new QCheckBox(tr("ban client I&D"));
|
||||
idBanCheckBox->setChecked(true);
|
||||
idBanEdit = new QLineEdit(QString::fromStdString(info.clientid()));
|
||||
idBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
if (QString::fromStdString(info.clientid()).isEmpty()) {
|
||||
idBanCheckBox->setChecked(false);
|
||||
}
|
||||
|
||||
QGridLayout *banTypeGrid = new QGridLayout;
|
||||
banTypeGrid->addWidget(nameBanCheckBox, 0, 0);
|
||||
banTypeGrid->addWidget(nameBanEdit, 0, 1);
|
||||
banTypeGrid->addWidget(ipBanCheckBox, 1, 0);
|
||||
banTypeGrid->addWidget(ipBanEdit, 1, 1);
|
||||
banTypeGrid->addWidget(idBanCheckBox, 2, 0);
|
||||
banTypeGrid->addWidget(idBanEdit, 2, 1);
|
||||
QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type"));
|
||||
banTypeGroupBox->setLayout(banTypeGrid);
|
||||
|
||||
permanentRadio = new QRadioButton(tr("&permanent ban"));
|
||||
temporaryRadio = new QRadioButton(tr("&temporary ban"));
|
||||
temporaryRadio->setChecked(true);
|
||||
connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits);
|
||||
daysLabel = new QLabel(tr("&Days:"));
|
||||
daysEdit = new QSpinBox;
|
||||
daysEdit->setMinimum(0);
|
||||
daysEdit->setValue(0);
|
||||
daysEdit->setMaximum(10000);
|
||||
daysLabel->setBuddy(daysEdit);
|
||||
hoursLabel = new QLabel(tr("&Hours:"));
|
||||
hoursEdit = new QSpinBox;
|
||||
hoursEdit->setMinimum(0);
|
||||
hoursEdit->setValue(0);
|
||||
hoursEdit->setMaximum(24);
|
||||
hoursLabel->setBuddy(hoursEdit);
|
||||
minutesLabel = new QLabel(tr("&Minutes:"));
|
||||
minutesEdit = new QSpinBox;
|
||||
minutesEdit->setMinimum(0);
|
||||
minutesEdit->setValue(5);
|
||||
minutesEdit->setMaximum(60);
|
||||
minutesLabel->setBuddy(minutesEdit);
|
||||
QGridLayout *durationLayout = new QGridLayout;
|
||||
durationLayout->addWidget(permanentRadio, 0, 0, 1, 6);
|
||||
durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6);
|
||||
durationLayout->addWidget(daysLabel, 2, 0);
|
||||
durationLayout->addWidget(daysEdit, 2, 1);
|
||||
durationLayout->addWidget(hoursLabel, 2, 2);
|
||||
durationLayout->addWidget(hoursEdit, 2, 3);
|
||||
durationLayout->addWidget(minutesLabel, 2, 4);
|
||||
durationLayout->addWidget(minutesEdit, 2, 5);
|
||||
QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban"));
|
||||
durationGroupBox->setLayout(durationLayout);
|
||||
|
||||
QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\n"
|
||||
"This is only saved for moderators and cannot be seen by the banned person."));
|
||||
reasonEdit = new QPlainTextEdit;
|
||||
|
||||
QLabel *visibleReasonLabel =
|
||||
new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person."));
|
||||
visibleReasonEdit = new QPlainTextEdit;
|
||||
|
||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||
|
||||
QPushButton *okButton = new QPushButton(tr("&OK"));
|
||||
okButton->setAutoDefault(true);
|
||||
connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked);
|
||||
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
||||
connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(okButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(banTypeGroupBox);
|
||||
vbox->addWidget(durationGroupBox);
|
||||
vbox->addWidget(reasonLabel);
|
||||
vbox->addWidget(reasonEdit);
|
||||
vbox->addWidget(visibleReasonLabel);
|
||||
vbox->addWidget(visibleReasonEdit);
|
||||
vbox->addWidget(deleteMessages);
|
||||
vbox->addLayout(buttonLayout);
|
||||
|
||||
setLayout(vbox);
|
||||
setWindowTitle(tr("Ban user from server"));
|
||||
}
|
||||
|
||||
WarningDialog::WarningDialog(const QString &userName, const QString &clientID, QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
descriptionLabel = new QLabel(tr("Which warning would you like to send?"));
|
||||
nameWarning = new QLineEdit(userName);
|
||||
nameWarning->setMaxLength(MAX_NAME_LENGTH);
|
||||
warnClientID = new QLineEdit(clientID);
|
||||
warnClientID->setMaxLength(MAX_NAME_LENGTH);
|
||||
warningOption = new QComboBox();
|
||||
warningOption->addItem("", "");
|
||||
|
||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||
|
||||
QPushButton *okButton = new QPushButton(tr("&OK"));
|
||||
okButton->setAutoDefault(true);
|
||||
connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked);
|
||||
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
||||
connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(okButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(descriptionLabel);
|
||||
vbox->addWidget(nameWarning);
|
||||
vbox->addWidget(warningOption);
|
||||
vbox->addWidget(deleteMessages);
|
||||
vbox->addLayout(buttonLayout);
|
||||
setLayout(vbox);
|
||||
setWindowTitle(tr("Warn user for misconduct"));
|
||||
}
|
||||
|
||||
void WarningDialog::okClicked()
|
||||
{
|
||||
if (nameWarning->text().simplified().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("User name to send a warning to can not be blank, please specify a user to warn."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (warningOption->currentData().toString().simplified().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Warning to use can not be blank, please select a valid warning to send."));
|
||||
return;
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
QString WarningDialog::getName() const
|
||||
{
|
||||
return nameWarning->text().simplified();
|
||||
}
|
||||
|
||||
QString WarningDialog::getWarnID() const
|
||||
{
|
||||
return warnClientID->text().simplified();
|
||||
}
|
||||
|
||||
QString WarningDialog::getReason() const
|
||||
{
|
||||
return warningOption->currentData().toString().simplified();
|
||||
}
|
||||
|
||||
int WarningDialog::getDeleteMessages() const
|
||||
{
|
||||
return deleteMessages->isChecked() ? -1 : 0;
|
||||
}
|
||||
|
||||
void WarningDialog::addWarningOption(const QString &warning, int startingIl)
|
||||
{
|
||||
if (startingIl > 1) {
|
||||
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning);
|
||||
} else {
|
||||
warningOption->addItem(warning, warning);
|
||||
}
|
||||
}
|
||||
|
||||
void BanDialog::okClicked()
|
||||
{
|
||||
if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You have to select a name-based, IP-based, clientId based, or some combination of "
|
||||
"the three to place a ban."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (nameBanCheckBox->isChecked()) {
|
||||
if (nameBanEdit->text().simplified() == "") {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You must have a value in the name ban when selecting the name ban checkbox."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ipBanCheckBox->isChecked()) {
|
||||
if (ipBanEdit->text().simplified() == "") {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You must have a value in the ip ban when selecting the ip ban checkbox."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (idBanCheckBox->isChecked()) {
|
||||
if (idBanEdit->text().simplified() == "") {
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("You must have a value in the clientid ban when selecting the clientid ban checkbox."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void BanDialog::enableTemporaryEdits(bool enabled)
|
||||
{
|
||||
daysLabel->setEnabled(enabled);
|
||||
daysEdit->setEnabled(enabled);
|
||||
hoursLabel->setEnabled(enabled);
|
||||
hoursEdit->setEnabled(enabled);
|
||||
minutesLabel->setEnabled(enabled);
|
||||
minutesEdit->setEnabled(enabled);
|
||||
}
|
||||
|
||||
QString BanDialog::getBanId() const
|
||||
{
|
||||
return idBanCheckBox->isChecked() ? idBanEdit->text() : QString();
|
||||
}
|
||||
|
||||
QString BanDialog::getBanName() const
|
||||
{
|
||||
return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString();
|
||||
}
|
||||
|
||||
QString BanDialog::getBanIP() const
|
||||
{
|
||||
return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString();
|
||||
}
|
||||
|
||||
int BanDialog::getMinutes() const
|
||||
{
|
||||
return permanentRadio->isChecked() ? 0
|
||||
: (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value());
|
||||
}
|
||||
|
||||
QString BanDialog::getReason() const
|
||||
{
|
||||
return reasonEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString BanDialog::getVisibleReason() const
|
||||
{
|
||||
return visibleReasonEdit->toPlainText();
|
||||
}
|
||||
|
||||
int BanDialog::getDeleteMessages() const
|
||||
{
|
||||
return deleteMessages->isChecked() ? -1 : 0;
|
||||
}
|
||||
|
||||
AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent)
|
||||
: QDialog(_parent), userName(_userName)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
auto *updateButton = new QPushButton(tr("Update Notes"));
|
||||
updateButton->setEnabled(false);
|
||||
connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept);
|
||||
|
||||
notes = new QPlainTextEdit(_notes);
|
||||
notes->setMinimumWidth(500);
|
||||
connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); });
|
||||
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(notes);
|
||||
vbox->addWidget(updateButton);
|
||||
|
||||
setLayout(vbox);
|
||||
setWindowTitle(tr("Admin Notes for %1").arg(_userName));
|
||||
}
|
||||
|
||||
QString AdminNotesDialog::getNotes() const
|
||||
{
|
||||
return notes->toPlainText();
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
#ifndef COCKATRICE_USER_LIST_DIALOG_H
|
||||
#define COCKATRICE_USER_LIST_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QPlainTextEdit;
|
||||
class QRadioButton;
|
||||
class QSpinBox;
|
||||
class QLineEdit;
|
||||
class QCheckBox;
|
||||
|
||||
class BanDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QLabel *daysLabel, *hoursLabel, *minutesLabel;
|
||||
QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages;
|
||||
QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit;
|
||||
QSpinBox *daysEdit, *hoursEdit, *minutesEdit;
|
||||
QRadioButton *permanentRadio, *temporaryRadio;
|
||||
QPlainTextEdit *reasonEdit, *visibleReasonEdit;
|
||||
|
||||
private slots:
|
||||
void okClicked();
|
||||
void enableTemporaryEdits(bool enabled);
|
||||
|
||||
public:
|
||||
explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr);
|
||||
[[nodiscard]] QString getBanName() const;
|
||||
[[nodiscard]] QString getBanIP() const;
|
||||
[[nodiscard]] QString getBanId() const;
|
||||
[[nodiscard]] int getMinutes() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] QString getVisibleReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
};
|
||||
|
||||
class WarningDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QLabel *descriptionLabel;
|
||||
QLineEdit *nameWarning;
|
||||
QComboBox *warningOption;
|
||||
QLineEdit *warnClientID;
|
||||
QCheckBox *deleteMessages;
|
||||
|
||||
private slots:
|
||||
void okClicked();
|
||||
|
||||
public:
|
||||
WarningDialog(const QString &userName, const QString &clientID, QWidget *parent = nullptr);
|
||||
[[nodiscard]] QString getName() const;
|
||||
[[nodiscard]] QString getWarnID() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
void addWarningOption(const QString &warning, int startingIl = 1);
|
||||
};
|
||||
|
||||
class AdminNotesDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString userName;
|
||||
QPlainTextEdit *notes;
|
||||
|
||||
public:
|
||||
explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr);
|
||||
[[nodiscard]] QString getName() const
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
[[nodiscard]] QString getNotes() const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_LIST_DIALOG_H
|
||||
|
|
@ -1,335 +1,21 @@
|
|||
#include "user_list_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../../cards/art_crop_attribution.h"
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../../interface/theme_manager.h"
|
||||
#include "../../interface/widgets/tabs/tab_account.h"
|
||||
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../game_selector.h"
|
||||
#include "user_context_menu.h"
|
||||
#include "user_list_painter.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QCursor>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QEvent>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QScreen>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
nameBanCheckBox = new QCheckBox(tr("ban &user name"));
|
||||
nameBanCheckBox->setChecked(true);
|
||||
nameBanEdit = new QLineEdit(QString::fromStdString(info.name()));
|
||||
nameBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
ipBanCheckBox = new QCheckBox(tr("ban &IP address"));
|
||||
ipBanCheckBox->setChecked(true);
|
||||
ipBanEdit = new QLineEdit(QString::fromStdString(info.address()));
|
||||
ipBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
idBanCheckBox = new QCheckBox(tr("ban client I&D"));
|
||||
idBanCheckBox->setChecked(true);
|
||||
idBanEdit = new QLineEdit(QString::fromStdString(info.clientid()));
|
||||
idBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
if (QString::fromStdString(info.clientid()).isEmpty()) {
|
||||
idBanCheckBox->setChecked(false);
|
||||
}
|
||||
|
||||
QGridLayout *banTypeGrid = new QGridLayout;
|
||||
banTypeGrid->addWidget(nameBanCheckBox, 0, 0);
|
||||
banTypeGrid->addWidget(nameBanEdit, 0, 1);
|
||||
banTypeGrid->addWidget(ipBanCheckBox, 1, 0);
|
||||
banTypeGrid->addWidget(ipBanEdit, 1, 1);
|
||||
banTypeGrid->addWidget(idBanCheckBox, 2, 0);
|
||||
banTypeGrid->addWidget(idBanEdit, 2, 1);
|
||||
QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type"));
|
||||
banTypeGroupBox->setLayout(banTypeGrid);
|
||||
|
||||
permanentRadio = new QRadioButton(tr("&permanent ban"));
|
||||
temporaryRadio = new QRadioButton(tr("&temporary ban"));
|
||||
temporaryRadio->setChecked(true);
|
||||
connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits);
|
||||
daysLabel = new QLabel(tr("&Days:"));
|
||||
daysEdit = new QSpinBox;
|
||||
daysEdit->setMinimum(0);
|
||||
daysEdit->setValue(0);
|
||||
daysEdit->setMaximum(10000);
|
||||
daysLabel->setBuddy(daysEdit);
|
||||
hoursLabel = new QLabel(tr("&Hours:"));
|
||||
hoursEdit = new QSpinBox;
|
||||
hoursEdit->setMinimum(0);
|
||||
hoursEdit->setValue(0);
|
||||
hoursEdit->setMaximum(24);
|
||||
hoursLabel->setBuddy(hoursEdit);
|
||||
minutesLabel = new QLabel(tr("&Minutes:"));
|
||||
minutesEdit = new QSpinBox;
|
||||
minutesEdit->setMinimum(0);
|
||||
minutesEdit->setValue(5);
|
||||
minutesEdit->setMaximum(60);
|
||||
minutesLabel->setBuddy(minutesEdit);
|
||||
QGridLayout *durationLayout = new QGridLayout;
|
||||
durationLayout->addWidget(permanentRadio, 0, 0, 1, 6);
|
||||
durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6);
|
||||
durationLayout->addWidget(daysLabel, 2, 0);
|
||||
durationLayout->addWidget(daysEdit, 2, 1);
|
||||
durationLayout->addWidget(hoursLabel, 2, 2);
|
||||
durationLayout->addWidget(hoursEdit, 2, 3);
|
||||
durationLayout->addWidget(minutesLabel, 2, 4);
|
||||
durationLayout->addWidget(minutesEdit, 2, 5);
|
||||
QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban"));
|
||||
durationGroupBox->setLayout(durationLayout);
|
||||
|
||||
QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\nThis is only saved for moderators and "
|
||||
"cannot be seen by the banned person."));
|
||||
reasonEdit = new QPlainTextEdit;
|
||||
|
||||
QLabel *visibleReasonLabel =
|
||||
new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person."));
|
||||
visibleReasonEdit = new QPlainTextEdit;
|
||||
|
||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||
|
||||
QPushButton *okButton = new QPushButton(tr("&OK"));
|
||||
okButton->setAutoDefault(true);
|
||||
connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked);
|
||||
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
||||
connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(okButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(banTypeGroupBox);
|
||||
vbox->addWidget(durationGroupBox);
|
||||
vbox->addWidget(reasonLabel);
|
||||
vbox->addWidget(reasonEdit);
|
||||
vbox->addWidget(visibleReasonLabel);
|
||||
vbox->addWidget(visibleReasonEdit);
|
||||
vbox->addWidget(deleteMessages);
|
||||
vbox->addLayout(buttonLayout);
|
||||
|
||||
setLayout(vbox);
|
||||
setWindowTitle(tr("Ban user from server"));
|
||||
}
|
||||
|
||||
WarningDialog::WarningDialog(const QString userName, const QString clientID, QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
descriptionLabel = new QLabel(tr("Which warning would you like to send?"));
|
||||
nameWarning = new QLineEdit(userName);
|
||||
nameWarning->setMaxLength(MAX_NAME_LENGTH);
|
||||
warnClientID = new QLineEdit(clientID);
|
||||
warnClientID->setMaxLength(MAX_NAME_LENGTH);
|
||||
warningOption = new QComboBox();
|
||||
warningOption->addItem("", "");
|
||||
|
||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||
|
||||
QPushButton *okButton = new QPushButton(tr("&OK"));
|
||||
okButton->setAutoDefault(true);
|
||||
connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked);
|
||||
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
||||
connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(okButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(descriptionLabel);
|
||||
vbox->addWidget(nameWarning);
|
||||
vbox->addWidget(warningOption);
|
||||
vbox->addWidget(deleteMessages);
|
||||
vbox->addLayout(buttonLayout);
|
||||
setLayout(vbox);
|
||||
setWindowTitle(tr("Warn user for misconduct"));
|
||||
}
|
||||
|
||||
void WarningDialog::okClicked()
|
||||
{
|
||||
if (nameWarning->text().simplified().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("User name to send a warning to can not be blank, please specify a user to warn."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (warningOption->currentData().toString().simplified().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Warning to use can not be blank, please select a valid warning to send."));
|
||||
return;
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
QString WarningDialog::getName() const
|
||||
{
|
||||
return nameWarning->text().simplified();
|
||||
}
|
||||
|
||||
QString WarningDialog::getWarnID() const
|
||||
{
|
||||
return warnClientID->text().simplified();
|
||||
}
|
||||
|
||||
QString WarningDialog::getReason() const
|
||||
{
|
||||
return warningOption->currentData().toString().simplified();
|
||||
}
|
||||
|
||||
int WarningDialog::getDeleteMessages() const
|
||||
{
|
||||
return deleteMessages->isChecked() ? -1 : 0;
|
||||
}
|
||||
|
||||
void WarningDialog::addWarningOption(const QString warning, int startingIl)
|
||||
{
|
||||
if (startingIl > 1) {
|
||||
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning);
|
||||
} else {
|
||||
warningOption->addItem(warning, warning);
|
||||
}
|
||||
}
|
||||
|
||||
void BanDialog::okClicked()
|
||||
{
|
||||
if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You have to select a name-based, IP-based, clientId based, or some combination of "
|
||||
"the three to place a ban."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (nameBanCheckBox->isChecked()) {
|
||||
if (nameBanEdit->text().simplified() == "") {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You must have a value in the name ban when selecting the name ban checkbox."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ipBanCheckBox->isChecked()) {
|
||||
if (ipBanEdit->text().simplified() == "") {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("You must have a value in the ip ban when selecting the ip ban checkbox."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (idBanCheckBox->isChecked()) {
|
||||
if (idBanEdit->text().simplified() == "") {
|
||||
QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("You must have a value in the clientid ban when selecting the clientid ban checkbox."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void BanDialog::enableTemporaryEdits(bool enabled)
|
||||
{
|
||||
daysLabel->setEnabled(enabled);
|
||||
daysEdit->setEnabled(enabled);
|
||||
hoursLabel->setEnabled(enabled);
|
||||
hoursEdit->setEnabled(enabled);
|
||||
minutesLabel->setEnabled(enabled);
|
||||
minutesEdit->setEnabled(enabled);
|
||||
}
|
||||
|
||||
QString BanDialog::getBanId() const
|
||||
{
|
||||
return idBanCheckBox->isChecked() ? idBanEdit->text() : QString();
|
||||
}
|
||||
|
||||
QString BanDialog::getBanName() const
|
||||
{
|
||||
return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString();
|
||||
}
|
||||
|
||||
QString BanDialog::getBanIP() const
|
||||
{
|
||||
return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString();
|
||||
}
|
||||
|
||||
int BanDialog::getMinutes() const
|
||||
{
|
||||
return permanentRadio->isChecked() ? 0
|
||||
: (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value());
|
||||
}
|
||||
|
||||
QString BanDialog::getReason() const
|
||||
{
|
||||
return reasonEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString BanDialog::getVisibleReason() const
|
||||
{
|
||||
return visibleReasonEdit->toPlainText();
|
||||
}
|
||||
|
||||
int BanDialog::getDeleteMessages() const
|
||||
{
|
||||
return deleteMessages->isChecked() ? -1 : 0;
|
||||
}
|
||||
|
||||
AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent)
|
||||
: QDialog(_parent), userName(_userName)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
auto *updateButton = new QPushButton(tr("Update Notes"));
|
||||
updateButton->setEnabled(false);
|
||||
connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept);
|
||||
|
||||
notes = new QPlainTextEdit(_notes);
|
||||
notes->setMinimumWidth(500);
|
||||
connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); });
|
||||
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(notes);
|
||||
vbox->addWidget(updateButton);
|
||||
|
||||
setLayout(vbox);
|
||||
setWindowTitle(tr("Admin Notes for %1").arg(_userName));
|
||||
}
|
||||
|
||||
QString AdminNotesDialog::getNotes() const
|
||||
{
|
||||
return notes->toPlainText();
|
||||
}
|
||||
|
||||
namespace UserListRoles
|
||||
{
|
||||
|
|
@ -613,7 +299,12 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
userTree->hideColumn(3);
|
||||
connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked);
|
||||
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
userTree->header()->setStretchLastSection(true);
|
||||
// QTreeWidget enables stretchLastSection by default. Left on, the hidden
|
||||
// last section absorbs viewport resizes, the Stretch sections never
|
||||
// redistribute, and the header keeps a stale length past the viewport —
|
||||
// an invisible horizontal pan range under ScrollBarAlwaysOff. Disable it
|
||||
// so the explicit resize modes in applyDisplayMode() own the geometry.
|
||||
userTree->header()->setStretchLastSection(false);
|
||||
|
||||
// Always create timers so callers never segfault on a null deref;
|
||||
// showPopupForUser / hidePopup already guard against a null userInfoPopup.
|
||||
|
|
@ -654,6 +345,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
&cardArtProvider->cache(), &cardArtParamsMap,
|
||||
window()); // parented to main window so it floats above siblings
|
||||
|
||||
// The invite availability is scoped to the room this list belongs to,
|
||||
// and gated on the room's buddy-only setting for the hovered user.
|
||||
userInfoPopup->setGameInviteAvailable(
|
||||
[this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); });
|
||||
|
||||
userInfoPopup->hide();
|
||||
userInfoPopup->setWindowOpacity(0.0);
|
||||
userInfoPopup->installEventFilter(this);
|
||||
|
|
@ -930,13 +626,24 @@ void UserListWidget::applyDisplayMode()
|
|||
{
|
||||
const bool styled = SettingsCache::instance().appearance().getStyleUserList();
|
||||
|
||||
// Both modes must keep the header length at the viewport width: with
|
||||
// ScrollBarAlwaysOff a nonzero horizontal range is invisible but still
|
||||
// pans via trackpad gestures, which reads as janky random drift.
|
||||
if (styled) {
|
||||
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
userTree->hideColumn(1);
|
||||
userTree->hideColumn(2);
|
||||
userTree->hideColumn(3);
|
||||
} else {
|
||||
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
// Bounded widths instead of ResizeToContents: content sizing measures
|
||||
// the FULL text width while the delegate elides afterwards, so long
|
||||
// names widened the header past the viewport. Fixed icon columns plus
|
||||
// a stretched name column keep the range at zero, eliding trims.
|
||||
userTree->header()->setSectionResizeMode(0, QHeaderView::Fixed);
|
||||
userTree->header()->resizeSection(0, 24);
|
||||
userTree->header()->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
userTree->header()->resizeSection(1, 22);
|
||||
userTree->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
userTree->showColumn(1);
|
||||
userTree->showColumn(2);
|
||||
userTree->hideColumn(3);
|
||||
|
|
@ -960,6 +667,8 @@ void UserListWidget::connectPopupSignals()
|
|||
|
||||
// Wire all action signals to UserContextMenu::exec*()
|
||||
connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
|
||||
connect(userInfoPopup, &UserInfoPopup::inviteRequested, this,
|
||||
[this](const QString &userName) { userContextMenu->execInvite(userName); });
|
||||
connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
|
||||
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
|
||||
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@
|
|||
#include "user_list_painter.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QQueue>
|
||||
#include <QSet>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTextEdit>
|
||||
|
|
@ -31,80 +29,12 @@ class QTreeWidget;
|
|||
class ServerInfo_User;
|
||||
class AbstractClient;
|
||||
class TabSupervisor;
|
||||
class QLabel;
|
||||
class QCheckBox;
|
||||
class QSpinBox;
|
||||
class QRadioButton;
|
||||
class QPlainTextEdit;
|
||||
class Response;
|
||||
class CommandContainer;
|
||||
class UserContextMenu;
|
||||
class UserListWidget;
|
||||
class QShowEvent;
|
||||
|
||||
class BanDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QLabel *daysLabel, *hoursLabel, *minutesLabel;
|
||||
QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages;
|
||||
QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit;
|
||||
QSpinBox *daysEdit, *hoursEdit, *minutesEdit;
|
||||
QRadioButton *permanentRadio, *temporaryRadio;
|
||||
QPlainTextEdit *reasonEdit, *visibleReasonEdit;
|
||||
private slots:
|
||||
void okClicked();
|
||||
void enableTemporaryEdits(bool enabled);
|
||||
|
||||
public:
|
||||
explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr);
|
||||
[[nodiscard]] QString getBanName() const;
|
||||
[[nodiscard]] QString getBanIP() const;
|
||||
[[nodiscard]] QString getBanId() const;
|
||||
[[nodiscard]] int getMinutes() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] QString getVisibleReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
};
|
||||
|
||||
class WarningDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QLabel *descriptionLabel;
|
||||
QLineEdit *nameWarning;
|
||||
QComboBox *warningOption;
|
||||
QLineEdit *warnClientID;
|
||||
QCheckBox *deleteMessages;
|
||||
private slots:
|
||||
void okClicked();
|
||||
|
||||
public:
|
||||
WarningDialog(const QString userName, const QString clientID, QWidget *parent = nullptr);
|
||||
[[nodiscard]] QString getName() const;
|
||||
[[nodiscard]] QString getWarnID() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
void addWarningOption(const QString warning, int startingIl = 1);
|
||||
};
|
||||
|
||||
class AdminNotesDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QString userName;
|
||||
QPlainTextEdit *notes;
|
||||
|
||||
public:
|
||||
explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr);
|
||||
[[nodiscard]] QString getName() const
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
[[nodiscard]] QString getNotes() const;
|
||||
};
|
||||
|
||||
class UserListItemDelegate : public QStyledItemDelegate
|
||||
{
|
||||
QTreeWidget *tree;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "../../client/settings/card_counter_settings.h"
|
||||
#include "../../palette_editor/palette_editor_dialog.h"
|
||||
#include "../dialogs/override_printing_warning.h"
|
||||
#include "../general/home_tab_button_color.h"
|
||||
#include "../interface/theme_manager.h"
|
||||
#include "../interface/widgets/general/background_sources.h"
|
||||
#include "../playmat/playmat_collection_dialog.h"
|
||||
|
|
@ -131,6 +132,14 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabDisplayCardName);
|
||||
|
||||
for (const auto &entry : HomeTabButtonColor::all()) {
|
||||
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
|
||||
}
|
||||
|
||||
homeTabButtonColorSourceBox.setCurrentIndex(settings.appearance().getHomeTabButtonColorSourceIndex());
|
||||
connect(&homeTabButtonColorSourceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabButtonColorSourceIndex);
|
||||
|
||||
updateHomeTabSettingsVisibility();
|
||||
|
||||
auto *homeTabGrid = new QGridLayout;
|
||||
|
|
@ -139,10 +148,54 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 1, 0);
|
||||
homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 1, 1);
|
||||
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
|
||||
|
||||
homeTabGroupBox = new QGroupBox;
|
||||
homeTabGroupBox->setLayout(homeTabGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// Styling settings
|
||||
styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
|
||||
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setStyleUserList);
|
||||
|
|
@ -248,7 +301,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
cardLayoutGroupBox->setLayout(cardLayoutGrid);
|
||||
|
||||
// Card counter colors
|
||||
|
||||
auto *cardCounterColorsLayout = new QGridLayout;
|
||||
cardCounterColorsLayout->setColumnStretch(1, 1);
|
||||
cardCounterColorsLayout->setColumnStretch(3, 1);
|
||||
|
|
@ -328,47 +380,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
tableGroupBox = new QGroupBox;
|
||||
tableGroupBox->setLayout(tableGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// putting it all together
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(themeGroupBox);
|
||||
|
|
@ -497,6 +508,15 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
|
||||
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
|
||||
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
|
||||
homeTabButtonColorSourceBox.setToolTip(
|
||||
tr("Automatic: extract from background if present, otherwise use theme default"));
|
||||
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
|
||||
stylingGroupBox->setTitle(tr("Styling settings"));
|
||||
styleUserListCheckBox.setText(tr("Style user list"));
|
||||
|
|
@ -540,9 +560,4 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
tableGroupBox->setTitle(tr("Table grid layout"));
|
||||
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
||||
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue