diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile
index f37315262..b08e568f3 100644
--- a/.ci/Arch/Dockerfile
+++ b/.ci/Arch/Dockerfile
@@ -8,6 +8,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
gtest \
mariadb-libs \
ninja \
+ openssl \
protobuf \
qt6-base \
qt6-declarative \
diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile
index 0fa227d6f..e3df94ab5 100644
--- a/.ci/Debian12/Dockerfile
+++ b/.ci/Debian12/Dockerfile
@@ -15,6 +15,7 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
+ libssl-dev \
ninja-build \
protobuf-compiler \
qt6-image-formats-plugins \
diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile
index 13e8b35c7..60e490c98 100644
--- a/.ci/Debian13/Dockerfile
+++ b/.ci/Debian13/Dockerfile
@@ -16,6 +16,7 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
+ libssl-dev \
ninja-build \
protobuf-compiler \
qt6-image-formats-plugins \
diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile
index 68e894543..4005bbf67 100644
--- a/.ci/Fedora43/Dockerfile
+++ b/.ci/Fedora43/Dockerfile
@@ -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 \
diff --git a/.ci/Fedora44/Dockerfile b/.ci/Fedora44/Dockerfile
index ffd7c1b9b..e0224cdc6 100644
--- a/.ci/Fedora44/Dockerfile
+++ b/.ci/Fedora44/Dockerfile
@@ -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 \
diff --git a/.ci/Servatrice_Debian12/Dockerfile b/.ci/Servatrice_Debian12/Dockerfile
index 21f6a036e..321aa7c0f 100644
--- a/.ci/Servatrice_Debian12/Dockerfile
+++ b/.ci/Servatrice_Debian12/Dockerfile
@@ -12,6 +12,7 @@ RUN apt-get update && \
libmariadb-dev-compat \
libprotobuf-dev \
libqt6sql6-mysql \
+ libssl-dev \
ninja-build \
protobuf-compiler \
qt6-tools-dev \
diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile
index 12320c276..10adc5e64 100644
--- a/.ci/Ubuntu24.04/Dockerfile
+++ b/.ci/Ubuntu24.04/Dockerfile
@@ -15,6 +15,7 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
+ libssl-dev \
ninja-build \
protobuf-compiler \
qt6-image-formats-plugins \
diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile
index ce3d9cd6c..1b6cf825f 100644
--- a/.ci/Ubuntu26.04/Dockerfile
+++ b/.ci/Ubuntu26.04/Dockerfile
@@ -16,6 +16,7 @@ RUN apt-get update && \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
+ libssl-dev \
ninja-build \
protobuf-compiler \
qt6-image-formats-plugins \
diff --git a/.ci/compile.sh b/.ci/compile.sh
index 0d65f9652..76d1c6c77 100755
--- a/.ci/compile.sh
+++ b/.ci/compile.sh
@@ -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"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index e895e2220..75fbc59f1 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -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
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 3ad86b66f..37a4a3cba 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -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"
diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
index df4fe233c..255e8b045 100644
--- a/.github/workflows/docker-release.yml
+++ b/.github/workflows/docker-release.yml
@@ -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-*
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 630b77eda..bca36ef9a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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()
diff --git a/Dockerfile b/Dockerfile
index 382309d47..7d3deb5fb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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/*
diff --git a/README.md b/README.md
index f22df461f..5935bb540 100644
--- a/README.md
+++ b/README.md
@@ -149,15 +149,15 @@ You can then
The following flags (with their non-default values) can be passed to `cmake`:
-| Flag | Description |
-| --- | --- |
-| `-DWITH_SERVER=1` | Build Servatrice server |
-| `-DWITH_CLIENT=0` | Don't build Cockatrice client |
-| `-DWITH_ORACLE=0` | Don't build Oracle card database tool |
-| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
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
**Note:** `make clean` will remove the .ts files |
-| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available |
+| Flag | Description |
+| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| `-DWITH_SERVER=1` | Build Servatrice server |
+| `-DWITH_CLIENT=0` | Don't build Cockatrice client |
+| `-DWITH_ORACLE=0` | Don't build Oracle card database tool |
+| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
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
**Note:** `make clean` will remove the .ts files |
+| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available |
# Run
diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake
index 0259d12e1..971c9094d 100644
--- a/cmake/FindQtRuntime.cmake
+++ b/cmake/FindQtRuntime.cmake
@@ -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).
diff --git a/cmake/pch/qtcore_pch.h b/cmake/pch/qtcore_pch.h
new file mode 100644
index 000000000..cc3dd12ee
--- /dev/null
+++ b/cmake/pch/qtcore_pch.h
@@ -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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
diff --git a/cmake/pch/qtwidgets_pch.h b/cmake/pch/qtwidgets_pch.h
new file mode 100644
index 000000000..2c63f450e
--- /dev/null
+++ b/cmake/pch/qtwidgets_pch.h
@@ -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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 2f629fed2..63ccc4e9c 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -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"
diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md
index 0c8bdb450..fd0a12507 100644
--- a/cockatrice/resources/help/search.md
+++ b/cockatrice/resources/help/search.md
@@ -52,6 +52,7 @@ In this list of examples below, each entry has an explanation and can be clicked
Edition:
[set:lea](#set:lea) (Cards that appear in Alpha, which has the set code LEA)
[e:lea OR e:leb](#e:lea OR e:leb) (Cards that appear in Alpha or Beta)
+[e<8ED](#e<8ED) (Cards that appear before 8th edition)
Negate:
[c:wu -c:m](#c:wu -c:m) (Any card that is white or blue, but not multicolored)
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
index 890a621c8..c1598bd25 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
@@ -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);
diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp
index 785f753e7..f109fbcd3 100644
--- a/cockatrice/src/filters/filter_builder.cpp
+++ b/cockatrice/src/filters/filter_builder.cpp
@@ -1,5 +1,6 @@
#include "filter_builder.h"
+#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include
@@ -21,7 +22,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast(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);
diff --git a/cockatrice/src/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp
index 6da18b662..0970e4272 100644
--- a/cockatrice/src/game/board/counter_state.cpp
+++ b/cockatrice/src/game/board/counter_state.cpp
@@ -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);
}
\ No newline at end of file
diff --git a/cockatrice/src/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h
index 0f2f16b55..4c7b34473 100644
--- a/cockatrice/src/game/board/counter_state.h
+++ b/cockatrice/src/game/board/counter_state.h
@@ -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;
diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp
index bc68d4d7c..f146cdbb4 100644
--- a/cockatrice/src/game/game_event_handler.cpp
+++ b/cockatrice/src/game/game_event_handler.cpp
@@ -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 {
diff --git a/cockatrice/src/game/player/event_processing_options.h b/cockatrice/src/game/player/event_processing_options.h
index 4c7663789..06238d77e 100644
--- a/cockatrice/src/game/player/event_processing_options.h
+++ b/cockatrice/src/game/player/event_processing_options.h
@@ -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
diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp
index bc48298f7..277b8b1d4 100644
--- a/cockatrice/src/game/player/player_event_handler.cpp
+++ b/cockatrice/src/game/player/player_event_handler.cpp
@@ -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));
diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h
index 48ad85e88..300cacd08 100644
--- a/cockatrice/src/game/player/player_event_handler.h
+++ b/cockatrice/src/game/player/player_event_handler.h
@@ -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);
diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp
index 45ba09aac..143df5c57 100644
--- a/cockatrice/src/game/player/player_logic.cpp
+++ b/cockatrice/src/game/player/player_logic.cpp
@@ -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()) {
diff --git a/cockatrice/src/game/player/player_manager.cpp b/cockatrice/src/game/player/player_manager.cpp
index 6772d3ff1..8486efbeb 100644
--- a/cockatrice/src/game/player/player_manager.cpp
+++ b/cockatrice/src/game/player/player_manager.cpp
@@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
return player;
}
+void PlayerManager::clearSpectators()
+{
+ const QList spectatorIds = spectators.keys();
+ for (int spectatorId : spectatorIds) {
+ removeSpectator(spectatorId);
+ }
+}
+
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
{
// Everything else cares about this
diff --git a/cockatrice/src/game/player/player_manager.h b/cockatrice/src/game/player/player_manager.h
index 2f8b87af8..504e65396 100644
--- a/cockatrice/src/game/player/player_manager.h
+++ b/cockatrice/src/game/player/player_manager.h
@@ -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;
diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp
index e63117e13..4ba04804f 100644
--- a/cockatrice/src/game_graphics/board/abstract_counter.cpp
+++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp
@@ -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).
diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h
index 9ddcc6d58..67b5b4074 100644
--- a/cockatrice/src/game_graphics/board/abstract_counter.h
+++ b/cockatrice/src/game_graphics/board/abstract_counter.h
@@ -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;
diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp
index c40c8c214..c2dc455cc 100644
--- a/cockatrice/src/game_graphics/board/card_item.cpp
+++ b/cockatrice/src/game_graphics/board/card_item.cpp
@@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
for (const auto &item : scene()->selectedItems()) {
CardItem *card = qgraphicsitem_cast(item);
- if (card == nullptr) {
+ if (card == nullptr || card == this) {
continue;
}
if (card->getZone() != state->getZone()) {
diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp
index 1278737a0..1acd02a75 100644
--- a/cockatrice/src/game_graphics/deckview/deck_view.cpp
+++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp
@@ -10,7 +10,6 @@
#include
#include
#include
-#include
#include
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(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);
diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp
index 87af4c73c..17af7618b 100644
--- a/cockatrice/src/game_graphics/game_scene.cpp
+++ b/cockatrice/src/game_graphics/game_scene.cpp
@@ -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 zoneViewCopy = zoneViews;
+ for (ZoneViewWidget *zone : zoneViewCopy) {
if (zone->getPlayer() == player) {
zone->close();
}
@@ -659,7 +669,10 @@ CardItem *GameScene::findTopmostCardInZone(const QList &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 zoneViewCopy = zoneViews;
+ for (auto *view : zoneViewCopy) {
ZoneViewZone *temp = view->getZone();
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
qobject_cast(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();
}
diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h
index c12696189..859d7a6eb 100644
--- a/cockatrice/src/game_graphics/game_scene.h
+++ b/cockatrice/src/game_graphics/game_scene.h
@@ -54,9 +54,11 @@ private:
QPointer hoveredCard; ///< Currently hovered card
QBasicTimer *animationTimer; ///< Timer for scene animations
QHash 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
+ 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.
diff --git a/cockatrice/src/game_graphics/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp
index 35989ff38..8dcbcfdaa 100644
--- a/cockatrice/src/game_graphics/hand_counter.cpp
+++ b/cockatrice/src/game_graphics/hand_counter.cpp
@@ -1,5 +1,6 @@
#include "hand_counter.h"
+#include "../interface/pixel_map_generator.h"
#include "zones/card_zone.h"
#include
@@ -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);
diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp
index 7eb3945b3..08cb6cac9 100644
--- a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp
+++ b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp
@@ -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"));
}
diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.h b/cockatrice/src/game_graphics/player/menu/tally_menu.h
index acd1daf67..11802fd20 100644
--- a/cockatrice/src/game_graphics/player/menu/tally_menu.h
+++ b/cockatrice/src/game_graphics/player/menu/tally_menu.h
@@ -24,6 +24,7 @@ private:
QAction *aTallyNone = nullptr;
QAction *aTallySubtypes = nullptr;
QAction *aTallyTotalPower = nullptr;
+ QAction *aTallyTotalToughness = nullptr;
QAction *createTallyAction(TallyType tallyType);
};
diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
index 8bf2703e1..122ab83be 100644
--- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp
+++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
@@ -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();
}
diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp
index 4268e1019..a7ec2e4a9 100644
--- a/cockatrice/src/game_graphics/player/player_list_widget.cpp
+++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp
@@ -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 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 i(players);
diff --git a/cockatrice/src/game_graphics/player/player_list_widget.h b/cockatrice/src/game_graphics/player/player_list_widget.h
index a53cfa989..f2f0be5fd 100644
--- a/cockatrice/src/game_graphics/player/player_list_widget.h
+++ b/cockatrice/src/game_graphics/player/player_list_widget.h
@@ -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
diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp
index 910ee9c17..d6c28370d 100644
--- a/cockatrice/src/game_graphics/player/player_target.cpp
+++ b/cockatrice/src/game_graphics/player/player_target.cpp
@@ -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());
diff --git a/cockatrice/src/game_graphics/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h
index af0e9c8b7..1d06c6274 100644
--- a/cockatrice/src/game_graphics/player/player_target.h
+++ b/cockatrice/src/game_graphics/player/player_target.h
@@ -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;
diff --git a/cockatrice/src/game_graphics/tally/stats_tally.cpp b/cockatrice/src/game_graphics/tally/stats_tally.cpp
index e7a6621fa..7e05c3fb1 100644
--- a/cockatrice/src/game_graphics/tally/stats_tally.cpp
+++ b/cockatrice/src/game_graphics/tally/stats_tally.cpp
@@ -34,3 +34,31 @@ QList StatsTally::computeTotalPower(const QList &cards)
QString name = QCoreApplication::translate("StatsTally", "Total Power");
return {TallyRow{name, QString::number(total)}};
}
+
+static int sumToughness(const QList &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 StatsTally::computeTotalToughness(const QList &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)}};
+}
diff --git a/cockatrice/src/game_graphics/tally/stats_tally.h b/cockatrice/src/game_graphics/tally/stats_tally.h
index 4c3d93b56..e499587eb 100644
--- a/cockatrice/src/game_graphics/tally/stats_tally.h
+++ b/cockatrice/src/game_graphics/tally/stats_tally.h
@@ -16,6 +16,14 @@ namespace StatsTally
*/
QList computeTotalPower(const QList &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 computeTotalToughness(const QList &cards);
+
} // namespace StatsTally
#endif // COCKATRICE_STATS_TALLY_H
diff --git a/cockatrice/src/game_graphics/tally/tally.cpp b/cockatrice/src/game_graphics/tally/tally.cpp
index aa2cae024..21806ee84 100644
--- a/cockatrice/src/game_graphics/tally/tally.cpp
+++ b/cockatrice/src/game_graphics/tally/tally.cpp
@@ -21,6 +21,8 @@ QList Tally::compute(const QList &cards, const TallyType t
return SubtypeTally::countSubtypes(cards);
case TallyType::TotalPower:
return StatsTally::computeTotalPower(cards);
+ case TallyType::TotalToughness:
+ return StatsTally::computeTotalToughness(cards);
}
return {};
}
diff --git a/cockatrice/src/game_graphics/tally/tally.h b/cockatrice/src/game_graphics/tally/tally.h
index 97406cddb..84c54918f 100644
--- a/cockatrice/src/game_graphics/tally/tally.h
+++ b/cockatrice/src/game_graphics/tally/tally.h
@@ -21,7 +21,8 @@ enum class TallyType
None,
Subtypes,
TotalPower,
- MaxValue = TotalPower // sentinel value
+ TotalToughness,
+ MaxValue = TotalToughness // sentinel value
};
namespace Tally
diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp
index b52a4955a..1a8f7a910 100644
--- a/cockatrice/src/game_graphics/zones/hand_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp
@@ -41,7 +41,8 @@ void HandZone::handleDropEvent(const QList &dragItems,
}
}
} else {
- x = calcDropIndexFromY(dropPoint.y());
+ bool sameZone = startZone == getLogic();
+ x = calcDropIndexFromY(dropPoint.y(), !sameZone);
}
Command_MoveCard cmd;
diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp
index c58c41b92..470c70fcf 100644
--- a/cockatrice/src/game_graphics/zones/select_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/select_zone.cpp
@@ -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()
diff --git a/cockatrice/src/game_graphics/zones/select_zone.h b/cockatrice/src/game_graphics/zones/select_zone.h
index 7408f29b6..b5d3ca37a 100644
--- a/cockatrice/src/game_graphics/zones/select_zone.h
+++ b/cockatrice/src/game_graphics/zones/select_zone.h
@@ -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.
diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp
index e9b14f13d..ff62097c7 100644
--- a/cockatrice/src/game_graphics/zones/stack_zone.cpp
+++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp
@@ -57,18 +57,14 @@ void StackZone::handleDropEvent(const QList &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(cards.size());
}
Command_MoveCard cmd;
diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
index 17118e80d..c13b79fea 100644
--- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
+++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
@@ -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(option);
if (titleBar) {
- titleBar->icon = QPixmap("theme:cockatrice");
+ titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
}
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
index 2f46e7941..8c81d641d 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
@@ -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()) {
diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp
index 39a0c1071..f03339da8 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.cpp
+++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp
@@ -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 cardsByType;
QMap cardTotalByType;
int cardTotal = 0;
+ QList subZones;
for (int j = 0; j < zoneNode->size(); j++) {
auto *card = dynamic_cast(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(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 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 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 cards;
+ QVector subZones;
+ for (int i = 0; i < node->size(); i++) {
+ if (auto *card = dynamic_cast(node->at(i))) {
+ cards.append(card);
+ } else if (auto *zone = dynamic_cast(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(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(node->at(i)));
- }
+ QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition();
+ printDeckListNode(&cellCursor, subZone);
}
cursor->movePosition(QTextCursor::End);
diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h
index ac23e1ee0..b851c6895 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.h
+++ b/cockatrice/src/interface/deck_loader/deck_loader.h
@@ -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 cards,
bool addComments = true,
- bool addSetNameAndNumber = true);
+ bool addSetNameAndNumber = true,
+ const QString &boardZoneName = QString());
};
#endif
diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp
index 9b8c4bcdc..e74e86471 100644
--- a/cockatrice/src/interface/pixel_map_generator.cpp
+++ b/cockatrice/src/interface/pixel_map_generator.cpp
@@ -1,5 +1,7 @@
#include "pixel_map_generator.h"
+#include "theme_manager.h"
+
#include
#include
#include
@@ -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 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();
+}
diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h
index 17720166a..b6e822fd9 100644
--- a/cockatrice/src/interface/pixel_map_generator.h
+++ b/cockatrice/src/interface/pixel_map_generator.h
@@ -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
diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp
index e6b4b3c7f..cc6be175a 100644
--- a/cockatrice/src/interface/theme_manager.cpp
+++ b/cockatrice/src/interface/theme_manager.cpp
@@ -1,10 +1,12 @@
#include "theme_manager.h"
#include "../../client/settings/cache_settings.h"
+#include "pixel_map_generator.h"
#include
#include
#include
+#include
#include
#include
#include
@@ -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();
}
diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h
index 79a1b6470..ac35042a0 100644
--- a/cockatrice/src/interface/theme_manager.h
+++ b/cockatrice/src/interface/theme_manager.h
@@ -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 "-"
+ // when a file exists at that stem, otherwise the plain "" 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
+ // ("-.") and returns an empty string when no
+ // variant exists — it never falls back to the plain "" 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
diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp
new file mode 100644
index 000000000..b26b73593
--- /dev/null
+++ b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp
@@ -0,0 +1,18 @@
+#include "card_art_utils.h"
+
+#include
+#include
+
+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
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.h b/cockatrice/src/interface/widgets/cards/card_art_utils.h
new file mode 100644
index 000000000..5c331a12c
--- /dev/null
+++ b/cockatrice/src/interface/widgets/cards/card_art_utils.h
@@ -0,0 +1,25 @@
+#ifndef CARD_ART_UTILS_H
+#define CARD_ART_UTILS_H
+
+#include
+
+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
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
index 3f36e559c..bfbdd7e42 100644
--- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
@@ -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));
}
}
diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h
index 2308ccf8d..a3bf70981 100644
--- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h
+++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h
@@ -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;
diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp
index 79ae087d7..de622bdc8 100644
--- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp
@@ -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
#include
@@ -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
diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp
index eaf3a67b0..b00d9db1e 100644
--- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp
@@ -5,6 +5,7 @@
#include "libcockatrice/card/database/card_database_manager.h"
#include
+#include
#include
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 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);
}
}
diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h
index b426fca30..53f3fa7cf 100644
--- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h
+++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h
@@ -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);
diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
index 089abc5c8..483fc71b0 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
@@ -1,5 +1,6 @@
#include "abstract_analytics_panel_widget.h"
+#include "../../pixel_map_generator.h"
#include "deck_list_statistics_analyzer.h"
#include
@@ -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);
diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp
index 7c782b074..00388a3cd 100644
--- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp
@@ -90,6 +90,13 @@ void CardDatabaseView::decrementCard(const QString &zoneName)
emit cardDecremented(currentCardName(), zoneName);
}
+void CardDatabaseView::setZoneMenuProvider(const std::function>()> &provider,
+ const std::function &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); });
diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h
index 175ec12b9..668444199 100644
--- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h
+++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h
@@ -4,6 +4,7 @@
#include "../../key_signals.h"
#include
+#include
#include
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>()> 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 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>()> &provider,
+ const std::function &newZoneHandler);
+
signals:
void cardChanged(const QString &cardName);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
index 2a491de4f..6269f0323 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
@@ -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
+
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> {
+ QList> 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);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
index 9da821813..c8bdacf0d 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
index 14defc8e9..a3653f03e 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
@@ -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
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -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();
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h
index 9db01e2e5..1e5f4e677 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h
@@ -19,6 +19,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -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);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
index 2d4fb60e8..c6b6e4416 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
@@ -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
#include
@@ -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();
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
index cef459752..c93f12b34 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
index eda741728..e563729a4 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
@@ -2,6 +2,7 @@
#include
#include
+#include
DeckStateManager::DeckStateManager(QObject *parent)
: QObject(parent), deckList(QSharedPointer(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 &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
*/
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h
index b9c99903e..2c8b34a39 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h
@@ -5,6 +5,7 @@
#include "deck_list_model.h"
#include
+#include
#include
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 &operation);
void doCardModified();
void doMetadataModified();
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp
new file mode 100644
index 000000000..9a0be2570
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp
@@ -0,0 +1,145 @@
+#include "deck_zone_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+DeckZoneDialog::DeckZoneDialog(QWidget *parent,
+ const QString &initialBoardName,
+ const std::function &_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 &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 &nameValidator)
+{
+ DeckZoneDialog dialog(parent, {}, nameValidator, false);
+ dialog.setZoneName(currentZoneName);
+ return dialog.exec() == QDialog::Accepted ? dialog.getZoneName() : QString();
+}
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
new file mode 100644
index 000000000..6f55617a8
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
@@ -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
+#include
+#include
+#include
+
+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 &_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 &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 &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 nameValidator;
+ bool allowBoardSelection;
+};
+
+#endif // DECK_ZONE_DIALOG_H
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp
index aa8a916f8..700012d45 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp
@@ -1,6 +1,7 @@
#include "dlg_connect.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include
#include
@@ -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);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
index 198fa259b..a4a31d78d 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
@@ -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
#include
+#include
+#include
+#include
#include
+#include
#include
+#include
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 &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;
+}
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h
index 6642ad8c6..526582135 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h
@@ -13,6 +13,9 @@
#include
#include
#include
+#include
+
+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 &convert);
+
private:
QVBoxLayout *layout;
QLabel *label;
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp
index f249976c2..a56aa8e35 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp
@@ -1,5 +1,6 @@
#include "dlg_edit_tokens.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h"
#include
@@ -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;
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp
index 7c107eb2f..556c270be 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp
index 6ae8c9adb..6d022861c 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp
index 883cfcd03..de7dd3e97 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp
index 10fcdcb43..0d030b973 100644
--- a/cockatrice/src/interface/widgets/general/home_widget.cpp
+++ b/cockatrice/src/interface/widgets/general/home_widget.cpp
@@ -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
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();
diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
index 618ac6f26..4af02fe4f 100644
--- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
+++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
@@ -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();
diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h
index 2c186ef95..21d7b6e06 100644
--- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h
+++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h
@@ -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;
diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
index 12116de7a..50e8ff63d 100644
--- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
+++ b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
@@ -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(qMin(total, INT_MAX)) : 0);
+ progressBar->setValue(static_cast(qMin(done, INT_MAX)));
+ if (total > 0) {
+ const int percent = static_cast((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();
diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h
index 0461d11d5..870e759ea 100644
--- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h
+++ b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h
@@ -30,6 +30,7 @@ public:
void retranslateUi() override;
void onUpdateFinished(bool success);
+ void onUpdateProgress(const QString &stage, qint64 done, qint64 total);
signals:
void updateRequested();
diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
index 57706cf93..9459c5ea9 100644
--- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
+++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
@@ -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));
diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
index 9c433ab5a..881c3e3ac 100644
--- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
+++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
@@ -1,5 +1,7 @@
#include "settings_button_widget.h"
+#include "../../pixel_map_generator.h"
+
#include
#include
#include
@@ -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);
diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp
index a2c1e0ff0..c51b96b6c 100644
--- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp
+++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp
index 6c85d950e..c92771f8c 100644
--- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp
+++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp
index f41002247..a8bf54e91 100644
--- a/cockatrice/src/interface/widgets/server/game_selector.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector.cpp
@@ -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);
diff --git a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
index 1f034b767..d62bf81aa 100644
--- a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
+++ b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
@@ -1,5 +1,7 @@
#include "remote_replay_list_tree_widget.h"
+#include "../../../pixel_map_generator.h"
+
#include
#include
#include
@@ -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()
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
index 3a1876fa1..2ba745715 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
@@ -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
#include
@@ -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);
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
index 2592237c4..e8283a891 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
+++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
class UserCardArtProvider : public QObject
{
@@ -16,7 +17,7 @@ public:
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap &cache() const;
- static QPixmap cropCardArt(const QPixmap &fullRes);
+ static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card);
signals:
void cardArtUpdated(const QString &userName);
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp
index 532112964..d49e3d540 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp
@@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview()
return;
}
- currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
+ currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card);
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp
index 8d5d423f6..0d2267a63 100644
--- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp
@@ -51,6 +51,8 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this);
+ aPromoteToDeveloper = new QAction(QString(), this);
+ aDemoteFromDeveloper = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this);
@@ -76,6 +78,8 @@ void UserContextMenu::retranslateUi()
aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from judge"));
+ aPromoteToDeveloper->setText(tr("Promote user to &developer"));
+ aDemoteFromDeveloper->setText(tr("Demote user from de&veloper"));
aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user"));
}
@@ -268,7 +272,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) {
- if (cmd.should_be_mod() || cmd.should_be_judge()) {
+ if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast(parent()), tr("Success"),
tr("Successfully promoted user."));
} else {
@@ -276,7 +280,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
}
} else {
- if (cmd.should_be_mod() || cmd.should_be_judge()) {
+ if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to promote user."));
} else {
QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to demote user."));
@@ -437,9 +441,18 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge);
}
+
+ if (userLevel.testFlag(ServerInfo_User::IsDeveloper) &&
+ (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
+ menu->addAction(aDemoteFromDeveloper);
+
+ } else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
+ (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
+ menu->addAction(aPromoteToDeveloper);
+ }
}
aDetails->setEnabled(true);
- aChat->setEnabled(anotherUser && online);
+ aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
aShowGames->setEnabled(online);
aReport->setEnabled(anotherUser);
aAddToBuddyList->setEnabled(anotherUser);
@@ -455,6 +468,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aInvestigateUser->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser);
+ aPromoteToJudge->setEnabled(anotherUser);
+ aDemoteFromJudge->setEnabled(anotherUser);
+ aPromoteToDeveloper->setEnabled(anotherUser);
+ aDemoteFromDeveloper->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) {
@@ -489,6 +506,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execAdjustMod(userName, actionClicked == aPromoteToMod);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
execAdjustJudge(userName, actionClicked == aPromoteToJudge);
+ } else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) {
+ execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper);
} else if (actionClicked == aBanHistory) {
execBanHistory(userName);
} else if (actionClicked == aWarnUser) {
@@ -606,7 +625,15 @@ void UserContextMenu::execAddToIgnore(const QString &userName)
Command_AddToList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
- client->sendCommand(client->prepareSessionCommand(cmd));
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ connect(pend, &PendingCommand::finished, this,
+ [this, userName](const Response &response, const CommandContainer &, const QVariant &) {
+ if (response.response_code() == Response::RespOk) {
+ QMessageBox::information(static_cast(parent()), tr("Ignore list"),
+ tr("%1 has been added to your ignore list.").arg(userName));
+ }
+ });
+ client->sendCommand(pend);
}
void UserContextMenu::execRemoveFromIgnore(const QString &userName)
@@ -698,4 +725,14 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
-}
\ No newline at end of file
+}
+
+void UserContextMenu::execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper)
+{
+ Command_AdjustMod cmd;
+ cmd.set_user_name(userName.toStdString());
+ cmd.set_should_be_developer(shouldBeDeveloper);
+ PendingCommand *pend = client->prepareAdminCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
+ client->sendCommand(pend);
+}
diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h
index f1ce931f8..6abbc057a 100644
--- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h
+++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h
@@ -45,6 +45,7 @@ private:
QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge;
+ QAction *aPromoteToDeveloper, *aDemoteFromDeveloper;
QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes;
std::function()> gameInviteLinkProvider;
@@ -123,6 +124,7 @@ public:
void execInvestigateUser(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod);
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
+ void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);
private:
void execInvite(const QString &userName, const GameInviteOption &option);
diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp
index 875bdfb05..3d89cecf5 100644
--- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp
@@ -122,6 +122,8 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
userLevelText = tr("Administrator");
+ } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
+ userLevelText = tr("Developer");
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
userLevelText = tr("Moderator");
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
index f6f34a6a5..8be76eea0 100644
--- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
@@ -245,6 +245,9 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
if (level.testFlag(ServerInfo_User::IsAdmin)) {
return QColor(245, 158, 11);
}
+ if (level.testFlag(ServerInfo_User::IsDeveloper)) {
+ return QColor(185, 28, 28);
+ }
if (level.testFlag(ServerInfo_User::IsModerator)) {
return QColor(59, 130, 246);
}
@@ -300,6 +303,8 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
} badge;
if (level.testFlag(ServerInfo_User::IsAdmin)) {
badge = {"ADMIN", QColor(245, 158, 11)};
+ } else if (level.testFlag(ServerInfo_User::IsDeveloper)) {
+ badge = {"DEV", QColor(185, 28, 28)};
} else if (level.testFlag(ServerInfo_User::IsModerator)) {
badge = {"MOD", QColor(59, 130, 246)};
} else if (level.testFlag(ServerInfo_User::IsJudge)) {
@@ -525,6 +530,13 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
add(games);
+ // ── Invite (only while the inviter has a joinable game for this user) ────
+ if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) {
+ auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme);
+ connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); });
+ add(invite);
+ }
+
// ── Buddy / ignore (registered users only) ────────────────────────────────
if (!isSelf && isReg) {
if (isBuddy) {
diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h
index 02cc2b44e..ed7320fba 100644
--- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h
+++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -149,6 +150,17 @@ public:
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
void refreshHeader();
+ /**
+ * Sets a predicate evaluated on every action-button rebuild. It receives
+ * the name of the user the popup currently shows; when it returns true an
+ * "Invite" button is shown. The popup itself never resolves the invite
+ * link, it just forwards the request.
+ */
+ void setGameInviteAvailable(std::function available)
+ {
+ gameInviteAvailable = std::move(available);
+ }
+
signals:
void mouseEnteredPopup();
void mouseLeftPopup();
@@ -159,6 +171,7 @@ signals:
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
void chatRequested(const QString &userName);
+ void inviteRequested(const QString &userName);
void detailsRequested(const QString &userName);
void showGamesRequested(const QString &userName);
void addBuddyRequested(const QString &userName);
@@ -200,6 +213,7 @@ private:
QString currentUser;
ServerInfo_User currentUserInfo;
bool currentOnline = false;
+ std::function gameInviteAvailable;
UserInfoHeaderWidget *header;
QWidget *actionArea; ///< rebuilt per user
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
index 5c65b090d..34a3d6ae1 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
@@ -49,6 +49,8 @@ QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool onl
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11);
+ } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
+ accentColor = QColor(185, 28, 28);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@@ -299,6 +301,8 @@ QList UserListPainter::buildBadges(const UserLevelFlags
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)};
+ } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
+ badges << Badge{"DEV", QColor(185, 28, 28)};
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@@ -385,9 +389,9 @@ void UserListPainter::paint(QPainter *painter,
const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online);
- const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) ||
- userLevel.testFlag(ServerInfo_User::IsModerator) ||
- userLevel.testFlag(ServerInfo_User::IsJudge);
+ const bool hasRole =
+ userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsDeveloper) ||
+ userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsJudge);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect);
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
index 2cacfc4f9..1bb7c5288 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
@@ -234,9 +234,11 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt());
- // Admins & Mods need no additional comparison checks, just to see if they're an admin or a moderator
+ // Admins, Developers & Mods need no additional comparison checks, just to see if they're an admin, a developer
+ // or a moderator
static const QList userLevelWithNoOtherPrefOrder = {
- ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator};
+ ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsDeveloper,
+ ServerInfo_User_UserLevelFlag_IsModerator};
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {
@@ -345,6 +347,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
&cardArtProvider->cache(), &cardArtParamsMap,
window()); // parented to main window so it floats above siblings
+ // The invite availability is scoped to the room this list belongs to,
+ // and gated on the room's buddy-only setting for the hovered user.
+ userInfoPopup->setGameInviteAvailable(
+ [this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); });
+
userInfoPopup->hide();
userInfoPopup->setWindowOpacity(0.0);
userInfoPopup->installEventFilter(this);
@@ -662,6 +669,8 @@ void UserListWidget::connectPopupSignals()
// Wire all action signals to UserContextMenu::exec*()
connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
+ connect(userInfoPopup, &UserInfoPopup::inviteRequested, this,
+ [this](const QString &userName) { userContextMenu->execInvite(userName); });
connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h
index 7531ef925..412271160 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h
+++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
class QTreeWidget;
diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp
index 881c54167..c8494f095 100644
--- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp
@@ -154,6 +154,48 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabGroupBox = new QGroupBox;
homeTabGroupBox->setLayout(homeTabGrid);
+ // Playmat settings
+ playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
+ playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
+ playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
+ int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
+ if (visIdx >= 0) {
+ playmatVisibilityCombo.setCurrentIndex(visIdx);
+ }
+ connect(&playmatVisibilityCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) {
+ SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
+ });
+ playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
+
+ // Playmat mode: Override / Fallback / Deck-only
+ playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
+ playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
+ playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
+ int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
+ if (modeIdx >= 0) {
+ playmatModeCombo.setCurrentIndex(modeIdx);
+ }
+ connect(&playmatModeCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) {
+ SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
+ });
+ playmatModeLabel.setBuddy(&playmatModeCombo);
+
+ // User-level playmat settings: fallback collection.
+ connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
+ &AppearanceSettingsPage::openPlaymatCollectionDialog);
+
+ auto *playmatGrid = new QGridLayout;
+ playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
+ playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
+ playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
+ playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
+ playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
+ playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
+
+ playmatGroupBox = new QGroupBox;
+ playmatGroupBox->setLayout(playmatGrid);
+
+ // Styling settings
styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&AppearanceSettings::setStyleUserList);
@@ -259,7 +301,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
cardLayoutGroupBox->setLayout(cardLayoutGrid);
// Card counter colors
-
auto *cardCounterColorsLayout = new QGridLayout;
cardCounterColorsLayout->setColumnStretch(1, 1);
cardCounterColorsLayout->setColumnStretch(3, 1);
@@ -339,47 +380,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
tableGroupBox = new QGroupBox;
tableGroupBox->setLayout(tableGrid);
- // Playmat settings
- playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
- playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
- playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
- int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
- if (visIdx >= 0) {
- playmatVisibilityCombo.setCurrentIndex(visIdx);
- }
- connect(&playmatVisibilityCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) {
- SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
- });
- playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
-
- // Playmat mode: Override / Fallback / Deck-only
- playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
- playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
- playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
- int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
- if (modeIdx >= 0) {
- playmatModeCombo.setCurrentIndex(modeIdx);
- }
- connect(&playmatModeCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) {
- SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
- });
- playmatModeLabel.setBuddy(&playmatModeCombo);
-
- // User-level playmat settings: fallback collection.
- connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
- &AppearanceSettingsPage::openPlaymatCollectionDialog);
-
- auto *playmatGrid = new QGridLayout;
- playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
- playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
- playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
- playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
- playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
- playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
-
- playmatGroupBox = new QGroupBox;
- playmatGroupBox->setLayout(playmatGrid);
-
// putting it all together
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(themeGroupBox);
@@ -512,6 +512,12 @@ void AppearanceSettingsPage::retranslateUi()
homeTabButtonColorSourceBox.setToolTip(
tr("Automatic: extract from background if present, otherwise use theme default"));
+ playmatGroupBox->setTitle(tr("Playmat settings"));
+ playmatVisibilityLabel.setText(tr("Playmat visibility:"));
+ playmatModeLabel.setText(tr("Default collection behavior:"));
+ playmatDefaultLabel.setText(tr("Default playmat collection:"));
+ playmatDefaultEditButton.setText(tr("Edit..."));
+
stylingGroupBox->setTitle(tr("Styling settings"));
styleUserListCheckBox.setText(tr("Style user list"));
@@ -554,9 +560,4 @@ void AppearanceSettingsPage::retranslateUi()
tableGroupBox->setTitle(tr("Table grid layout"));
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
- playmatGroupBox->setTitle(tr("Playmat settings"));
- playmatVisibilityLabel.setText(tr("Playmat visibility:"));
- playmatModeLabel.setText(tr("Default collection behavior:"));
- playmatDefaultLabel.setText(tr("Default playmat collection:"));
- playmatDefaultEditButton.setText(tr("Edit..."));
}
diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h
index 6b0369694..8db71ff8f 100644
--- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h
@@ -44,46 +44,55 @@ private:
QLabel homeTabButtonColorSourceLabel;
QComboBox homeTabButtonColorSourceBox;
- QCheckBox styleUserListCheckBox;
- QCheckBox showShortcutsCheckBox;
- QCheckBox showGameSelectorFilterToolbarCheckBox;
- QLabel minPlayersForMultiColumnLayoutLabel;
- QLabel maxFontSizeForCardsLabel;
- QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;
- QCheckBox bumpSetsWithCardsInDeckToTopCheckBox;
- QCheckBox displayCardNamesCheckBox;
- QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
- QCheckBox cardScalingCheckBox;
- QCheckBox roundCardCornersCheckBox;
- QLabel verticalCardOverlapPercentLabel;
- QSpinBox verticalCardOverlapPercentBox;
- QLabel cardViewInitialRowsMaxLabel;
- QSpinBox cardViewInitialRowsMaxBox;
- QLabel cardViewExpandedRowsMaxLabel;
- QSpinBox cardViewExpandedRowsMaxBox;
- QCheckBox horizontalHandCheckBox;
- QCheckBox leftJustifiedHandCheckBox;
- QCheckBox invertVerticalCoordinateCheckBox;
QLabel playmatVisibilityLabel;
QComboBox playmatVisibilityCombo;
QLabel playmatModeLabel;
QComboBox playmatModeCombo;
QLabel playmatDefaultLabel;
QPushButton playmatDefaultEditButton;
+
+ QCheckBox styleUserListCheckBox;
+
+ QCheckBox showShortcutsCheckBox;
+ QCheckBox showGameSelectorFilterToolbarCheckBox;
+
+ QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;
+ QCheckBox bumpSetsWithCardsInDeckToTopCheckBox;
+
+ QCheckBox displayCardNamesCheckBox;
+ QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
+ QCheckBox cardScalingCheckBox;
+ QCheckBox roundCardCornersCheckBox;
+ QLabel maxFontSizeForCardsLabel;
+ QSpinBox maxFontSizeForCardsEdit;
+
+ QLabel verticalCardOverlapPercentLabel;
+ QSpinBox verticalCardOverlapPercentBox;
+ QLabel cardViewInitialRowsMaxLabel;
+ QSpinBox cardViewInitialRowsMaxBox;
+ QLabel cardViewExpandedRowsMaxLabel;
+ QSpinBox cardViewExpandedRowsMaxBox;
+
+ QList cardCounterNames;
+
+ QCheckBox horizontalHandCheckBox;
+ QCheckBox leftJustifiedHandCheckBox;
+
+ QCheckBox invertVerticalCoordinateCheckBox;
+ QLabel minPlayersForMultiColumnLayoutLabel;
+ QSpinBox minPlayersForMultiColumnLayoutEdit;
+
QGroupBox *themeGroupBox;
QGroupBox *homeTabGroupBox;
+ QGroupBox *playmatGroupBox;
QGroupBox *stylingGroupBox;
QGroupBox *menuGroupBox;
QGroupBox *printingsGroupBox;
QGroupBox *cardsGroupBox;
QGroupBox *cardLayoutGroupBox;
- QGroupBox *handGroupBox;
- QGroupBox *playmatGroupBox;
- QGroupBox *tableGroupBox;
QGroupBox *cardCountersGroupBox;
- QList cardCounterNames;
- QSpinBox minPlayersForMultiColumnLayoutEdit;
- QSpinBox maxFontSizeForCardsEdit;
+ QGroupBox *handGroupBox;
+ QGroupBox *tableGroupBox;
public:
AppearanceSettingsPage();
diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp
index f425afe60..f3eac05b8 100644
--- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp
@@ -1,6 +1,7 @@
#include "deck_editor_settings_page.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "update/card_spoiler/spoiler_background_updater.h"
#include
@@ -53,15 +54,15 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
aAdd = new QAction(this);
- aAdd->setIcon(QPixmap("theme:icons/increment"));
+ aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL);
aEdit = new QAction(this);
- aEdit->setIcon(QPixmap("theme:icons/pencil"));
+ aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL);
aRemove = new QAction(this);
- aRemove->setIcon(QPixmap("theme:icons/decrement"));
+ aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
auto *urlToolBar = new QToolBar;
diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp
index a293660f9..62b06fb60 100644
--- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp
@@ -425,29 +425,28 @@ void GeneralSettingsPage::updateStartupServerControlsVisibility()
void GeneralSettingsPage::retranslateUi()
{
+ const auto &settings = SettingsCache::instance();
+
languageGroupBox->setTitle(tr("Language settings"));
languageLabel.setText(tr("Language:"));
-
- versionGroupBox->setTitle(tr("Version settings"));
- cardDatabaseGroupBox->setTitle(tr("Card database"));
- startupGroupBox->setTitle(tr("Startup settings"));
-
- if (SettingsCache::instance().getIsPortableBuild()) {
- pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)"));
- } else {
- pathsGroupBox->setTitle(tr("Paths"));
- }
advertiseTranslationPageLabel.setText(
QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
- deckPathLabel.setText(tr("Decks directory:"));
- filtersPathLabel.setText(tr("Filters directory:"));
- replaysPathLabel.setText(tr("Replays directory:"));
- picsPathLabel.setText(tr("Pictures directory:"));
- cardDatabasePathLabel.setText(tr("Card database:"));
- customCardDatabasePathLabel.setText(tr("Custom database directory:"));
- tokenDatabasePathLabel.setText(tr("Token database:"));
+
+ versionGroupBox->setTitle(tr("Version settings"));
updateReleaseChannelLabel.setText(tr("Update channel"));
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
+ updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
+ newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
+
+ // We can't change the strings after they're put into the QComboBox, so this is our workaround
+ int oldIndex = updateReleaseChannelBox.currentIndex();
+ updateReleaseChannelBox.clear();
+ for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) {
+ updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8()));
+ }
+ updateReleaseChannelBox.setCurrentIndex(oldIndex);
+
+ cardDatabaseGroupBox->setTitle(tr("Card database"));
startupCardUpdateCheckBehaviorLabel.setText(tr("Check for card database updates on startup"));
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexNone, tr("Don't check"));
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexPrompt,
@@ -456,8 +455,13 @@ void GeneralSettingsPage::retranslateUi()
tr("Always update in the background"));
cardUpdateCheckIntervalLabel.setText(tr("Check for card database updates every"));
cardUpdateCheckIntervalSpinBox.setSuffix(tr(" days"));
- updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
- newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
+
+ QDate lastCheckDate = settings.updates().getLastCardUpdateCheck();
+ int daysAgo = lastCheckDate.daysTo(QDate::currentDate());
+ lastCardUpdateCheckDateLabel.setText(
+ tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo));
+
+ startupGroupBox->setTitle(tr("Startup settings"));
showTipsOnStartup.setText(tr("Show tips on startup"));
startupTabLabel.setText(tr("Startup tab:"));
startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home"));
@@ -473,21 +477,18 @@ void GeneralSettingsPage::retranslateUi()
startupServerLabel.setText(tr("Server:"));
startupRoomLabel.setText(tr("Room:"));
startupRoomNameEdit->setPlaceholderText(tr("Room name"));
- resetAllPathsButton->setText(tr("Reset all paths"));
- const auto &settings = SettingsCache::instance();
-
- QDate lastCheckDate = settings.updates().getLastCardUpdateCheck();
- int daysAgo = lastCheckDate.daysTo(QDate::currentDate());
-
- lastCardUpdateCheckDateLabel.setText(
- tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo));
-
- // We can't change the strings after they're put into the QComboBox, so this is our workaround
- int oldIndex = updateReleaseChannelBox.currentIndex();
- updateReleaseChannelBox.clear();
- for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) {
- updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8()));
+ if (settings.getIsPortableBuild()) {
+ pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)"));
+ } else {
+ pathsGroupBox->setTitle(tr("Paths"));
}
- updateReleaseChannelBox.setCurrentIndex(oldIndex);
-}
\ No newline at end of file
+ deckPathLabel.setText(tr("Decks directory:"));
+ filtersPathLabel.setText(tr("Filters directory:"));
+ replaysPathLabel.setText(tr("Replays directory:"));
+ picsPathLabel.setText(tr("Pictures directory:"));
+ cardDatabasePathLabel.setText(tr("Card database:"));
+ customCardDatabasePathLabel.setText(tr("Custom database directory:"));
+ tokenDatabasePathLabel.setText(tr("Token database:"));
+ resetAllPathsButton->setText(tr("Reset all paths"));
+}
diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h
index 8dd7e8798..e0c1a47bf 100644
--- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h
@@ -42,6 +42,37 @@ private:
QGroupBox *startupGroupBox;
QGroupBox *pathsGroupBox;
+ QLabel languageLabel;
+ QComboBox languageBox;
+ QLabel advertiseTranslationPageLabel;
+
+ QLabel updateReleaseChannelLabel;
+ QComboBox updateReleaseChannelBox;
+ QCheckBox startupUpdateCheckCheckBox;
+ QCheckBox updateNotificationCheckBox;
+ QCheckBox newVersionOracleCheckBox;
+
+ QLabel startupCardUpdateCheckBehaviorLabel;
+ QComboBox startupCardUpdateCheckBehaviorSelector;
+ QLabel cardUpdateCheckIntervalLabel;
+ QSpinBox cardUpdateCheckIntervalSpinBox;
+ QLabel lastCardUpdateCheckDateLabel;
+
+ QCheckBox showTipsOnStartup;
+ QLabel startupTabLabel;
+ QComboBox startupTabSelector;
+ QLabel startupServerLabel;
+ QComboBox startupServerSelector;
+ QLabel startupRoomLabel;
+ QLineEdit *startupRoomNameEdit;
+
+ QLabel deckPathLabel;
+ QLabel filtersPathLabel;
+ QLabel replaysPathLabel;
+ QLabel picsPathLabel;
+ QLabel cardDatabasePathLabel;
+ QLabel customCardDatabasePathLabel;
+ QLabel tokenDatabasePathLabel;
QLineEdit *deckPathEdit;
QLineEdit *filtersPathEdit;
QLineEdit *replaysPathEdit;
@@ -51,33 +82,6 @@ private:
QLineEdit *tokenDatabasePathEdit;
QPushButton *resetAllPathsButton;
QLabel *allPathsResetLabel;
- QComboBox languageBox;
- QCheckBox startupUpdateCheckCheckBox;
- QLabel startupCardUpdateCheckBehaviorLabel;
- QComboBox startupCardUpdateCheckBehaviorSelector;
- QLabel cardUpdateCheckIntervalLabel;
- QSpinBox cardUpdateCheckIntervalSpinBox;
- QLabel lastCardUpdateCheckDateLabel;
- QCheckBox updateNotificationCheckBox;
- QCheckBox newVersionOracleCheckBox;
- QComboBox updateReleaseChannelBox;
- QLabel languageLabel;
- QLabel deckPathLabel;
- QLabel filtersPathLabel;
- QLabel replaysPathLabel;
- QLabel picsPathLabel;
- QLabel cardDatabasePathLabel;
- QLabel customCardDatabasePathLabel;
- QLabel tokenDatabasePathLabel;
- QLabel updateReleaseChannelLabel;
- QLabel advertiseTranslationPageLabel;
- QCheckBox showTipsOnStartup;
- QLabel startupTabLabel;
- QComboBox startupTabSelector;
- QLabel startupServerLabel;
- QComboBox startupServerSelector;
- QLabel startupRoomLabel;
- QLineEdit *startupRoomNameEdit;
};
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp
index e4f24ab73..a3b89f8c4 100644
--- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp
@@ -1,6 +1,7 @@
#include "messages_settings_page.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h"
#include
@@ -59,6 +60,10 @@ MessagesSettingsPage::MessagesSettingsPage()
connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
&ChatSettings::setRoomHistory);
+ ignoreAllPrivateMessagesCheckBox.setChecked(SettingsCache::instance().chat().getIgnoreAllPrivateMessages());
+ connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
+ &ChatSettings::setIgnoreAllPrivateMessages);
+
customAlertString = new QLineEdit();
customAlertString->setText(SettingsCache::instance().chat().getHighlightWords());
connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(),
@@ -76,6 +81,7 @@ MessagesSettingsPage::MessagesSettingsPage()
chatGrid->addWidget(&messagePopups, 5, 0);
chatGrid->addWidget(&mentionPopups, 6, 0);
chatGrid->addWidget(&roomHistory, 7, 0);
+ chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0);
chatGroupBox = new QGroupBox;
chatGroupBox->setLayout(chatGrid);
@@ -102,15 +108,15 @@ MessagesSettingsPage::MessagesSettingsPage()
}
aAdd = new QAction(this);
- aAdd->setIcon(QPixmap("theme:icons/increment"));
+ aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd);
aEdit = new QAction(this);
- aEdit->setIcon(QPixmap("theme:icons/pencil"));
+ aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit);
aRemove = new QAction(this);
- aRemove->setIcon(QPixmap("theme:icons/decrement"));
+ aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove);
auto *messageToolBar = new QToolBar;
@@ -256,6 +262,7 @@ void MessagesSettingsPage::retranslateUi()
messagePopups.setText(tr("Enable desktop notifications for private messages"));
mentionPopups.setText(tr("Enable desktop notification for mentions"));
roomHistory.setText(tr("Enable room message history on join"));
+ ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages"));
hexLabel.setText(tr("(Color is hexadecimal)"));
hexHighlightLabel.setText(tr("(Color is hexadecimal)"));
customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only"));
diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h
index e98ae0592..436ebbad9 100644
--- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h
@@ -40,6 +40,7 @@ private:
QCheckBox messagePopups;
QCheckBox mentionPopups;
QCheckBox roomHistory;
+ QCheckBox ignoreAllPrivateMessagesCheckBox;
QGroupBox *chatGroupBox;
QGroupBox *highlightGroupBox;
QGroupBox *messageGroupBox;
diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp
index 1f1867f7c..1277d0e49 100644
--- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp
@@ -3,6 +3,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcut_treeview.h"
#include "../../../client/settings/shortcuts_settings.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include "../interface/widgets/utility/sequence_edit.h"
@@ -47,8 +48,8 @@ ShortcutSettingsPage::ShortcutSettingsPage()
btnResetAll = new QPushButton(this);
btnClearAll = new QPushButton(this);
- btnResetAll->setIcon(QPixmap("theme:icons/update"));
- btnClearAll->setIcon(QPixmap("theme:icons/clearsearch"));
+ btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update")));
+ btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
// layout
auto *_editLayout = new QGridLayout;
diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp
index 182e75aac..2c6e062da 100644
--- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp
@@ -20,26 +20,7 @@ enum visualDeckStoragePromptForConversionIndex
UserInterfaceSettingsPage::UserInterfaceSettingsPage()
{
- // general settings and notification settings
- notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled());
- connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
- &InterfaceSettings::setNotificationsEnabled);
- connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
- &UserInterfaceSettingsPage::setNotificationEnabled);
-
- specNotificationsEnabledCheckBox.setChecked(
- SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled());
- specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled());
- connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
- &InterfaceSettings::setSpectatorNotificationsEnabled);
-
- buddyConnectNotificationsEnabledCheckBox.setChecked(
- SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled());
- buddyConnectNotificationsEnabledCheckBox.setEnabled(
- SettingsCache::instance().userInterface().getNotificationsEnabled());
- connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED,
- &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled);
-
+ // general settings
doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay());
connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setDoubleClickToPlay);
@@ -103,6 +84,26 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
generalGroupBox = new QGroupBox;
generalGroupBox->setLayout(generalGrid);
+ // notification settings
+ notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled());
+ connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
+ &InterfaceSettings::setNotificationsEnabled);
+ connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
+ &UserInterfaceSettingsPage::setNotificationEnabled);
+
+ specNotificationsEnabledCheckBox.setChecked(
+ SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled());
+ specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled());
+ connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
+ &InterfaceSettings::setSpectatorNotificationsEnabled);
+
+ buddyConnectNotificationsEnabledCheckBox.setChecked(
+ SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled());
+ buddyConnectNotificationsEnabledCheckBox.setEnabled(
+ SettingsCache::instance().userInterface().getNotificationsEnabled());
+ connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED,
+ &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled);
+
auto *notificationsGrid = new QGridLayout;
notificationsGrid->addWidget(¬ificationsEnabledCheckBox, 0, 0);
notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0);
@@ -355,6 +356,7 @@ void UserInterfaceSettingsPage::retranslateUi()
notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar"));
specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating"));
buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect"));
+
animationGroupBox->setTitle(tr("Animation settings"));
enableAllAnimationsButton.setText(tr("&Enable all animations"));
disableAllAnimationsButton.setText(tr("&Disable all animations"));
@@ -362,6 +364,7 @@ void UserInterfaceSettingsPage::retranslateUi()
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
+
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
@@ -397,8 +400,8 @@ void UserInterfaceSettingsPage::retranslateUi()
0, CommanderBracketNames::CommanderSpellbookBracketNames);
commanderSpellbookIntegrationBracketNamingSelector.setItemText(
1, CommanderBracketNames::OfficialCommanderBracketNames);
-
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer);
+
replayGroupBox->setTitle(tr("Replay settings"));
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
rewindBufferingMsBox.setSuffix(" ms");
diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h
index 0dc4cf4e8..e8a30fb1f 100644
--- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h
@@ -23,9 +23,6 @@ private slots:
void updateCommanderSpellbookUiState();
private:
- QCheckBox notificationsEnabledCheckBox;
- QCheckBox specNotificationsEnabledCheckBox;
- QCheckBox buddyConnectNotificationsEnabledCheckBox;
QCheckBox doubleClickToPlayCheckBox;
QCheckBox clickPlaysAllSelectedCheckBox;
QCheckBox playToStackCheckBox;
@@ -37,12 +34,18 @@ private:
QCheckBox showTotalSelectionCountCheckBox;
QCheckBox useTearOffMenusCheckBox;
QCheckBox keepGameChatFocusCheckBox;
+
+ QCheckBox notificationsEnabledCheckBox;
+ QCheckBox specNotificationsEnabledCheckBox;
+ QCheckBox buddyConnectNotificationsEnabledCheckBox;
+
QPushButton enableAllAnimationsButton;
QPushButton disableAllAnimationsButton;
QCheckBox tapAnimationCheckBox;
QCheckBox arrowDrawAnimationCheckBox;
QCheckBox lifeCounterAnimationsCheckBox;
QCheckBox battlefieldFlashCheckBox;
+
QCheckBox openDeckInNewTabCheckBox;
QLabel visualDeckStoragePromptForConversionLabel;
QComboBox visualDeckStoragePromptForConversionSelector;
@@ -57,8 +60,10 @@ private:
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer;
QComboBox commanderSpellbookIntegrationBracketNamingSelector;
+
QLabel rewindBufferingMsLabel;
QSpinBox rewindBufferingMsBox;
+
QGroupBox *generalGroupBox;
QGroupBox *notificationsGroupBox;
QGroupBox *animationGroupBox;
diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
index 98b21d0f1..888197f53 100644
--- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
+++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
@@ -1,6 +1,7 @@
#include "tab_archidekt.h"
#include "../../../../../client/settings/cache_settings.h"
+#include "../../../../pixel_map_generator.h"
#include "../../../cards/additional_info/mana_symbol_widget.h"
#include "../../../utility/completer_utils.h"
#include "../../tab_supervisor.h"
@@ -213,7 +214,7 @@ void TabArchidekt::setupFilterWidgets()
// Format filter (collapsible)
formatButton = new SettingsButtonWidget(secondaryToolbar);
formatButton->setButtonText(tr("Formats"));
- formatButton->setButtonIcon(QPixmap("theme:icons/scale_balanced"));
+ formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
QWidget *formatContainer = new QWidget(secondaryToolbar);
QGridLayout *formatLayout = new QGridLayout(formatContainer);
diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
index 4f50e38a6..2cbbedfa4 100644
--- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
+++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
@@ -1,6 +1,7 @@
#include "commander_bracket_widget.h"
#include "../../../../../client/settings/cache_settings.h"
+#include "../../../../pixel_map_generator.h"
#include "commander_bracket_service.h"
#include
@@ -30,7 +31,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent
bracketInfoButton->setEnabled(false);
bracketRefreshButton = new QToolButton(this);
- bracketRefreshButton->setIcon(QPixmap("theme:icons/reload"));
+ bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload")));
bracketRefreshButton->setAutoRaise(true);
connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate);
diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp
index 410a48d40..dbcf50966 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp
+++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp
@@ -137,6 +137,11 @@ void TabAccount::retranslateUi()
buddyList->retranslateUi();
ignoreList->retranslateUi();
userInfoBox->retranslateUi();
+
+ buddyList->setToolTip(tr("Buddies are marked with a star in chat, a sound plays when they join or leave the "
+ "server, and they can be invited to buddy-only games."));
+ ignoreList->setToolTip(tr("Ignored users' chat messages are hidden from you, and they cannot send you private "
+ "messages or join your games."));
}
void TabAccount::processListUsersResponse(const Response &response)
diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp
index 49e42e4cf..f3535d850 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp
+++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp
@@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
#include "../interface/widgets/utility/get_text_with_max.h"
@@ -105,19 +106,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Left side actions
aOpenLocalDeck = new QAction(this);
- aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
+ aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck);
aRenameLocal = new QAction(this);
- aRenameLocal->setIcon(QPixmap("theme:icons/rename"));
+ aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename")));
connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal);
aUpload = new QAction(this);
- aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
+ aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload);
aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
aDeleteLocalDeck = new QAction(this);
- aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
+ aDeleteLocalDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck);
aOpenDecksFolder = new QAction(this);
@@ -126,16 +127,16 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Right side actions
aOpenRemoteDeck = new QAction(this);
- aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil"));
+ aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck);
aDownload = new QAction(this);
- aDownload->setIcon(QPixmap("theme:icons/arrow_left_green"));
+ aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green")));
connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload);
aNewFolder = new QAction(this);
aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder);
aDeleteRemoteDeck = new QAction(this);
- aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
+ aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
// Add actions to toolbars
diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp
new file mode 100644
index 000000000..39652ef7d
--- /dev/null
+++ b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp
@@ -0,0 +1,261 @@
+/**
+ * @file tab_developer.cpp
+ * @ingroup ServerTabs
+ */
+//! \todo Document this file.
+
+#include "tab_developer.h"
+
+#include
+#include
+#include
+#include
+#include
+#include