mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
Merge branch 'master' into tooomm-patch-33
This commit is contained in:
commit
860ff503b1
233 changed files with 7900 additions and 1711 deletions
|
|
@ -8,6 +8,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
|||
gtest \
|
||||
mariadb-libs \
|
||||
ninja \
|
||||
openssl \
|
||||
protobuf \
|
||||
qt6-base \
|
||||
qt6-declarative \
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ RUN dnf install -y \
|
|||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ RUN dnf install -y \
|
|||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
protobuf-devel \
|
||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
|
|
|
|||
|
|
@ -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,6 +15,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
libssl-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
|
|
|
|||
|
|
@ -150,6 +150,11 @@ if [[ $MAKE_TEST == "1" ]]; then
|
|||
fi
|
||||
if [[ $USE_CCACHE == "1" ]]; 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 [[ -n $CCACHE_SIZE ]]; then
|
||||
# This setting persists after running the script
|
||||
ccache --max-size "$CCACHE_SIZE"
|
||||
|
|
|
|||
2
.github/workflows/codeql.yml
vendored
2
.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
|
||||
|
|
|
|||
12
.github/workflows/desktop-build.yml
vendored
12
.github/workflows/desktop-build.yml
vendored
|
|
@ -152,7 +152,7 @@ jobs:
|
|||
env:
|
||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
||||
CCACHE_EVICTION_AGE: 7d
|
||||
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CMAKE_GENERATOR: 'Ninja'
|
||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -338,7 +342,7 @@ jobs:
|
|||
timeout-minutes: 100
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
||||
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
|
|
|
|||
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-*
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
# Cockatrice, Oracle, Servatrice, Test
|
||||
# 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)
|
||||
# CMake 3.16 is required for Qt6 and target_precompile_headers()
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Compile Cockatrice
|
||||
option(WITH_CLIENT "Build Cockatrice client" ON)
|
||||
|
|
@ -18,10 +18,10 @@ option(TEST "Build tests" OFF)
|
|||
|
||||
# Check for translation updates
|
||||
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
|
||||
# Use compiler cache (ccache)
|
||||
option(USE_CCACHE "Cache the build results with ccache" ON)
|
||||
# Use vcpkg regardless of OS
|
||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||
# Use compiler cache (ccache)
|
||||
option(USE_CCACHE "Cache the build results with ccache" OFF)
|
||||
# Treat warnings as errors (Debug builds only)
|
||||
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
|
||||
|
||||
|
|
@ -34,13 +34,24 @@ if(NOT CMAKE_BUILD_TYPE)
|
|||
)
|
||||
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)
|
||||
|
|
@ -205,6 +216,9 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC compiler
|
|||
endif()
|
||||
endforeach()
|
||||
|
||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||
|
||||
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Clang compiler
|
||||
# -O2 Balanced optimization
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
||||
|
|
@ -221,11 +235,13 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Clang compiler
|
|||
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") # <-- required for clang? see adding pr description again
|
||||
|
||||
else() # Undefined compiler
|
||||
message(WARNING
|
||||
"Unknown C++ compiler: ${CMAKE_CXX_COMPILER_ID}"
|
||||
)
|
||||
|
||||
endif()
|
||||
|
||||
# GNU systems need to define the Mersenne Exponent for SFMT for the RNG to compile without warning
|
||||
|
|
@ -274,11 +290,6 @@ if(WIN32) # Windows (including 64bit)
|
|||
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
README.md
18
README.md
|
|
@ -149,15 +149,15 @@ You can then
|
|||
<br>
|
||||
|
||||
The following flags (with their non-default values) can be passed to `cmake`:
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
||||
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
||||
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
||||
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||
| Flag | Description |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
||||
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
||||
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
||||
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||
|
||||
|
||||
# Run
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ if(WITH_CLIENT)
|
|||
)
|
||||
endif()
|
||||
if(WITH_ORACLE)
|
||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
|
||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml)
|
||||
endif()
|
||||
if(TEST)
|
||||
# Union of Qt modules required across all test targets (independent of application targets).
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -166,6 +166,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||
src/interface/widgets/cards/card_art_utils.cpp
|
||||
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
||||
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
||||
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
||||
|
|
@ -214,6 +215,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
|
||||
|
|
@ -380,6 +382,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_card_art_rules.cpp
|
||||
src/interface/widgets/tabs/tab_deck_editor.cpp
|
||||
src/interface/widgets/tabs/tab_deck_storage.cpp
|
||||
src/interface/widgets/tabs/tab_developer.cpp
|
||||
src/interface/widgets/tabs/tab_game.cpp
|
||||
src/interface/widgets/tabs/tab_home.cpp
|
||||
src/interface/widgets/tabs/tab_logs.cpp
|
||||
|
|
@ -516,6 +519,8 @@ qt6_add_executable(
|
|||
MANUAL_FINALIZATION
|
||||
)
|
||||
|
||||
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
||||
|
||||
qt6_add_shaders(
|
||||
cockatrice
|
||||
"onboarding_shaders"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "remote_connection_controller.h"
|
||||
|
||||
#include "../../../interface/pixel_map_generator.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../interface/widgets/dialogs/dlg_connect.h"
|
||||
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
||||
|
|
@ -180,7 +181,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
|
|||
"games will be lost.\nReason for shutdown: %1",
|
||||
"", event.minutes())
|
||||
.arg(QString::fromStdString(event.reason())));
|
||||
serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
|
||||
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
|
||||
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
||||
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
||||
serverShutdownMessageBox.setVisible(true);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "filter_builder.h"
|
||||
|
||||
#include "../interface/pixel_map_generator.h"
|
||||
#include "../interface/widgets/utility/custom_line_edit.h"
|
||||
|
||||
#include <QComboBox>
|
||||
|
|
@ -21,7 +22,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
|||
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
||||
}
|
||||
|
||||
QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
|
||||
QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
|
||||
ok->setObjectName("ok");
|
||||
ok->setMaximumSize(20, 20);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
|
|||
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
|
||||
}
|
||||
|
||||
void CounterState::setValue(int newValue)
|
||||
void CounterState::setValue(int newValue, bool skipDamageAnimation)
|
||||
{
|
||||
if (newValue == value) {
|
||||
return;
|
||||
}
|
||||
int old = value;
|
||||
value = newValue;
|
||||
emit valueChanged(old, newValue);
|
||||
emit valueChanged(old, newValue, skipDamageAnimation);
|
||||
}
|
||||
|
|
@ -35,10 +35,23 @@ public:
|
|||
return value;
|
||||
}
|
||||
|
||||
void setValue(int newValue);
|
||||
/**
|
||||
* @brief Set the counter value.
|
||||
* @param newValue The new value.
|
||||
* @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views
|
||||
* suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay
|
||||
* rewinds.
|
||||
*/
|
||||
void setValue(int newValue, bool skipDamageAnimation = false);
|
||||
|
||||
signals:
|
||||
void valueChanged(int oldValue, int newValue);
|
||||
/**
|
||||
* @brief Emitted whenever the value changes.
|
||||
* @param oldValue The previous value.
|
||||
* @param newValue The new value.
|
||||
* @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views.
|
||||
*/
|
||||
void valueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
||||
|
||||
private:
|
||||
int id;
|
||||
|
|
|
|||
|
|
@ -430,12 +430,13 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
|||
QString playerName = QString::fromStdString(playerInfo.user_info().name());
|
||||
emit addPlayerToAutoCompleteList(playerName);
|
||||
|
||||
if (game->getPlayerManager()->getPlayers().contains(playerId)) {
|
||||
PlayerManager *playerManager = game->getPlayerManager();
|
||||
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerInfo.spectator()) {
|
||||
game->getPlayerManager()->addSpectator(playerId, playerInfo);
|
||||
playerManager->addSpectator(playerId, playerInfo);
|
||||
emit logJoinSpectator(playerName);
|
||||
emit spectatorJoined(playerInfo);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@
|
|||
enum EventProcessingOption
|
||||
{
|
||||
SKIP_REVEAL_WINDOW = 0x0001,
|
||||
SKIP_TAP_ANIMATION = 0x0002
|
||||
SKIP_TAP_ANIMATION = 0x0002,
|
||||
SKIP_DAMAGE_ANIMATION = 0x0004
|
||||
};
|
||||
|
||||
// Wrap it in a QFlags typedef
|
||||
|
|
|
|||
|
|
@ -262,14 +262,15 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
|
|||
player->addCounter(event.counter_info());
|
||||
}
|
||||
|
||||
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
|
||||
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options)
|
||||
{
|
||||
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
|
||||
if (!ctr) {
|
||||
return;
|
||||
}
|
||||
int oldValue = ctr->getValue();
|
||||
ctr->setValue(event.value());
|
||||
const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION);
|
||||
ctr->setValue(event.value(), skipDamageAnimation);
|
||||
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
|
||||
}
|
||||
|
||||
|
|
@ -625,7 +626,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
|
|||
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
|
||||
break;
|
||||
case GameEvent::SET_COUNTER:
|
||||
eventSetCounter(event.GetExtension(Event_SetCounter::ext));
|
||||
eventSetCounter(event.GetExtension(Event_SetCounter::ext), options);
|
||||
break;
|
||||
case GameEvent::DEL_COUNTER:
|
||||
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ public:
|
|||
void eventCreateCounter(const Event_CreateCounter &event);
|
||||
|
||||
/// Set a player-level counter value.
|
||||
void eventSetCounter(const Event_SetCounter &event);
|
||||
void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options);
|
||||
|
||||
/// Delete a player-level counter.
|
||||
void eventDelCounter(const Event_DelCounter &event);
|
||||
|
|
|
|||
|
|
@ -175,7 +175,15 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
|
|||
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
|
||||
auto *card = new CardItem(this);
|
||||
card->processCardInfo(cardInfo);
|
||||
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
|
||||
// Zones without coordinates (hand, piles, stack) preserve the order
|
||||
// they arrive in on the server in the positions of their cards list.
|
||||
// The x coordinate of such cards is always 0, so inserting at it
|
||||
// would reverse the list on reconnect. Append instead.
|
||||
if (zoneInfo.with_coords()) {
|
||||
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
|
||||
} else {
|
||||
zone->addCard(card, false, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (zoneInfo.has_always_reveal_top_card()) {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
|
|||
return player;
|
||||
}
|
||||
|
||||
void PlayerManager::clearSpectators()
|
||||
{
|
||||
const QList<int> spectatorIds = spectators.keys();
|
||||
for (int spectatorId : spectatorIds) {
|
||||
removeSpectator(spectatorId);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
|
||||
{
|
||||
// Everything else cares about this
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ public:
|
|||
emit spectatorRemoved(spectatorId, spectatorInfo);
|
||||
}
|
||||
|
||||
/** @brief Remove all spectators, emitting the removal signal for each. */
|
||||
void clearSpectators();
|
||||
|
||||
[[nodiscard]] AbstractGame *getGame() const
|
||||
{
|
||||
return game;
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
|
|||
{
|
||||
setAcceptHoverEvents(true);
|
||||
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
|
||||
value = newValue;
|
||||
onValueChanged(oldValue, newValue);
|
||||
onValueChanged(oldValue, newValue, skipDamageAnimation);
|
||||
update();
|
||||
});
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff)
|
|||
setTextValue(QString::number(curValue));
|
||||
}
|
||||
|
||||
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/)
|
||||
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/)
|
||||
{
|
||||
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
||||
// flash the counter on meaningful changes (life gain/loss).
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ protected:
|
|||
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
||||
*
|
||||
* Called whenever the counter's value changes, before the item repaints.
|
||||
* @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds).
|
||||
*/
|
||||
virtual void onValueChanged(int oldValue, int newValue);
|
||||
virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
||||
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
|
|||
|
||||
for (const auto &item : scene()->selectedItems()) {
|
||||
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
||||
if (card == nullptr) {
|
||||
if (card == nullptr || card == this) {
|
||||
continue;
|
||||
}
|
||||
if (card->getZone() != state->getZone()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
QObject::disconnect(nullptr, nullptr, this, nullptr);
|
||||
// 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;
|
||||
|
|
@ -216,7 +221,12 @@ void GameScene::removePlayer(PlayerLogic *player)
|
|||
|
||||
clearArrowsForPlayer(player->getPlayerInfo()->getId());
|
||||
|
||||
for (ZoneViewWidget *zone : zoneViews) {
|
||||
// Closing a view removes it from zoneViews synchronously, so iterate over a
|
||||
// copy: otherwise a player with several open views (e.g. library and hand)
|
||||
// only has the first one closed here and the remaining views are left
|
||||
// pointing at a player that is about to be deleted.
|
||||
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
|
||||
for (ZoneViewWidget *zone : zoneViewCopy) {
|
||||
if (zone->getPlayer() == player) {
|
||||
zone->close();
|
||||
}
|
||||
|
|
@ -659,7 +669,10 @@ CardItem *GameScene::findTopmostCardInZone(const QList<QGraphicsItem *> &items,
|
|||
*/
|
||||
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
|
||||
{
|
||||
for (auto &view : zoneViews) {
|
||||
// Closing a view removes it from zoneViews synchronously, so iterate over a
|
||||
// copy to make sure every already-open matching view is closed.
|
||||
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
|
||||
for (auto *view : zoneViewCopy) {
|
||||
ZoneViewZone *temp = view->getZone();
|
||||
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
|
||||
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {
|
||||
|
|
@ -777,8 +790,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 +817,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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "hand_counter.h"
|
||||
|
||||
#include "../interface/pixel_map_generator.h"
|
||||
#include "zones/card_zone.h"
|
||||
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
|
|
@ -32,7 +33,8 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
|
|||
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
|
||||
QPixmap cachedPixmap;
|
||||
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
|
||||
cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
cachedPixmap =
|
||||
themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
|
||||
}
|
||||
resetPainterTransform(painter);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../../game/player/player_actions.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||
#include "../../interface/widgets/cards/card_art_utils.h"
|
||||
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
|
|
@ -251,8 +252,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
|||
AbstractCounter *widget;
|
||||
if (state->getName() == "life") {
|
||||
widget = playerTarget->addCounter(state);
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||
if (newValue < oldValue) {
|
||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
|
||||
if (newValue < oldValue && !skipDamageAnimation) {
|
||||
tableZoneGraphicsItem->triggerDamageShimmer();
|
||||
}
|
||||
});
|
||||
|
|
@ -442,7 +443,7 @@ void PlayerGraphicsItem::updatePlaymat()
|
|||
hasPlaymat = true;
|
||||
emit playmatChanged(true);
|
||||
}
|
||||
playmatPixmap = fullRes;
|
||||
playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
|
|||
QWidget *parent)
|
||||
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
||||
{
|
||||
readyIcon = QPixmap("theme:icons/ready_start");
|
||||
notReadyIcon = QPixmap("theme:icons/not_ready_start");
|
||||
concededIcon = QPixmap("theme:icons/conceded");
|
||||
readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
|
||||
notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
|
||||
concededIcon = themePixmap(QStringLiteral("icons/conceded"));
|
||||
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
|
||||
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
|
||||
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
|
||||
lockIcon = QPixmap("theme:icons/lock");
|
||||
lockIcon = themePixmap(QStringLiteral("icons/lock"));
|
||||
|
||||
if (tabSupervisor) {
|
||||
itemDelegate = new PlayerListItemDelegate(this);
|
||||
|
|
@ -92,6 +92,11 @@ void PlayerListWidget::retranslateUi()
|
|||
|
||||
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
|
||||
{
|
||||
if (players.contains(player.player_id())) {
|
||||
updatePlayerProperties(player);
|
||||
return;
|
||||
}
|
||||
|
||||
QTreeWidgetItem *newPlayer = new PlayerListTWI;
|
||||
players.insert(player.player_id(), newPlayer);
|
||||
updatePlayerProperties(player);
|
||||
|
|
@ -176,6 +181,17 @@ void PlayerListWidget::removePlayer(int playerId)
|
|||
delete takeTopLevelItem(indexOfTopLevelItem(player));
|
||||
}
|
||||
|
||||
void PlayerListWidget::clearSpectators()
|
||||
{
|
||||
const QList<int> playerIds = players.keys();
|
||||
for (int playerId : playerIds) {
|
||||
QTreeWidgetItem *player = players.value(playerId, 0);
|
||||
if (player && !player->data(1, Qt::UserRole).toBool()) {
|
||||
removePlayer(playerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerListWidget::setActivePlayer(int playerId)
|
||||
{
|
||||
QMapIterator<int, QTreeWidgetItem *> i(players);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ public slots:
|
|||
void addPlayer(const ServerInfo_PlayerProperties &player);
|
||||
void removePlayer(int playerId);
|
||||
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
|
||||
void clearSpectators();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
|
|||
}
|
||||
}
|
||||
|
||||
void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
||||
void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation)
|
||||
{
|
||||
flashDelta = newValue - oldValue;
|
||||
if (flashDelta == 0) {
|
||||
|
|
@ -81,6 +81,11 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
|||
return;
|
||||
}
|
||||
|
||||
if (skipDamageAnimation) {
|
||||
flashAlpha = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
flashAlpha = 1.0;
|
||||
flashClock.start();
|
||||
if (scene()) {
|
||||
|
|
@ -132,8 +137,18 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
|
|||
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
|
||||
QSize translatedSize = translatedRect.size().toSize();
|
||||
QPixmap cachedPixmap;
|
||||
// The key must cover everything the generated pawn depends on: the rendered
|
||||
// size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is
|
||||
// 0 for every null pixmap, so the default-pawn branch additionally needs the
|
||||
// pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors
|
||||
// in the key — otherwise two players without a custom avatar (and the same
|
||||
// user level) would share one cached pawn.
|
||||
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
|
||||
QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey());
|
||||
QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) +
|
||||
"_" + QString::number(fullPixmap.cacheKey()) + "_" +
|
||||
QString::fromStdString(info->privlevel()).toLower() + "_" +
|
||||
QString::fromStdString(info->pawn_colors().left_side()) + "_" +
|
||||
QString::fromStdString(info->pawn_colors().right_side());
|
||||
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
|
||||
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem
|
|||
{
|
||||
Q_OBJECT
|
||||
protected:
|
||||
void onValueChanged(int oldValue, int newValue) override;
|
||||
void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override;
|
||||
|
||||
private:
|
||||
static constexpr qreal flashDurationMs = 450.0;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
|||
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
|
||||
searchEdit.setClearButtonEnabled(true);
|
||||
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||
auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
||||
auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
|
||||
|
||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||
|
||||
|
|
@ -549,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
|
|||
{
|
||||
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
|
||||
if (titleBar) {
|
||||
titleBar->icon = QPixmap("theme:cockatrice");
|
||||
titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_picture_loader.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../pixel_map_generator.h"
|
||||
#include "card_picture_loader_cache_method.h"
|
||||
#include "card_picture_loader_local_schemes.h"
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
|||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||
QPixmap tmpPixmap("theme:cardback");
|
||||
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
||||
|
||||
if (tmpPixmap.isNull()) {
|
||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
|
||||
|
|
@ -83,7 +84,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
|
|||
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||
QPixmap tmpPixmap("theme:cardback");
|
||||
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
||||
|
||||
if (tmpPixmap.isNull()) {
|
||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
|
||||
|
|
@ -105,7 +106,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
|
|||
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||
QPixmap tmpPixmap("theme:cardback");
|
||||
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
||||
|
||||
if (tmpPixmap.isNull()) {
|
||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
|
||||
|
|
@ -138,7 +139,8 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
|||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
if (bigPixmap.isNull()) {
|
||||
getCardBackLoadingFailedPixmap(pixmap, size);
|
||||
// Leave the pixmap null so callers fall back to a solid color
|
||||
// instead of showing the card back.
|
||||
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
||||
if (!failedAtTime.isValid() ||
|
||||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "pixel_map_generator.h"
|
||||
|
||||
#include "theme_manager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
|
|
@ -14,6 +16,7 @@
|
|||
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
|
||||
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
|
||||
#define DEFAULT_COLOR_ADMIN "#ff2701";
|
||||
#define DEFAULT_COLOR_DEVELOPER "#B8B8B8"
|
||||
|
||||
/**
|
||||
* Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
|
||||
|
|
@ -82,7 +85,13 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
|
|||
/**
|
||||
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
|
||||
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
|
||||
* @param path The path to the file, with no file extension. File formats will be automatically detected.
|
||||
*
|
||||
* The path may already carry the resolved file extension (e.g. via
|
||||
* ThemeManager::assetPath); such paths are loaded directly. Otherwise a
|
||||
* format-agnostic lookup probes png, jpg and finally svg.
|
||||
*
|
||||
* @param path The path to the file, with no file extension unless the caller
|
||||
* already resolved it. File formats will be automatically detected.
|
||||
* @param size The desired size of the pixmap.
|
||||
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
|
||||
*
|
||||
|
|
@ -90,6 +99,19 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
|
|||
*/
|
||||
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
|
||||
{
|
||||
if (path.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) {
|
||||
return loadSvg(path, size, expandOnly);
|
||||
}
|
||||
if (path.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) ||
|
||||
path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
|
||||
path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)) {
|
||||
QPixmap pix(path);
|
||||
if (!pix.isNull()) {
|
||||
return pix.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto formats = {"png", "jpg"};
|
||||
|
||||
QPixmap returnPixmap;
|
||||
|
|
@ -111,7 +133,8 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
|
|||
return pmCache.value(key);
|
||||
}
|
||||
|
||||
QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height));
|
||||
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("phases/") + name),
|
||||
QSize(height, height));
|
||||
|
||||
pmCache.insert(key, pixmap);
|
||||
return pixmap;
|
||||
|
|
@ -359,6 +382,8 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
|
|||
|
||||
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
|
||||
colorLeft = DEFAULT_COLOR_ADMIN;
|
||||
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
|
||||
colorLeft = DEFAULT_COLOR_DEVELOPER;
|
||||
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
|
||||
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
|
||||
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;
|
||||
|
|
@ -396,7 +421,8 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
|
|||
return pmCache.value(key);
|
||||
}
|
||||
|
||||
QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true);
|
||||
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")),
|
||||
QSize(height, height), true);
|
||||
pmCache.insert(key, pixmap);
|
||||
return pixmap;
|
||||
}
|
||||
|
|
@ -411,7 +437,8 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
|
|||
}
|
||||
|
||||
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
|
||||
QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true);
|
||||
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name),
|
||||
QSize(height, height), true);
|
||||
|
||||
pmCache.insert(key, pixmap);
|
||||
return pixmap;
|
||||
|
|
@ -472,6 +499,13 @@ QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
|
|||
|
||||
QPixmap loadColorAdjustedPixmap(const QString &name)
|
||||
{
|
||||
// Prefer an authored scheme-qualified variant when one exists for this asset.
|
||||
const QString variant = themeManager->schemeVariantPath(QStringView(name).mid(QStringLiteral("theme:").size()));
|
||||
if (!variant.isEmpty()) {
|
||||
return QPixmap(QStringLiteral("theme:") + variant);
|
||||
}
|
||||
|
||||
// Legacy fallback: runtime-invert for dark mode when no authored variant.
|
||||
if (qApp->palette().windowText().color().lightness() > 200) {
|
||||
QImage img(name);
|
||||
img.invertPixels();
|
||||
|
|
@ -482,3 +516,21 @@ QPixmap loadColorAdjustedPixmap(const QString &name)
|
|||
return QPixmap(name);
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap themePixmap(QStringView prefix)
|
||||
{
|
||||
const QString resolved = themeManager->assetPath(prefix);
|
||||
return QPixmap(QStringLiteral("theme:") + resolved);
|
||||
}
|
||||
|
||||
void clearPixmapGeneratorCaches()
|
||||
{
|
||||
PhasePixmapGenerator::clear();
|
||||
CounterPixmapGenerator::clear();
|
||||
PingPixmapGenerator::clear();
|
||||
CountryPixmapGenerator::clear();
|
||||
UserLevelPixmapGenerator::clear();
|
||||
LockPixmapGenerator::clear();
|
||||
DropdownIconPixmapGenerator::clear();
|
||||
ManaSymbolPixmapGenerator::clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,4 +156,15 @@ public:
|
|||
|
||||
QPixmap loadColorAdjustedPixmap(const QString &name);
|
||||
|
||||
// Loads a "theme:" asset (with no file extension in prefix), preferring the
|
||||
// scheme-qualified variant (prefix-dark / prefix-light, resolved via
|
||||
// ThemeManager::assetPath) and falling back to the plain asset. Callers load
|
||||
// the returned path directly. Use for scheme-sensitive pixmaps like
|
||||
// backgrounds, the card back, and the app logo.
|
||||
QPixmap themePixmap(QStringView prefix);
|
||||
|
||||
// Clears every PixmapGenerator's static cache so scheme variants are
|
||||
// re-resolved when the active theme or color scheme changes.
|
||||
void clearPixmapGeneratorCaches();
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
#include "theme_manager.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "pixel_map_generator.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QLibraryInfo>
|
||||
#include <QMap>
|
||||
#include <QMetaEnum>
|
||||
|
|
@ -140,6 +142,48 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const
|
|||
}
|
||||
}
|
||||
|
||||
QString ThemeManager::schemeVariantPath(QStringView prefix) const
|
||||
{
|
||||
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
|
||||
QStringLiteral(".svg")};
|
||||
const QString scheme = isDarkMode(currentThemePath) ? QStringLiteral("dark") : QStringLiteral("light");
|
||||
const QString variantStem = prefix.toString() + QLatin1Char('-') + scheme;
|
||||
|
||||
for (const QString &format : formats) {
|
||||
if (QFileInfo::exists(QStringLiteral("theme:") + variantStem + format)) {
|
||||
return variantStem + format;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString ThemeManager::assetPath(QStringView prefix) const
|
||||
{
|
||||
// Probe order mirrors tryLoadImage: a theme may override the default SVG
|
||||
// with a raster of the same stem, so raster wins over SVG within a stem.
|
||||
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
|
||||
QStringLiteral(".svg")};
|
||||
|
||||
auto findExisting = [](const QString &stem) {
|
||||
for (const QString &format : formats) {
|
||||
if (QFileInfo::exists(QStringLiteral("theme:") + stem + format)) {
|
||||
return stem + format;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
// Prefer the scheme-qualified variant when it exists, else the plain
|
||||
// asset as the super fallback. Both return the resolved path including
|
||||
// its file extension so callers can load it directly.
|
||||
const QString variant = schemeVariantPath(prefix);
|
||||
if (!variant.isEmpty()) {
|
||||
return variant;
|
||||
}
|
||||
const QString resolvedPlain = findExisting(prefix.toString());
|
||||
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
|
||||
}
|
||||
|
||||
bool ThemeManager::isBuiltInTheme()
|
||||
{
|
||||
const auto themeName = SettingsCache::instance().getThemeName();
|
||||
|
|
@ -195,7 +239,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
|||
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
||||
{
|
||||
QBrush brush;
|
||||
QPixmap tmp = QPixmap("theme:zones/" + fileName);
|
||||
QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
|
||||
if (tmp.isNull()) {
|
||||
brush.setColor(fallbackColor);
|
||||
brush.setStyle(Qt::SolidPattern);
|
||||
|
|
@ -209,7 +253,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
|||
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
|
||||
{
|
||||
QBrush brush;
|
||||
QPixmap tmp = QPixmap("theme:zones/" + fileName);
|
||||
QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
|
||||
|
||||
if (tmp.isNull()) {
|
||||
brush = fallbackBrush;
|
||||
|
|
@ -393,9 +437,19 @@ void ThemeManager::themeChangedSlot()
|
|||
currentThemePath = dirPath;
|
||||
QDir dir(dirPath);
|
||||
|
||||
// CSS
|
||||
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
|
||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
||||
// CSS — prefer the scheme-qualified stylesheet (style-dark.css /
|
||||
// style-light.css) when present, else the plain style.css as fallback.
|
||||
if (!dirPath.isEmpty()) {
|
||||
const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
|
||||
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
|
||||
scheme + QStringLiteral(".css");
|
||||
if (dir.exists(schemeCss)) {
|
||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss));
|
||||
} else if (dir.exists(STYLE_CSS_NAME)) {
|
||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
||||
} else {
|
||||
qApp->setStyleSheet("");
|
||||
}
|
||||
} else {
|
||||
qApp->setStyleSheet("");
|
||||
}
|
||||
|
|
@ -446,6 +500,7 @@ void ThemeManager::themeChangedSlot()
|
|||
}
|
||||
|
||||
QPixmapCache::clear();
|
||||
clearPixmapGeneratorCaches();
|
||||
|
||||
emit themeChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,20 @@ public:
|
|||
// Load/save per-scheme palette colors
|
||||
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
||||
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
||||
// Resolve prefix to a scheme-qualified "theme:" path. Existence is probed
|
||||
// internally across the formats themes may ship (.png/.jpg/.svg), so
|
||||
// callers load the returned path directly. Prefers "<prefix>-<dark|light>"
|
||||
// when a file exists at that stem, otherwise the plain "<prefix>" as the
|
||||
// super fallback. The resolved scheme covers explicit light/dark as well
|
||||
// as OS-resolved "system". Returns the path with its file extension when a
|
||||
// match is found; unqualified assets keep working unchanged.
|
||||
QString assetPath(QStringView prefix) const;
|
||||
// Like assetPath, but resolves only the scheme-qualified variant
|
||||
// ("<prefix>-<dark|light>.<ext>") and returns an empty string when no
|
||||
// variant exists — it never falls back to the plain "<prefix>" asset.
|
||||
// Callers that must distinguish "no authored variant" (e.g. to keep a
|
||||
// legacy runtime fallback alive) should use this instead of assetPath.
|
||||
QString schemeVariantPath(QStringView prefix) const;
|
||||
// Load the theme's shipped default palette, falling back to the system
|
||||
// theme directory when it is absent from the resolved (user) directory.
|
||||
static PaletteConfig
|
||||
|
|
|
|||
18
cockatrice/src/interface/widgets/cards/card_art_utils.cpp
Normal file
18
cockatrice/src/interface/widgets/cards/card_art_utils.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#include "card_art_utils.h"
|
||||
|
||||
#include <QTransform>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
|
||||
namespace CardArtUtils
|
||||
{
|
||||
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card)
|
||||
{
|
||||
if (!card.getInfo().getUiAttributes().landscapeOrientation) {
|
||||
return art;
|
||||
}
|
||||
|
||||
QTransform transform;
|
||||
transform.rotate(90);
|
||||
return art.transformed(transform, Qt::SmoothTransformation);
|
||||
}
|
||||
} // namespace CardArtUtils
|
||||
25
cockatrice/src/interface/widgets/cards/card_art_utils.h
Normal file
25
cockatrice/src/interface/widgets/cards/card_art_utils.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#ifndef CARD_ART_UTILS_H
|
||||
#define CARD_ART_UTILS_H
|
||||
|
||||
#include <QPixmap>
|
||||
|
||||
class ExactCard;
|
||||
|
||||
namespace CardArtUtils
|
||||
{
|
||||
/**
|
||||
* @brief Rotates a card's art upright when its layout shows sideways.
|
||||
*
|
||||
* Sideways-layout cards (planes, sieges/battles, split cards) store their
|
||||
* landscape artwork rotated 90° inside a portrait frame. Art-crop displays,
|
||||
* playmat art, and the card-info picture must show such art upright before
|
||||
* sampling or painting. Portrait cards are returned unchanged.
|
||||
*
|
||||
* @param art The card pixmap to orient.
|
||||
* @param card The card describing the art orientation.
|
||||
* @return @p art rotated 90° clockwise for sideways-layout cards, else @p art.
|
||||
*/
|
||||
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card);
|
||||
} // namespace CardArtUtils
|
||||
|
||||
#endif // CARD_ART_UTILS_H
|
||||
|
|
@ -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 "../../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../../window_main.h"
|
||||
#include "card_art_utils.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
|
|
@ -193,12 +194,7 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
|
|||
|
||||
QPixmap transformedPixmap = resizedPixmap; // Default pixmap
|
||||
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
|
||||
if (exactCard.getInfo().getUiAttributes().landscapeOrientation) {
|
||||
// Rotate pixmap 90 degrees to the left
|
||||
QTransform transform;
|
||||
transform.rotate(90);
|
||||
transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
|
||||
}
|
||||
transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard);
|
||||
}
|
||||
|
||||
// Handle DPI scaling
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "abstract_analytics_panel_widget.h"
|
||||
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "deck_list_statistics_analyzer.h"
|
||||
|
||||
#include <QPushButton>
|
||||
|
|
@ -20,7 +21,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck
|
|||
|
||||
// config button
|
||||
configureButton = new QPushButton(this);
|
||||
configureButton->setIcon(QPixmap("theme:icons/cogwheel"));
|
||||
configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
|
||||
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
|
||||
bannerAndSettingsLayout->addWidget(configureButton, 0);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
|
|||
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
||||
auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
|
||||
|
||||
setFocusProxy(searchEdit);
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
|
|
@ -59,13 +59,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
|
|||
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
|
||||
|
||||
aAddCard = new QAction(QString(), this);
|
||||
aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green"));
|
||||
aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
|
||||
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
|
||||
auto *tbAddCard = new QToolButton(this);
|
||||
tbAddCard->setDefaultAction(aAddCard);
|
||||
|
||||
aAddCardToSideboard = new QAction(QString(), this);
|
||||
aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue"));
|
||||
aAddCardToSideboard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_blue")));
|
||||
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
|
||||
auto *tbAddCardToSideboard = new QToolButton(this);
|
||||
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);
|
||||
|
|
|
|||
|
|
@ -2,20 +2,24 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../playmat/playmat_settings_dialog.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#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>
|
||||
|
|
@ -189,25 +193,25 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
|
||||
|
||||
aIncrement = new QAction(QString(), this);
|
||||
aIncrement->setIcon(QPixmap("theme:icons/increment"));
|
||||
aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment")));
|
||||
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
|
||||
auto *tbIncrement = new QToolButton(this);
|
||||
tbIncrement->setDefaultAction(aIncrement);
|
||||
|
||||
aDecrement = new QAction(QString(), this);
|
||||
aDecrement->setIcon(QPixmap("theme:icons/decrement"));
|
||||
aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
|
||||
auto *tbDecrement = new QToolButton(this);
|
||||
tbDecrement->setDefaultAction(aDecrement);
|
||||
|
||||
aRemoveCard = new QAction(QString(), this);
|
||||
aRemoveCard->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
|
||||
auto *tbRemoveCard = new QToolButton(this);
|
||||
tbRemoveCard->setDefaultAction(aRemoveCard);
|
||||
|
||||
aSwapCard = new QAction(QString(), this);
|
||||
aSwapCard->setIcon(QPixmap("theme:icons/swap"));
|
||||
aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap")));
|
||||
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
|
||||
auto *tbSwapCard = new QToolButton(this);
|
||||
tbSwapCard->setDefaultAction(aSwapCard);
|
||||
|
|
@ -772,14 +776,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();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
|
|
@ -102,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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../../filters/filter_builder.h"
|
||||
#include "../../../filters/filter_tree_model.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QMenu>
|
||||
|
|
@ -42,11 +43,11 @@ void DeckEditorFilterDockWidget::createFiltersDock()
|
|||
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
|
||||
|
||||
aClearFilterOne = new QAction(QString(), this);
|
||||
aClearFilterOne->setIcon(QPixmap("theme:icons/decrement"));
|
||||
aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
|
||||
|
||||
aClearFilterAll = new QAction(QString(), this);
|
||||
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
|
||||
aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
|
||||
connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll);
|
||||
|
||||
auto *filterDelOne = new QToolButton();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "deck_list_history_manager_widget.h"
|
||||
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "deck_state_manager.h"
|
||||
|
||||
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
|
||||
|
|
@ -10,7 +11,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
|
|||
layout = new QHBoxLayout(this);
|
||||
|
||||
aUndo = new QAction(QString(), this);
|
||||
aUndo->setIcon(QPixmap("theme:icons/arrow_undo"));
|
||||
aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo")));
|
||||
aUndo->setShortcut(QKeySequence::Undo);
|
||||
aUndo->setShortcutContext(Qt::ApplicationShortcut);
|
||||
connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo);
|
||||
|
|
@ -19,7 +20,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
|
|||
undoButton->setDefaultAction(aUndo);
|
||||
|
||||
aRedo = new QAction(QString(), this);
|
||||
aRedo->setIcon(QPixmap("theme:icons/arrow_redo"));
|
||||
aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo")));
|
||||
aRedo->setShortcut(QKeySequence::Redo);
|
||||
aRedo->setShortcutContext(Qt::ApplicationShortcut);
|
||||
connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo);
|
||||
|
|
@ -31,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
|
|||
layout->addWidget(redoButton);
|
||||
|
||||
historyButton = new SettingsButtonWidget(this);
|
||||
historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history"));
|
||||
historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history")));
|
||||
|
||||
historyLabel = new QLabel(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,6 +1,7 @@
|
|||
#include "dlg_connect.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
|
|
@ -21,7 +22,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
|||
previousHosts = new QComboBox(this);
|
||||
|
||||
btnDeleteServer = new QPushButton(this);
|
||||
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||
btnDeleteServer->setFixedWidth(30);
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
|||
|
||||
hps = new HandlePublicServers(this);
|
||||
btnRefreshServers = new QPushButton(this);
|
||||
btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
|
||||
btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
|
||||
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
||||
btnRefreshServers->setFixedWidth(30);
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
|||
updateDisplayInfo(previousHosts->currentText());
|
||||
|
||||
btnForgotPassword = new QPushButton(this);
|
||||
btnForgotPassword->setIcon(QPixmap("theme:icons/forgot_password"));
|
||||
btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password")));
|
||||
btnForgotPassword->setToolTip(tr("Reset Password"));
|
||||
btnForgotPassword->setFixedWidth(30);
|
||||
connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword);
|
||||
|
|
|
|||
|
|
@ -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,5 +1,6 @@
|
|||
#include "dlg_edit_tokens.h"
|
||||
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../interface/widgets/utility/get_text_with_max.h"
|
||||
|
||||
#include <QAction>
|
||||
|
|
@ -90,10 +91,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul
|
|||
&DlgEditTokens::tokenSelectionChanged);
|
||||
|
||||
QAction *aAddToken = new QAction(tr("Add token"), this);
|
||||
aAddToken->setIcon(QPixmap("theme:icons/increment"));
|
||||
aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment")));
|
||||
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
|
||||
QAction *aRemoveToken = new QAction(tr("Remove token"), this);
|
||||
aRemoveToken->setIcon(QPixmap("theme:icons/decrement"));
|
||||
aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
|
||||
|
||||
auto *databaseToolBar = new QToolBar;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "dlg_manage_sets.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../interface/widgets/utility/custom_line_edit.h"
|
||||
|
||||
|
|
@ -35,28 +36,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
|||
setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
|
||||
aTop = new QAction(QString(), this);
|
||||
aTop->setIcon(QPixmap("theme:icons/arrow_top_green"));
|
||||
aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green")));
|
||||
aTop->setToolTip(tr("Move selected set to the top"));
|
||||
aTop->setEnabled(false);
|
||||
connect(aTop, &QAction::triggered, this, &WndSets::actTop);
|
||||
setsEditToolBar->addAction(aTop);
|
||||
|
||||
aUp = new QAction(QString(), this);
|
||||
aUp->setIcon(QPixmap("theme:icons/arrow_up_green"));
|
||||
aUp->setIcon(themePixmap(QStringLiteral("icons/arrow_up_green")));
|
||||
aUp->setToolTip(tr("Move selected set up"));
|
||||
aUp->setEnabled(false);
|
||||
connect(aUp, &QAction::triggered, this, &WndSets::actUp);
|
||||
setsEditToolBar->addAction(aUp);
|
||||
|
||||
aDown = new QAction(QString(), this);
|
||||
aDown->setIcon(QPixmap("theme:icons/arrow_down_green"));
|
||||
aDown->setIcon(themePixmap(QStringLiteral("icons/arrow_down_green")));
|
||||
aDown->setToolTip(tr("Move selected set down"));
|
||||
aDown->setEnabled(false);
|
||||
connect(aDown, &QAction::triggered, this, &WndSets::actDown);
|
||||
setsEditToolBar->addAction(aDown);
|
||||
|
||||
aBottom = new QAction(QString(), this);
|
||||
aBottom->setIcon(QPixmap("theme:icons/arrow_bottom_green"));
|
||||
aBottom->setIcon(themePixmap(QStringLiteral("icons/arrow_bottom_green")));
|
||||
aBottom->setToolTip(tr("Move selected set to the bottom"));
|
||||
aBottom->setEnabled(false);
|
||||
connect(aBottom, &QAction::triggered, this, &WndSets::actBottom);
|
||||
|
|
@ -66,7 +67,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
|||
searchField = new LineEditUnfocusable;
|
||||
searchField->setObjectName("searchEdit");
|
||||
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
|
||||
searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition);
|
||||
searchField->addAction(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition);
|
||||
searchField->setClearButtonEnabled(true);
|
||||
setFocusProxy(searchField);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "dlg_register.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../server/handle_public_servers.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
|||
previousHosts = new QComboBox(this);
|
||||
|
||||
btnDeleteServer = new QPushButton(this);
|
||||
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||
btnDeleteServer->setFixedWidth(30);
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
|||
|
||||
hps = new HandlePublicServers(this);
|
||||
btnRefreshServers = new QPushButton(this);
|
||||
btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
|
||||
btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
|
||||
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
||||
btnRefreshServers->setFixedWidth(30);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "dlg_settings.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../main.h"
|
||||
#include "../settings_page/appearance_settings_page.h"
|
||||
#include "../settings_page/deck_editor_settings_page.h"
|
||||
|
|
@ -96,7 +97,7 @@ void DlgSettings::setupUi()
|
|||
// Search bar
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||
searchEdit->installEventFilter(this);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
||||
|
||||
|
|
@ -132,7 +133,7 @@ void DlgSettings::setupUi()
|
|||
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
||||
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
||||
pagesWidget->addWidget(makeScrollable(storagePage));
|
||||
pagesWidget->addWidget(messagesPage);
|
||||
pagesWidget->addWidget(makeScrollable(messagesPage));
|
||||
pagesWidget->addWidget(soundPage);
|
||||
pagesWidget->addWidget(shortcutsPage);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../../theme_manager.h"
|
||||
#include "../../window_main.h"
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
|
|
@ -20,7 +21,8 @@
|
|||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
|
||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))),
|
||||
overlay(themePixmap(QStringLiteral("cockatrice")))
|
||||
{
|
||||
layout = new QGridLayout(this);
|
||||
|
||||
|
|
@ -56,6 +58,9 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
// Scheme flips (light/dark/system with an OS switch) fire on themeManager,
|
||||
// not on SettingsCache::themeChanged, so re-resolve the variant background.
|
||||
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
}
|
||||
|
|
@ -74,7 +79,7 @@ void HomeWidget::initializeBackgroundFromSource()
|
|||
switch (backgroundSourceType) {
|
||||
case BackgroundSources::Theme:
|
||||
cardChangeTimer->stop();
|
||||
background = QPixmap("theme:backgrounds/home");
|
||||
background = themePixmap(QStringLiteral("backgrounds/home"));
|
||||
backgroundSourceDeck = DeckList();
|
||||
backgroundSourceCard->setCard(ExactCard());
|
||||
updateButtonsToBackgroundColor();
|
||||
|
|
|
|||
|
|
@ -182,6 +182,13 @@ void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
|
|||
}
|
||||
}
|
||||
|
||||
void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total)
|
||||
{
|
||||
if (cardDatabasePage) {
|
||||
cardDatabasePage->onUpdateProgress(stage, done, total);
|
||||
}
|
||||
}
|
||||
|
||||
void FirstRunWizard::finish()
|
||||
{
|
||||
accept();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ public slots:
|
|||
/** @brief Forwarded from MainWindow once the background card database update process exits. */
|
||||
void onCardDatabaseUpdateFinished(bool success);
|
||||
|
||||
/** @brief Forwarded from MainWindow while the background card database update process runs. */
|
||||
void onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <climits>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
|
||||
|
|
@ -179,6 +180,25 @@ void CardDatabaseSetupPage::onUpdateFinished(bool success)
|
|||
}
|
||||
}
|
||||
|
||||
void CardDatabaseSetupPage::onUpdateProgress(const QString &stage, qint64 done, qint64 total)
|
||||
{
|
||||
if (state != State::Running) {
|
||||
return;
|
||||
}
|
||||
progressBar->setRange(0, total > 0 ? static_cast<int>(qMin<qint64>(total, INT_MAX)) : 0);
|
||||
progressBar->setValue(static_cast<int>(qMin<qint64>(done, INT_MAX)));
|
||||
if (total > 0) {
|
||||
const int percent = static_cast<int>((100.0 * done) / total);
|
||||
if (stage == QLatin1String("download")) {
|
||||
statusLabel->setText(tr("Downloading the card database (%1%)…").arg(percent));
|
||||
} else if (stage == QLatin1String("scan")) {
|
||||
statusLabel->setText(tr("Parsing the card database (%1%)…").arg(percent));
|
||||
} else if (stage == QLatin1String("import")) {
|
||||
statusLabel->setText(tr("Importing cards (%1%)…").arg(percent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString CardDatabaseSetupPage::nextButtonText() const
|
||||
{
|
||||
return state == State::NotStarted ? tr("Download") : QString();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public:
|
|||
void retranslateUi() override;
|
||||
|
||||
void onUpdateFinished(bool success);
|
||||
void onUpdateProgress(const QString &stage, qint64 done, qint64 total);
|
||||
|
||||
signals:
|
||||
void updateRequested();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "../cards/card_art_utils.h"
|
||||
#include "../utility/completer_utils.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
|
|
@ -276,7 +277,7 @@ void PlaymatSettingsDialog::reloadPreview()
|
|||
return;
|
||||
}
|
||||
|
||||
currentPixmap = fullRes;
|
||||
currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
|
||||
preview->setPixmap(currentPixmap);
|
||||
preview->setParams(currentParams);
|
||||
preview->setAttribution(buildArtAttribution(card));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "settings_button_widget.h"
|
||||
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
|
|
@ -8,7 +10,7 @@
|
|||
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
|
||||
: QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr))
|
||||
{
|
||||
button->setIcon(QPixmap("theme:icons/cogwheel"));
|
||||
button->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
|
||||
button->setCheckable(true);
|
||||
button->setFixedSize(32, 32);
|
||||
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);
|
||||
|
|
|
|||
|
|
@ -142,8 +142,10 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode)
|
|||
}
|
||||
|
||||
// backwards skip => always skip tap animation
|
||||
// backwards skip => always skip damage animation (battlefield shimmer / life counter flash)
|
||||
if (playbackMode == BACKWARD_SKIP) {
|
||||
options |= SKIP_TAP_ANIMATION;
|
||||
options |= SKIP_DAMAGE_ANIMATION;
|
||||
}
|
||||
|
||||
emit eventReplayed(replay->event_list(currentEvent), options);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "replay_manager.h"
|
||||
#include "replay_quick_settings_widget.h"
|
||||
|
|
@ -50,15 +51,15 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
|||
replayPlayButton = new QToolButton;
|
||||
replayPlayButton->setIconSize(QSize(32, 32));
|
||||
QIcon playButtonIcon = QIcon();
|
||||
playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off);
|
||||
playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On);
|
||||
playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/start")), QIcon::Normal, QIcon::Off);
|
||||
playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/pause")), QIcon::Normal, QIcon::On);
|
||||
replayPlayButton->setIcon(playButtonIcon);
|
||||
replayPlayButton->setCheckable(true);
|
||||
connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled);
|
||||
|
||||
replayFastForwardButton = new QToolButton;
|
||||
replayFastForwardButton->setIconSize(QSize(32, 32));
|
||||
replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward"));
|
||||
replayFastForwardButton->setIcon(themePixmap(QStringLiteral("replay/fastforward")));
|
||||
replayFastForwardButton->setCheckable(true);
|
||||
connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "game_selector.h"
|
||||
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
||||
#include "../interface/widgets/dialogs/dlg_filter_games.h"
|
||||
#include "../interface/widgets/tabs/tab_account.h"
|
||||
|
|
@ -95,10 +96,10 @@ GameSelector::GameSelector(AbstractClient *_client,
|
|||
}
|
||||
|
||||
filterButton = new QPushButton;
|
||||
filterButton->setIcon(QPixmap("theme:icons/search"));
|
||||
filterButton->setIcon(themePixmap(QStringLiteral("icons/search")));
|
||||
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
|
||||
clearFilterButton = new QPushButton;
|
||||
clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch"));
|
||||
clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
|
||||
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
|
||||
clearFilterButton->setEnabled(!filtersSetToDefault);
|
||||
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "remote_replay_list_tree_widget.h"
|
||||
|
||||
#include "../../../pixel_map_generator.h"
|
||||
|
||||
#include <QFileIconProvider>
|
||||
#include <QHeaderView>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
|
@ -37,7 +39,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client,
|
|||
QFileIconProvider fip;
|
||||
dirIcon = fip.icon(QFileIconProvider::Folder);
|
||||
fileIcon = fip.icon(QFileIconProvider::File);
|
||||
lockIcon = QPixmap("theme:icons/lock");
|
||||
lockIcon = themePixmap(QStringLiteral("icons/lock"));
|
||||
}
|
||||
|
||||
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "user_card_art_provider.h"
|
||||
|
||||
#include "../../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../../cards/card_art_utils.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
|
@ -52,16 +53,25 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
|
|||
processQueue();
|
||||
}
|
||||
|
||||
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
|
||||
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card)
|
||||
{
|
||||
const QSize sz = fullRes.size();
|
||||
QPixmap source = fullRes;
|
||||
|
||||
// Sideways-layout cards (plane, siege/battle, split) store their landscape
|
||||
// artwork rotated 90° inside a portrait frame. Rotate it upright first so
|
||||
// the crop below lands on the horizontal art, mirroring the way
|
||||
// CardInfoPictureWidget displays these cards.
|
||||
const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation;
|
||||
source = CardArtUtils::rotateSidewaysLayoutArt(source, card);
|
||||
|
||||
const QSize sz = source.size();
|
||||
const int marginX = sz.width() * 0.07;
|
||||
const int topMargin = sz.height() * 0.11;
|
||||
const int bottomMargin = sz.height() * 0.45;
|
||||
const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11;
|
||||
const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45;
|
||||
|
||||
const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
|
||||
const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
|
||||
|
||||
return fullRes.copy(foilRect.intersected(fullRes.rect()));
|
||||
return source.copy(artRect.intersected(source.rect()));
|
||||
}
|
||||
|
||||
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
|
||||
|
|
@ -111,7 +121,7 @@ void UserCardArtProvider::processQueue()
|
|||
|
||||
// Synchronous hit (already loaded/on disk)
|
||||
if (!fullRes.isNull()) {
|
||||
insertIntoCache(key, cropCardArt(fullRes));
|
||||
insertIntoCache(key, cropCardArt(fullRes, card));
|
||||
pending.remove(key);
|
||||
|
||||
emit cardArtUpdated(userName);
|
||||
|
|
@ -135,7 +145,7 @@ void UserCardArtProvider::processQueue()
|
|||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (!fullRes.isNull()) {
|
||||
self->insertIntoCache(key, self->cropCardArt(fullRes));
|
||||
self->insertIntoCache(key, self->cropCardArt(fullRes, card));
|
||||
}
|
||||
|
||||
self->pending.remove(key);
|
||||
|
|
|
|||
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