mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge branch 'master' into tooomm-qt5
This commit is contained in:
commit
9766c9dcf8
117 changed files with 5448 additions and 1793 deletions
|
|
@ -8,6 +8,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
||||||
gtest \
|
gtest \
|
||||||
mariadb-libs \
|
mariadb-libs \
|
||||||
ninja \
|
ninja \
|
||||||
|
openssl \
|
||||||
protobuf \
|
protobuf \
|
||||||
qt6-base \
|
qt6-base \
|
||||||
qt6-declarative \
|
qt6-declarative \
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
|
libssl-dev \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
|
libssl-dev \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ RUN dnf install -y \
|
||||||
git \
|
git \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
|
openssl-devel \
|
||||||
protobuf-devel \
|
protobuf-devel \
|
||||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||||
qt6-qtimageformats \
|
qt6-qtimageformats \
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ RUN dnf install -y \
|
||||||
git \
|
git \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
|
openssl-devel \
|
||||||
protobuf-devel \
|
protobuf-devel \
|
||||||
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||||
qt6-qtimageformats \
|
qt6-qtimageformats \
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ RUN apt-get update && \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
|
libssl-dev \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
|
libssl-dev \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
|
libssl-dev \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,9 @@ if [[ $MAKE_TEST ]]; then
|
||||||
fi
|
fi
|
||||||
if [[ $USE_CCACHE ]]; then
|
if [[ $USE_CCACHE ]]; then
|
||||||
flags+=("-DUSE_CCACHE=1")
|
flags+=("-DUSE_CCACHE=1")
|
||||||
|
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||||
|
# consumes a precompiled header, silently recompiling everything on every run.
|
||||||
|
ccache --set-config sloppiness=pch_defines,time_macros
|
||||||
if [[ $CCACHE_SIZE ]]; then
|
if [[ $CCACHE_SIZE ]]; then
|
||||||
# This setting persists after running the script
|
# This setting persists after running the script
|
||||||
ccache --max-size "$CCACHE_SIZE"
|
ccache --max-size "$CCACHE_SIZE"
|
||||||
|
|
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout repository"
|
- name: "Checkout repository"
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: "Initialize CodeQL"
|
- name: "Initialize CodeQL"
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v4
|
||||||
|
|
|
||||||
8
.github/workflows/desktop-build.yml
vendored
8
.github/workflows/desktop-build.yml
vendored
|
|
@ -176,8 +176,12 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
RUN --server --debug --test --ccache "$CCACHE_SIZE" \
|
args=()
|
||||||
--cmake-generator "$CMAKE_GENERATOR"
|
[[ $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"
|
- name: "Build release package"
|
||||||
id: build
|
id: build
|
||||||
|
|
|
||||||
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -127,7 +127,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Download digests"
|
- name: "Download digests"
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digest-*
|
pattern: digest-*
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,20 @@
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.25)
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
|
||||||
# Early detect ccache
|
# Use compiler cache (ccache)
|
||||||
option(USE_CCACHE "Cache the build results with ccache" ON)
|
option(USE_CCACHE "Cache the build results with ccache" ON)
|
||||||
# Treat warnings as errors (Debug builds only)
|
# Treat warnings as errors (Debug builds only)
|
||||||
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
|
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
|
||||||
# Check for translation updates
|
# Check for translation updates
|
||||||
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
|
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
|
||||||
# Compile servatrice
|
# Compile Cockatrice
|
||||||
option(WITH_SERVER "build servatrice" OFF)
|
option(WITH_CLIENT "Build Cockatrice client" ON)
|
||||||
# Compile cockatrice
|
# Compile Oracle
|
||||||
option(WITH_CLIENT "build cockatrice" ON)
|
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
||||||
# Compile oracle
|
# Compile Servatrice
|
||||||
option(WITH_ORACLE "build oracle" ON)
|
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||||
# Compile tests
|
# Compile tests
|
||||||
option(TEST "build tests" OFF)
|
option(TEST "Build tests" OFF)
|
||||||
# Use vcpkg regardless of OS
|
# Use vcpkg regardless of OS
|
||||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||||
|
|
||||||
|
|
@ -38,13 +38,24 @@ else()
|
||||||
)
|
)
|
||||||
endif()
|
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)
|
find_program(CCACHE_PROGRAM ccache)
|
||||||
if(CCACHE_PROGRAM)
|
if(CCACHE_PROGRAM)
|
||||||
# Support Unix Makefiles and Ninja
|
# Support Unix Makefiles and Ninja
|
||||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
|
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}")
|
message(STATUS "Found CCache ${CCACHE_PROGRAM}")
|
||||||
endif()
|
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()
|
endif()
|
||||||
|
|
||||||
if(WIN32 OR USE_VCPKG) # Windows (including 64bit) or USE_VCPKG set
|
if(WIN32 OR USE_VCPKG) # Windows (including 64bit) or USE_VCPKG set
|
||||||
|
|
@ -166,6 +177,9 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}")
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}")
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||||
else()
|
else()
|
||||||
# other: osx/llvm, bsd/llvm
|
# other: osx/llvm, bsd/llvm
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
||||||
|
|
@ -174,6 +188,9 @@ else()
|
||||||
else()
|
else()
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra")
|
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Reduce compiler I/O by using pipes between stages instead of temp files
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||||
|
|
@ -206,11 +223,6 @@ if(WIN32)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
if(OPENSSL_FOUND)
|
if(OPENSSL_FOUND)
|
||||||
include_directories(${OPENSSL_INCLUDE_DIRS})
|
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()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ RUN apt-get update \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
|
libssl-dev \
|
||||||
qt6-websockets-dev \
|
qt6-websockets-dev \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
|
|
@ -42,6 +43,7 @@ RUN apt-get update \
|
||||||
libprotobuf32t64 \
|
libprotobuf32t64 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libqt6websockets6 \
|
libqt6websockets6 \
|
||||||
|
libssl3 \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
||||||
24
cmake/pch/qtcore_pch.h
Normal file
24
cmake/pch/qtcore_pch.h
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/** @file qtcore_pch.h
|
||||||
|
* @brief Precompiled header for all Qt targets (Qt Core only).
|
||||||
|
*
|
||||||
|
* Safe for every target that links Qt Core, including the headless
|
||||||
|
* Servatrice binary. Keep this header free of any widget/gui types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QBasicTimer>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVariant>
|
||||||
30
cmake/pch/qtwidgets_pch.h
Normal file
30
cmake/pch/qtwidgets_pch.h
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/** @file qtwidgets_pch.h
|
||||||
|
* @brief Precompiled header for GUI targets (Cockatrice client, Oracle).
|
||||||
|
*
|
||||||
|
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
||||||
|
* Network layers that virtually every client translation unit re-parses.
|
||||||
|
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "qtcore_pch.h"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QGraphicsItem>
|
||||||
|
#include <QGraphicsScene>
|
||||||
|
#include <QGraphicsView>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QMainWindow>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QTabWidget>
|
||||||
|
#include <QToolBar>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QWidget>
|
||||||
|
|
@ -214,6 +214,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
|
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
|
||||||
src/interface/widgets/deck_editor/deck_list_style_proxy.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_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/deck_editor/printing_disabled_info_widget.cpp
|
||||||
src/interface/widgets/general/background_sources.cpp
|
src/interface/widgets/general/background_sources.cpp
|
||||||
src/interface/widgets/general/display/background_plate_widget.cpp
|
src/interface/widgets/general/display/background_plate_widget.cpp
|
||||||
|
|
@ -271,6 +272,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/server/user/user_context_menu.cpp
|
src/interface/widgets/server/user/user_context_menu.cpp
|
||||||
src/interface/widgets/server/user/user_info_box.cpp
|
src/interface/widgets/server/user/user_info_box.cpp
|
||||||
src/interface/widgets/server/user/user_info_connection.cpp
|
src/interface/widgets/server/user/user_info_connection.cpp
|
||||||
|
src/interface/widgets/server/user/user_list_dialog.cpp
|
||||||
src/interface/widgets/server/user/user_list_manager.cpp
|
src/interface/widgets/server/user/user_list_manager.cpp
|
||||||
src/interface/widgets/server/user/user_list_painter.cpp
|
src/interface/widgets/server/user/user_list_painter.cpp
|
||||||
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
||||||
|
|
@ -515,6 +517,8 @@ qt6_add_executable(
|
||||||
MANUAL_FINALIZATION
|
MANUAL_FINALIZATION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
||||||
|
|
||||||
qt6_add_shaders(
|
qt6_add_shaders(
|
||||||
cockatrice
|
cockatrice
|
||||||
"onboarding_shaders"
|
"onboarding_shaders"
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ In this list of examples below, each entry has an explanation and can be clicked
|
||||||
<dt><u>E</u>dition:</dt>
|
<dt><u>E</u>dition:</dt>
|
||||||
<dd>[set:lea](#set:lea) <small>(Cards that appear in Alpha, which has the set code LEA)</small></dd>
|
<dd>[set:lea](#set:lea) <small>(Cards that appear in Alpha, which has the set code LEA)</small></dd>
|
||||||
<dd>[e:lea OR e:leb](#e:lea OR e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
|
<dd>[e:lea OR e:leb](#e:lea OR e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
|
||||||
|
<dd>[e<8ED](#e<8ED) <small>(Cards that appear before 8th edition)</small></dd>
|
||||||
|
|
||||||
<dt>Negate:</dt>
|
<dt>Negate:</dt>
|
||||||
<dd>[c:wu -c:m](#c:wu -c:m) <small>(Any card that is white or blue, but not multicolored)</small></dd>
|
<dd>[c:wu -c:m](#c:wu -c:m) <small>(Any card that is white or blue, but not multicolored)</small></dd>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
||||||
|
|
@ -381,12 +380,10 @@ void DeckViewScene::rebuildTree()
|
||||||
addItem(container);
|
addItem(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int j = 0; j < currentZone->size(); j++) {
|
// Cards in custom zones nested under a board are regular board cards in-game.
|
||||||
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
// They are collected recursively (like every other consumer) and reported with
|
||||||
if (!currentCard) {
|
// the top-level board zone as their origin, so that sideboard plans keep working.
|
||||||
continue;
|
for (auto *currentCard : deck->getCardNodes({currentZone->getName()})) {
|
||||||
}
|
|
||||||
|
|
||||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||||
auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName());
|
auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName());
|
||||||
container->addCard(newCard);
|
container->addCard(newCard);
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,16 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
||||||
|
|
||||||
GameScene::~GameScene()
|
GameScene::~GameScene()
|
||||||
{
|
{
|
||||||
// Sever all incoming connections (animated item destroy-tracking) before the
|
// Sever all destroyed->removeAnimatedItem connections before the members below
|
||||||
// members below are destroyed: the base QGraphicsScene destructor destroys the
|
// are destroyed: the base QGraphicsScene destructor destroys the remaining items,
|
||||||
// remaining items, and their destroyed() signals must not reach slots that
|
// and their destroyed() signals must not reach slots that reference members that
|
||||||
// reference members that no longer exist.
|
// no longer exist. The connection handle overload is used because the string-based
|
||||||
QObject::disconnect(nullptr, nullptr, this, nullptr);
|
// 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;
|
delete animationTimer;
|
||||||
animationTimer = nullptr;
|
animationTimer = nullptr;
|
||||||
|
|
@ -777,8 +782,15 @@ void GameScene::registerAnimationItem(IAnimatedItem *item)
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!animatedItems.contains(object)) {
|
// Guard against duplicate connections using the connection map, not
|
||||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
|
// 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);
|
animatedItems.insert(object, item);
|
||||||
if (animationTimer && !animationTimer->isActive()) {
|
if (animationTimer && !animationTimer->isActive()) {
|
||||||
|
|
@ -797,6 +809,7 @@ void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
||||||
void GameScene::removeAnimatedItem(QObject *item)
|
void GameScene::removeAnimatedItem(QObject *item)
|
||||||
{
|
{
|
||||||
animatedItems.remove(item);
|
animatedItems.remove(item);
|
||||||
|
animationItemConnections.remove(item);
|
||||||
if (animationTimer && animatedItems.isEmpty()) {
|
if (animationTimer && animatedItems.isEmpty()) {
|
||||||
animationTimer->stop();
|
animationTimer->stop();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,11 @@ private:
|
||||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||||
int playerRotation; ///< Rotation offset for player layout
|
QHash<QObject *, QMetaObject::Connection>
|
||||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item
|
||||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
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.
|
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@ TallyMenu::TallyMenu()
|
||||||
aTallyNone = createTallyAction(TallyType::None);
|
aTallyNone = createTallyAction(TallyType::None);
|
||||||
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
||||||
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
||||||
|
aTallyTotalToughness = createTallyAction(TallyType::TotalToughness);
|
||||||
|
|
||||||
addAction(aTallyNone);
|
addAction(aTallyNone);
|
||||||
addSeparator();
|
addSeparator();
|
||||||
addAction(aTallySubtypes);
|
addAction(aTallySubtypes);
|
||||||
addAction(aTallyTotalPower);
|
addAction(aTallyTotalPower);
|
||||||
|
addAction(aTallyTotalToughness);
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
@ -54,4 +56,5 @@ void TallyMenu::retranslateUi()
|
||||||
aTallyNone->setText(tr("None"));
|
aTallyNone->setText(tr("None"));
|
||||||
aTallySubtypes->setText(tr("Subtypes"));
|
aTallySubtypes->setText(tr("Subtypes"));
|
||||||
aTallyTotalPower->setText(tr("Total Power"));
|
aTallyTotalPower->setText(tr("Total Power"));
|
||||||
|
aTallyTotalToughness->setText(tr("Total Toughness"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ private:
|
||||||
QAction *aTallyNone = nullptr;
|
QAction *aTallyNone = nullptr;
|
||||||
QAction *aTallySubtypes = nullptr;
|
QAction *aTallySubtypes = nullptr;
|
||||||
QAction *aTallyTotalPower = nullptr;
|
QAction *aTallyTotalPower = nullptr;
|
||||||
|
QAction *aTallyTotalToughness = nullptr;
|
||||||
|
|
||||||
QAction *createTallyAction(TallyType tallyType);
|
QAction *createTallyAction(TallyType tallyType);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,31 @@ QList<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &cards)
|
||||||
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
||||||
return {TallyRow{name, QString::number(total)}};
|
return {TallyRow{name, QString::number(total)}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int sumToughness(const QList<CardItem *> &cards)
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
for (auto card : cards) {
|
||||||
|
QVariantList parsed = CardItem::parsePT(card->getPT());
|
||||||
|
if (parsed.size() == 2) {
|
||||||
|
int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int
|
||||||
|
total += qMax(toughness, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<TallyRow> StatsTally::computeTotalToughness(const QList<CardItem *> &cards)
|
||||||
|
{
|
||||||
|
// don't bother if none of the cards have pt
|
||||||
|
bool hasPT =
|
||||||
|
std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); });
|
||||||
|
if (!hasPT) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = sumToughness(cards);
|
||||||
|
|
||||||
|
QString name = QCoreApplication::translate("StatsTally", "Total Toughness");
|
||||||
|
return {TallyRow{name, QString::number(total)}};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ namespace StatsTally
|
||||||
*/
|
*/
|
||||||
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sums the toughness of all selected cards
|
||||||
|
*
|
||||||
|
* @param cards The list of selected card items to analyze.
|
||||||
|
* @return A single row containing the total, or an empty list if none of the cards have pt
|
||||||
|
*/
|
||||||
|
QList<TallyRow> computeTotalToughness(const QList<CardItem *> &cards);
|
||||||
|
|
||||||
} // namespace StatsTally
|
} // namespace StatsTally
|
||||||
|
|
||||||
#endif // COCKATRICE_STATS_TALLY_H
|
#endif // COCKATRICE_STATS_TALLY_H
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType t
|
||||||
return SubtypeTally::countSubtypes(cards);
|
return SubtypeTally::countSubtypes(cards);
|
||||||
case TallyType::TotalPower:
|
case TallyType::TotalPower:
|
||||||
return StatsTally::computeTotalPower(cards);
|
return StatsTally::computeTotalPower(cards);
|
||||||
|
case TallyType::TotalToughness:
|
||||||
|
return StatsTally::computeTotalToughness(cards);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ enum class TallyType
|
||||||
None,
|
None,
|
||||||
Subtypes,
|
Subtypes,
|
||||||
TotalPower,
|
TotalPower,
|
||||||
MaxValue = TotalPower // sentinel value
|
TotalToughness,
|
||||||
|
MaxValue = TotalToughness // sentinel value
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace Tally
|
namespace Tally
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
x = calcDropIndexFromY(dropPoint.y());
|
bool sameZone = startZone == getLogic();
|
||||||
|
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
Command_MoveCard cmd;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons
|
||||||
return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
|
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();
|
const auto &cards = getLogic()->getCards();
|
||||||
if (cards.isEmpty()) {
|
if (cards.isEmpty()) {
|
||||||
|
|
@ -94,7 +94,8 @@ int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
||||||
if (effectiveOffset <= 0.0) {
|
if (effectiveOffset <= 0.0) {
|
||||||
return 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()
|
void SelectZone::restoreStaleEscapedCards()
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,12 @@ protected:
|
||||||
/**
|
/**
|
||||||
* @brief Computes the card index at a given y-coordinate within the zone's vertical layout.
|
* @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.
|
* 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.
|
* @brief Positions cards vertically with alternating left/right x-offsets.
|
||||||
|
|
|
||||||
|
|
@ -57,18 +57,14 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &cards = getLogic()->getCards();
|
bool sameZone = startZone == getLogic();
|
||||||
int index;
|
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
|
||||||
if (startZone == getLogic()) {
|
if (sameZone) {
|
||||||
// Reordering within the zone: use drop position
|
|
||||||
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
|
||||||
// Same-zone no-op: don't move a card onto itself
|
// 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()) {
|
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Coming from another zone: append at end (top of stack, rendered on top)
|
|
||||||
index = static_cast<int>(cards.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
Command_MoveCard cmd;
|
||||||
|
|
|
||||||
|
|
@ -375,15 +375,32 @@ void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckL
|
||||||
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
||||||
const InnerDecklistNode *zoneNode,
|
const InnerDecklistNode *zoneNode,
|
||||||
bool addComments,
|
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
|
// group cards by card type and count the subtotals
|
||||||
QMultiMap<QString, DecklistCardNode *> cardsByType;
|
QMultiMap<QString, DecklistCardNode *> cardsByType;
|
||||||
QMap<QString, int> cardTotalByType;
|
QMap<QString, int> cardTotalByType;
|
||||||
int cardTotal = 0;
|
int cardTotal = 0;
|
||||||
|
QList<const InnerDecklistNode *> subZones;
|
||||||
|
|
||||||
for (int j = 0; j < zoneNode->size(); j++) {
|
for (int j = 0; j < zoneNode->size(); j++) {
|
||||||
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
|
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
|
||||||
|
if (!card) {
|
||||||
|
// Cards collected in nested sub-zones are exported by recursion so
|
||||||
|
// they don't end up invisible in the plain text output. They are
|
||||||
|
// deferred until after this zone's own header and cards so they read
|
||||||
|
// as part of this zone's block.
|
||||||
|
if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zoneNode->at(j))) {
|
||||||
|
subZones.append(subZone);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||||
QString cardType = info ? info->getMainCardType() : "unknown";
|
QString cardType = info ? info->getMainCardType() : "unknown";
|
||||||
|
|
@ -411,25 +428,30 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
||||||
|
|
||||||
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
|
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
|
||||||
|
|
||||||
saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber);
|
saveToStream_DeckZoneCards(out, cards, addComments, addSetNameAndNumber, owningBoardZoneName);
|
||||||
|
|
||||||
if (addComments) {
|
if (addComments) {
|
||||||
out << "\n";
|
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,
|
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
|
||||||
const InnerDecklistNode *zoneNode,
|
|
||||||
QList<DecklistCardNode *> cards,
|
QList<DecklistCardNode *> cards,
|
||||||
bool addComments,
|
bool addComments,
|
||||||
bool addSetNameAndNumber)
|
bool addSetNameAndNumber,
|
||||||
|
const QString &boardZoneName)
|
||||||
{
|
{
|
||||||
// QMultiMap sorts values in reverse order
|
// QMultiMap sorts values in reverse order
|
||||||
for (int i = cards.size() - 1; i >= 0; --i) {
|
for (int i = cards.size() - 1; i >= 0; --i) {
|
||||||
DecklistCardNode *card = cards[i];
|
DecklistCardNode *card = cards[i];
|
||||||
|
|
||||||
if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) {
|
if (boardZoneName == DECK_ZONE_SIDE && addComments) {
|
||||||
out << "SB: ";
|
out << "SB: ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -510,9 +532,26 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
|
||||||
|
|
||||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
||||||
{
|
{
|
||||||
|
if (!node || node->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const int totalColumns = 2;
|
const int totalColumns = 2;
|
||||||
|
|
||||||
if (node->height() == 1) {
|
// Dispatch children by type instead of trusting a whole-node height: a deck
|
||||||
|
// node may hold direct cards and nested zones side by side (custom zones),
|
||||||
|
// and an empty node would previously crash on at(0).
|
||||||
|
QVector<const AbstractDecklistCardNode *> cards;
|
||||||
|
QVector<const InnerDecklistNode *> subZones;
|
||||||
|
for (int i = 0; i < node->size(); i++) {
|
||||||
|
if (auto *card = dynamic_cast<const AbstractDecklistCardNode *>(node->at(i))) {
|
||||||
|
cards.append(card);
|
||||||
|
} else if (auto *zone = dynamic_cast<const InnerDecklistNode *>(node->at(i))) {
|
||||||
|
subZones.append(zone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cards.isEmpty()) {
|
||||||
QTextBlockFormat blockFormat;
|
QTextBlockFormat blockFormat;
|
||||||
QTextCharFormat charFormat;
|
QTextCharFormat charFormat;
|
||||||
charFormat.setFontPointSize(11);
|
charFormat.setFontPointSize(11);
|
||||||
|
|
@ -523,9 +562,9 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
||||||
tableFormat.setCellPadding(0);
|
tableFormat.setCellPadding(0);
|
||||||
tableFormat.setCellSpacing(0);
|
tableFormat.setCellSpacing(0);
|
||||||
tableFormat.setBorder(0);
|
tableFormat.setBorder(0);
|
||||||
QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat);
|
QTextTable *table = cursor->insertTable(cards.size() + 1, totalColumns, tableFormat);
|
||||||
for (int i = 0; i < node->size(); i++) {
|
for (int i = 0; i < cards.size(); i++) {
|
||||||
auto *card = dynamic_cast<AbstractDecklistCardNode *>(node->at(i));
|
const AbstractDecklistCardNode *card = cards[i];
|
||||||
|
|
||||||
QTextCharFormat cellCharFormat;
|
QTextCharFormat cellCharFormat;
|
||||||
cellCharFormat.setFontPointSize(9);
|
cellCharFormat.setFontPointSize(9);
|
||||||
|
|
@ -540,7 +579,13 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
||||||
cellCursor = cell.firstCursorPosition();
|
cellCursor = cell.firstCursorPosition();
|
||||||
cellCursor.insertText(card->getName());
|
cellCursor.insertText(card->getName());
|
||||||
}
|
}
|
||||||
} else if (node->height() == 2) {
|
}
|
||||||
|
|
||||||
|
for (const InnerDecklistNode *subZone : subZones) {
|
||||||
|
if (subZone->isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
QTextBlockFormat blockFormat;
|
QTextBlockFormat blockFormat;
|
||||||
QTextCharFormat charFormat;
|
QTextCharFormat charFormat;
|
||||||
charFormat.setFontPointSize(14);
|
charFormat.setFontPointSize(14);
|
||||||
|
|
@ -559,10 +604,8 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
||||||
tableFormat.setColumnWidthConstraints(constraints);
|
tableFormat.setColumnWidthConstraints(constraints);
|
||||||
|
|
||||||
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
|
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
|
||||||
for (int i = 0; i < node->size(); i++) {
|
QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition();
|
||||||
QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition();
|
printDeckListNode(&cellCursor, subZone);
|
||||||
printDeckListNode(&cellCursor, dynamic_cast<InnerDecklistNode *>(node->at(i)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor->movePosition(QTextCursor::End);
|
cursor->movePosition(QTextCursor::End);
|
||||||
|
|
|
||||||
|
|
@ -159,12 +159,13 @@ private:
|
||||||
static void saveToStream_DeckZone(QTextStream &out,
|
static void saveToStream_DeckZone(QTextStream &out,
|
||||||
const InnerDecklistNode *zoneNode,
|
const InnerDecklistNode *zoneNode,
|
||||||
bool addComments = true,
|
bool addComments = true,
|
||||||
bool addSetNameAndNumber = true);
|
bool addSetNameAndNumber = true,
|
||||||
|
const QString &boardZoneName = QString());
|
||||||
static void saveToStream_DeckZoneCards(QTextStream &out,
|
static void saveToStream_DeckZoneCards(QTextStream &out,
|
||||||
const InnerDecklistNode *zoneNode,
|
|
||||||
QList<DecklistCardNode *> cards,
|
QList<DecklistCardNode *> cards,
|
||||||
bool addComments = true,
|
bool addComments = true,
|
||||||
bool addSetNameAndNumber = true);
|
bool addSetNameAndNumber = true,
|
||||||
|
const QString &boardZoneName = QString());
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -174,16 +174,18 @@ void CardGroupDisplayWidget::updateCardDisplays()
|
||||||
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
|
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
|
||||||
|
|
||||||
// 4. persist the source index
|
// 4. persist the source index
|
||||||
QPersistentModelIndex persistent(sourceIndex);
|
addCardWidgets(QPersistentModelIndex(sourceIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the card amount
|
void CardGroupDisplayWidget::addCardWidgets(const QPersistentModelIndex &persistent)
|
||||||
int cardAmount =
|
{
|
||||||
sourceIndex.sibling(sourceIndex.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
|
// Get the card amount
|
||||||
|
int cardAmount = persistent.sibling(persistent.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
|
||||||
|
|
||||||
// Create multiple widgets for the card count
|
// Create multiple widgets for the card count
|
||||||
for (int copy = 0; copy < cardAmount; ++copy) {
|
for (int copy = 0; copy < cardAmount; ++copy) {
|
||||||
addToLayout(constructWidgetForIndex(persistent));
|
addToLayout(constructWidgetForIndex(persistent));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public:
|
||||||
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
|
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
|
||||||
void refreshSelectionForIndex(const QPersistentModelIndex &persistent);
|
void refreshSelectionForIndex(const QPersistentModelIndex &persistent);
|
||||||
void clearAllDisplayWidgets();
|
void clearAllDisplayWidgets();
|
||||||
|
void addCardWidgets(const QPersistentModelIndex &persistent);
|
||||||
|
|
||||||
DeckListModel *deckListModel;
|
DeckListModel *deckListModel;
|
||||||
QItemSelectionModel *selectionModel;
|
QItemSelectionModel *selectionModel;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "libcockatrice/card/database/card_database_manager.h"
|
#include "libcockatrice/card/database/card_database_manager.h"
|
||||||
|
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
|
#include <algorithm>
|
||||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||||
|
|
||||||
DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
||||||
|
|
@ -51,11 +52,6 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
||||||
// User Interaction
|
// User Interaction
|
||||||
// =====================================================================================================================
|
// =====================================================================================================================
|
||||||
|
|
||||||
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
|
|
||||||
{
|
|
||||||
emit cardClicked(event, card, zoneName);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DeckCardZoneDisplayWidget::onHover(const ExactCard &card)
|
void DeckCardZoneDisplayWidget::onHover(const ExactCard &card)
|
||||||
{
|
{
|
||||||
emit cardHovered(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();
|
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) {
|
if (displayType == DisplayType::Overlap) {
|
||||||
auto *displayWidget = new OverlappedCardGroupDisplayWidget(
|
auto *displayWidget = new OverlappedCardGroupDisplayWidget(
|
||||||
cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria,
|
cardGroupContainer, deckListModel, selectionModel, index, effectiveZoneName, categoryName,
|
||||||
activeSortCriteria, subBannerOpacity, cardSizeWidget);
|
activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget);
|
||||||
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this,
|
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this, routeCardClick);
|
||||||
&DeckCardZoneDisplayWidget::onClick);
|
|
||||||
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardHovered, this,
|
connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardHovered, this,
|
||||||
&DeckCardZoneDisplayWidget::onHover);
|
&DeckCardZoneDisplayWidget::onHover);
|
||||||
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
|
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
|
||||||
|
|
@ -111,9 +113,9 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
|
||||||
indexToWidgetMap.insert(index, displayWidget);
|
indexToWidgetMap.insert(index, displayWidget);
|
||||||
} else if (displayType == DisplayType::Flat) {
|
} else if (displayType == DisplayType::Flat) {
|
||||||
auto *displayWidget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, selectionModel, index,
|
auto *displayWidget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, selectionModel, index,
|
||||||
zoneName, categoryName, activeGroupCriteria,
|
effectiveZoneName, categoryName, activeGroupCriteria,
|
||||||
activeSortCriteria, subBannerOpacity, cardSizeWidget);
|
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, &FlatCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover);
|
||||||
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
|
connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this,
|
||||||
&DeckCardZoneDisplayWidget::cleanupInvalidCardGroup);
|
&DeckCardZoneDisplayWidget::cleanupInvalidCardGroup);
|
||||||
|
|
@ -126,24 +128,18 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex
|
||||||
|
|
||||||
void DeckCardZoneDisplayWidget::displayCards()
|
void DeckCardZoneDisplayWidget::displayCards()
|
||||||
{
|
{
|
||||||
QSortFilterProxyModel proxy;
|
if (!trackedIndex.isValid()) {
|
||||||
proxy.setSourceModel(deckListModel);
|
return;
|
||||||
proxy.setSortRole(Qt::EditRole);
|
}
|
||||||
proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
|
|
||||||
|
|
||||||
// 1. trackedIndex is a source index → map it to proxy space
|
// Iterate the direct children of the tracked zone, keeping the tree view's row
|
||||||
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
|
// order (criteria groups first, then custom zones, both in the model's sort order).
|
||||||
|
QList<QPersistentModelIndex> rows;
|
||||||
// 2. iterate children under the proxy parent
|
for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) {
|
||||||
for (int i = 0; i < proxy.rowCount(proxyParent); ++i) {
|
rows.append(QPersistentModelIndex(deckListModel->index(i, 0, trackedIndex)));
|
||||||
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);
|
|
||||||
|
|
||||||
|
for (const QPersistentModelIndex &persistent : rows) {
|
||||||
constructAppropriateWidget(persistent);
|
constructAppropriateWidget(persistent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ public:
|
||||||
void addCardsToOverlapWidget();
|
void addCardsToOverlapWidget();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onClick(QMouseEvent *event, const ExactCard &card);
|
|
||||||
void onHover(const ExactCard &card);
|
void onHover(const ExactCard &card);
|
||||||
void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget);
|
void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget);
|
||||||
void constructAppropriateWidget(QPersistentModelIndex index);
|
void constructAppropriateWidget(QPersistentModelIndex index);
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,13 @@ void CardDatabaseView::decrementCard(const QString &zoneName)
|
||||||
emit cardDecremented(currentCardName(), zoneName);
|
emit cardDecremented(currentCardName(), zoneName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardDatabaseView::setZoneMenuProvider(const std::function<QList<QPair<QString, QStringList>>()> &provider,
|
||||||
|
const std::function<QString()> &newZoneHandler)
|
||||||
|
{
|
||||||
|
zoneMenuProvider = provider;
|
||||||
|
this->newZoneHandler = newZoneHandler;
|
||||||
|
}
|
||||||
|
|
||||||
void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/)
|
void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/)
|
||||||
{
|
{
|
||||||
if (!current.isValid()) {
|
if (!current.isValid()) {
|
||||||
|
|
@ -142,6 +149,50 @@ void CardDatabaseView::openCustomMenu(QPoint point)
|
||||||
[this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); });
|
[this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); });
|
||||||
connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked);
|
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)) {
|
if (canBeCommander(*card)) {
|
||||||
QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)"));
|
QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)"));
|
||||||
connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); });
|
connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); });
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "../../key_signals.h"
|
#include "../../key_signals.h"
|
||||||
|
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
|
#include <functional>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
|
|
||||||
class CardDatabaseModel;
|
class CardDatabaseModel;
|
||||||
|
|
@ -19,6 +20,13 @@ class CardDatabaseView : public QTreeView
|
||||||
KeySignals searchKeySignals;
|
KeySignals searchKeySignals;
|
||||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||||
|
|
||||||
|
/// Provides the custom zones available in the current deck, grouped by board zone.
|
||||||
|
/// The list contains (board zone name, custom zone names) pairs for every board.
|
||||||
|
std::function<QList<QPair<QString, QStringList>>()> zoneMenuProvider;
|
||||||
|
/// Handler invoked when the user picks "New zone..." from the add-to-zone menu.
|
||||||
|
/// Returns the name of the created zone, or an empty string if creation was cancelled.
|
||||||
|
std::function<QString()> newZoneHandler;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model);
|
explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model);
|
||||||
|
|
||||||
|
|
@ -33,6 +41,17 @@ public:
|
||||||
return &searchKeySignals;
|
return &searchKeySignals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the provider used to populate the "Add to zone" submenu of the context menu.
|
||||||
|
* If no provider is set, the submenu is not shown.
|
||||||
|
*
|
||||||
|
* @param provider Returns the custom zones of the current deck, grouped by board zone
|
||||||
|
* @param newZoneHandler Creates a new custom zone and returns its name, or an empty string
|
||||||
|
* if creation was cancelled. The menu entry is hidden when not provided.
|
||||||
|
*/
|
||||||
|
void setZoneMenuProvider(const std::function<QList<QPair<QString, QStringList>>()> &provider,
|
||||||
|
const std::function<QString()> &newZoneHandler);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void cardChanged(const QString &cardName);
|
void cardChanged(const QString &cardName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
#include "deck_editor_card_database_dock_widget.h"
|
#include "deck_editor_card_database_dock_widget.h"
|
||||||
|
|
||||||
|
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
|
||||||
|
#include "card_database_view.h"
|
||||||
|
#include "deck_state_manager.h"
|
||||||
|
#include "deck_zone_dialog.h"
|
||||||
|
|
||||||
|
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||||
|
|
||||||
DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent)
|
DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent)
|
||||||
{
|
{
|
||||||
setObjectName("databaseDisplayDock");
|
setObjectName("databaseDisplayDock");
|
||||||
|
|
@ -15,6 +22,27 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck
|
||||||
{
|
{
|
||||||
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel);
|
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel);
|
||||||
|
|
||||||
|
databaseDisplayWidget->getDatabaseView()->setZoneMenuProvider(
|
||||||
|
[deckEditor]() -> QList<QPair<QString, QStringList>> {
|
||||||
|
QList<QPair<QString, QStringList>> result;
|
||||||
|
auto *deckListModel = deckEditor->deckStateManager->getModel();
|
||||||
|
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||||
|
result.append({boardName, deckListModel->getCustomZoneNames(boardName)});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[this, deckEditor]() -> QString {
|
||||||
|
QString boardName;
|
||||||
|
const QString zoneName =
|
||||||
|
DeckZoneDialog::promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) {
|
||||||
|
return deckEditor->deckStateManager->validateNewZoneName(candidate);
|
||||||
|
});
|
||||||
|
if (!zoneName.isEmpty()) {
|
||||||
|
deckEditor->deckStateManager->createCustomZone(boardName, zoneName);
|
||||||
|
}
|
||||||
|
return zoneName;
|
||||||
|
});
|
||||||
|
|
||||||
auto *frame = new QVBoxLayout;
|
auto *frame = new QVBoxLayout;
|
||||||
frame->setObjectName("databaseDisplayFrame");
|
frame->setObjectName("databaseDisplayFrame");
|
||||||
frame->addWidget(databaseDisplayWidget);
|
frame->addWidget(databaseDisplayWidget);
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,18 @@
|
||||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||||
#include "deck_list_style_proxy.h"
|
#include "deck_list_style_proxy.h"
|
||||||
#include "deck_state_manager.h"
|
#include "deck_state_manager.h"
|
||||||
|
#include "deck_zone_dialog.h"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDockWidget>
|
#include <QDockWidget>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/utility/macros.h>
|
#include <libcockatrice/utility/macros.h>
|
||||||
|
|
@ -772,14 +775,213 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i
|
||||||
|
|
||||||
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)
|
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)
|
||||||
{
|
{
|
||||||
|
const QModelIndex sourceIndex = proxy->mapToSource(deckView->indexAt(point));
|
||||||
|
|
||||||
QMenu menu;
|
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"));
|
QAction *selectPrinting = menu.addAction(tr("Select Printing"));
|
||||||
connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector);
|
connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector);
|
||||||
|
|
||||||
menu.exec(deckView->mapToGlobal(point));
|
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()
|
void DeckEditorDeckDockWidget::refreshShortcuts()
|
||||||
{
|
{
|
||||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDockWidget>
|
#include <QDockWidget>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMenu>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
|
|
@ -102,6 +103,11 @@ private:
|
||||||
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
|
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
|
||||||
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
|
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:
|
private slots:
|
||||||
void decklistCustomMenu(QPoint point);
|
void decklistCustomMenu(QPoint point);
|
||||||
void updateCard(QModelIndex, const QModelIndex ¤t);
|
void updateCard(QModelIndex, const QModelIndex ¤t);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
|
|
||||||
DeckStateManager::DeckStateManager(QObject *parent)
|
DeckStateManager::DeckStateManager(QObject *parent)
|
||||||
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
||||||
|
|
@ -307,6 +308,170 @@ bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx)
|
||||||
return offsetCountAtIndex(idx, -1);
|
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)
|
bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset)
|
||||||
{
|
{
|
||||||
if (!idx.isValid()) {
|
if (!idx.isValid()) {
|
||||||
|
|
@ -367,6 +532,25 @@ void DeckStateManager::requestHistorySave(const QString &reason)
|
||||||
historyManager->save(deckList->createMemento(reason));
|
historyManager->save(deckList->createMemento(reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DeckStateManager::modifyTree(const QString &reason, const std::function<bool(DecklistNodeTree *)> &operation)
|
||||||
|
{
|
||||||
|
DeckListMemento memento = deckList->createMemento(reason);
|
||||||
|
bool success = operation(deckList->getTree());
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
historyManager->save(memento);
|
||||||
|
deckListModel->rebuildTree();
|
||||||
|
deckList->refreshDeckHash();
|
||||||
|
emit deckListModel->deckHashChanged();
|
||||||
|
// removeCustomZone can drop whole card sets the model never notified
|
||||||
|
// about (rebuildTree emits no cardNodesChanged), so tell the consumers.
|
||||||
|
emit deckListModel->cardNodesChanged();
|
||||||
|
doCardModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Handles updating state and emitting signals whenever the cards are modified
|
* @brief Handles updating state and emitting signals whenever the cards are modified
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "deck_list_model.h"
|
#include "deck_list_model.h"
|
||||||
|
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
#include <functional>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
|
||||||
class DeckListHistoryManager;
|
class DeckListHistoryManager;
|
||||||
|
|
@ -236,6 +237,68 @@ public:
|
||||||
*/
|
*/
|
||||||
bool decrementCountAtIndex(const QModelIndex &idx);
|
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.
|
* 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.
|
* @param steps Number of steps to undo.
|
||||||
|
|
@ -257,6 +320,7 @@ public slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool offsetCountAtIndex(const QModelIndex &idx, int offset);
|
bool offsetCountAtIndex(const QModelIndex &idx, int offset);
|
||||||
|
bool modifyTree(const QString &reason, const std::function<bool(DecklistNodeTree *)> &operation);
|
||||||
void doCardModified();
|
void doCardModified();
|
||||||
void doMetadataModified();
|
void doMetadataModified();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
#include "deck_zone_dialog.h"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
|
DeckZoneDialog::DeckZoneDialog(QWidget *parent,
|
||||||
|
const QString &initialBoardName,
|
||||||
|
const std::function<QString(const QString &)> &_nameValidator,
|
||||||
|
bool _allowBoardSelection)
|
||||||
|
: QDialog(parent), nameValidator(_nameValidator), allowBoardSelection(_allowBoardSelection)
|
||||||
|
{
|
||||||
|
nameLabel = new QLabel(this);
|
||||||
|
nameEdit = new QLineEdit(this);
|
||||||
|
nameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
|
||||||
|
errorLabel = new QLabel(this);
|
||||||
|
errorLabel->hide();
|
||||||
|
|
||||||
|
boardLabel = new QLabel(this);
|
||||||
|
boardCombo = new QComboBox(this);
|
||||||
|
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||||
|
// Use the icon overload explicitly so `boardName` lands in the user data role
|
||||||
|
// (visible text is applied below in retranslateUi). The two-argument form
|
||||||
|
// addItem({}, boardName) would be ambiguous and resolve to the icon overload
|
||||||
|
// with empty user data, yielding empty entries and an empty getBoardName().
|
||||||
|
boardCombo->addItem({}, {}, boardName);
|
||||||
|
}
|
||||||
|
if (!initialBoardName.isEmpty()) {
|
||||||
|
int idx = boardCombo->findData(initialBoardName);
|
||||||
|
if (idx != -1) {
|
||||||
|
boardCombo->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
layout->addWidget(nameLabel);
|
||||||
|
layout->addWidget(nameEdit);
|
||||||
|
layout->addWidget(errorLabel);
|
||||||
|
if (allowBoardSelection) {
|
||||||
|
layout->addWidget(boardLabel);
|
||||||
|
layout->addWidget(boardCombo);
|
||||||
|
} else {
|
||||||
|
boardLabel->hide();
|
||||||
|
boardCombo->hide();
|
||||||
|
}
|
||||||
|
layout->addWidget(buttonBox);
|
||||||
|
|
||||||
|
retranslateUi();
|
||||||
|
|
||||||
|
connect(nameEdit, &QLineEdit::textChanged, this, [this] { validateName(); });
|
||||||
|
validateName();
|
||||||
|
|
||||||
|
nameEdit->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DeckZoneDialog::getZoneName() const
|
||||||
|
{
|
||||||
|
return nameEdit->text().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DeckZoneDialog::getBoardName() const
|
||||||
|
{
|
||||||
|
return boardCombo->currentData().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckZoneDialog::setZoneName(const QString &zoneName)
|
||||||
|
{
|
||||||
|
nameEdit->setText(zoneName);
|
||||||
|
nameEdit->selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckZoneDialog::changeEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
QDialog::changeEvent(event);
|
||||||
|
|
||||||
|
if (event->type() == QEvent::LanguageChange) {
|
||||||
|
retranslateUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckZoneDialog::retranslateUi()
|
||||||
|
{
|
||||||
|
setWindowTitle(allowBoardSelection ? tr("New zone") : tr("Rename zone"));
|
||||||
|
|
||||||
|
nameLabel->setText(tr("Zone &name:"));
|
||||||
|
nameLabel->setBuddy(nameEdit);
|
||||||
|
|
||||||
|
boardLabel->setText(tr("&Parent zone:"));
|
||||||
|
boardLabel->setBuddy(boardCombo);
|
||||||
|
|
||||||
|
for (int i = 0; i < boardCombo->count(); i++) {
|
||||||
|
boardCombo->setItemText(i, InnerDecklistNode::visibleNameFromName(boardCombo->itemData(i).toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckZoneDialog::validateName()
|
||||||
|
{
|
||||||
|
const QString zoneName = nameEdit->text().trimmed();
|
||||||
|
QString error;
|
||||||
|
if (zoneName.isEmpty()) {
|
||||||
|
error = tr("Enter a zone name.");
|
||||||
|
} else if (nameValidator) {
|
||||||
|
error = nameValidator(zoneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorLabel->setText(error);
|
||||||
|
errorLabel->setVisible(!error.isEmpty());
|
||||||
|
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DeckZoneDialog::promptForNewZone(QWidget *parent,
|
||||||
|
const QString &initialBoardName,
|
||||||
|
QString *chosenBoardName,
|
||||||
|
const std::function<QString(const QString &)> &nameValidator)
|
||||||
|
{
|
||||||
|
DeckZoneDialog dialog(parent, initialBoardName, nameValidator);
|
||||||
|
if (dialog.exec() != QDialog::Accepted) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenBoardName) {
|
||||||
|
*chosenBoardName = dialog.getBoardName();
|
||||||
|
}
|
||||||
|
return dialog.getZoneName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DeckZoneDialog::promptForRename(QWidget *parent,
|
||||||
|
const QString ¤tZoneName,
|
||||||
|
const std::function<QString(const QString &)> &nameValidator)
|
||||||
|
{
|
||||||
|
DeckZoneDialog dialog(parent, {}, nameValidator, false);
|
||||||
|
dialog.setZoneName(currentZoneName);
|
||||||
|
return dialog.exec() == QDialog::Accepted ? dialog.getZoneName() : QString();
|
||||||
|
}
|
||||||
123
cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
Normal file
123
cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* @file deck_zone_dialog.h
|
||||||
|
* @ingroup DeckEditorWidgets
|
||||||
|
* @brief Shared dialog for creating custom deck zones.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DECK_ZONE_DIALOG_H
|
||||||
|
#define DECK_ZONE_DIALOG_H
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QString>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class QComboBox;
|
||||||
|
class QDialogButtonBox;
|
||||||
|
class QLabel;
|
||||||
|
class QLineEdit;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Modal dialog asking for the name and parent zone of a new custom deck zone.
|
||||||
|
*
|
||||||
|
* Menus construct the dialog transiently around exec(), so validation state only
|
||||||
|
* ever reflects the name currently typed.
|
||||||
|
*/
|
||||||
|
class DeckZoneDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Constructs the dialog and runs the initial validation pass.
|
||||||
|
*
|
||||||
|
* @param parent The parent widget for the dialog
|
||||||
|
* @param initialBoardName The board zone to preselect in the combo. Unknown names
|
||||||
|
* fall back to main.
|
||||||
|
* @param _nameValidator Given the trimmed candidate name, returns an empty string
|
||||||
|
* when it is usable, otherwise a user-facing error message. May be empty.
|
||||||
|
* @param _allowBoardSelection When false the parent-zone combo is hidden and the
|
||||||
|
* dialog acts as a rename prompt for an existing zone.
|
||||||
|
*/
|
||||||
|
explicit DeckZoneDialog(QWidget *parent = nullptr,
|
||||||
|
const QString &initialBoardName = {},
|
||||||
|
const std::function<QString(const QString &)> &_nameValidator = {},
|
||||||
|
bool _allowBoardSelection = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The trimmed zone name entered by the user.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString getZoneName() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The internal name of the board zone selected in the combo.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString getBoardName() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prefills the name field, e.g. with the current name when renaming.
|
||||||
|
*
|
||||||
|
* @param zoneName The text to put into the name field, selected for quick editing
|
||||||
|
*/
|
||||||
|
void setZoneName(const QString &zoneName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prompts the user for a new custom zone name and the board zone to nest it under.
|
||||||
|
*
|
||||||
|
* Convenience wrapper that runs DeckZoneDialog modally.
|
||||||
|
*
|
||||||
|
* @param parent The parent widget for the dialog
|
||||||
|
* @param initialBoardName The board zone to preselect in the dialog. Unknown names fall
|
||||||
|
* back to main.
|
||||||
|
* @param chosenBoardName (out) The internal name of the board zone the user chose
|
||||||
|
* @param nameValidator Optional validator forwarded to the dialog
|
||||||
|
* @return The trimmed zone name, or an empty string if the user cancelled
|
||||||
|
*/
|
||||||
|
static QString promptForNewZone(QWidget *parent,
|
||||||
|
const QString &initialBoardName,
|
||||||
|
QString *chosenBoardName,
|
||||||
|
const std::function<QString(const QString &)> &nameValidator = {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prompts the user for a new name for an existing custom zone.
|
||||||
|
*
|
||||||
|
* Same inline validation as promptForNewZone, but without a parent-zone picker.
|
||||||
|
*
|
||||||
|
* @param parent The parent widget for the dialog
|
||||||
|
* @param currentZoneName The current name, prefilled for editing
|
||||||
|
* @param nameValidator Validator deciding whether a candidate name is usable. It sees
|
||||||
|
* the current name too, so callers wanting to allow unchanged names must
|
||||||
|
* special-case that themselves.
|
||||||
|
* @return The trimmed new name, or an empty string if the user cancelled
|
||||||
|
*/
|
||||||
|
static QString promptForRename(QWidget *parent,
|
||||||
|
const QString ¤tZoneName,
|
||||||
|
const std::function<QString(const QString &)> &nameValidator = {});
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void changeEvent(QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief Sets every user-visible string. Runs on construction and on runtime
|
||||||
|
* language changes.
|
||||||
|
*/
|
||||||
|
void retranslateUi();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Validates the current input, toggling Ok and the inline error label.
|
||||||
|
*/
|
||||||
|
void validateName();
|
||||||
|
|
||||||
|
QLabel *nameLabel;
|
||||||
|
QLineEdit *nameEdit;
|
||||||
|
QLabel *errorLabel;
|
||||||
|
QLabel *boardLabel;
|
||||||
|
QComboBox *boardCombo;
|
||||||
|
QDialogButtonBox *buttonBox;
|
||||||
|
std::function<QString(const QString &)> nameValidator;
|
||||||
|
bool allowBoardSelection;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DECK_ZONE_DIALOG_H
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
#include "dlg_convert_deck_to_cod_format.h"
|
#include "dlg_convert_deck_to_cod_format.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../deck_loader/deck_loader.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||||
|
|
||||||
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
|
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -38,3 +46,71 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const
|
||||||
{
|
{
|
||||||
return dontAskAgainCheckbox->isChecked();
|
return dontAskAgainCheckbox->isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||||
|
|
||||||
|
if (QFile::exists(newFileName)) {
|
||||||
|
QMessageBox::StandardButton reply =
|
||||||
|
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||||
|
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
return reply == QMessageBox::Yes;
|
||||||
|
}
|
||||||
|
return true; // Safe to proceed
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent,
|
||||||
|
const QString &filePath,
|
||||||
|
const std::function<bool()> &convert)
|
||||||
|
{
|
||||||
|
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve saved preference if the prompt is disabled
|
||||||
|
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
||||||
|
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmOverwriteIfExists(parent, filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return convert();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the dialog to the user
|
||||||
|
DialogConvertDeckToCodFormat conversionDialog(parent);
|
||||||
|
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||||
|
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
||||||
|
!conversionDialog.dontAskAgain());
|
||||||
|
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert file
|
||||||
|
if (!confirmOverwriteIfExists(parent, filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!convert()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversionDialog.dontAskAgain()) {
|
||||||
|
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||||
|
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
class DialogConvertDeckToCodFormat : public QDialog
|
class DialogConvertDeckToCodFormat : public QDialog
|
||||||
{
|
{
|
||||||
|
|
@ -24,6 +27,21 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] bool dontAskAgain() const;
|
[[nodiscard]] bool dontAskAgain() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks whether the deck file at \a filePath can store tags.
|
||||||
|
*
|
||||||
|
* If the file is not a .cod deck, prompts the user for conversion to the
|
||||||
|
* Cockatrice format, honoring the saved "always convert / don't ask again"
|
||||||
|
* preference. On acceptance \a convert is called to perform the conversion.
|
||||||
|
*
|
||||||
|
* @param parent The widget to parent the prompt to.
|
||||||
|
* @param filePath The path of the deck file to check.
|
||||||
|
* @param convert Called to convert the deck once the user agrees.
|
||||||
|
* @return true if tags can be stored (no conversion needed, or the conversion
|
||||||
|
* was performed), false if the user declined to convert.
|
||||||
|
*/
|
||||||
|
static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function<bool()> &convert);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QVBoxLayout *layout;
|
QVBoxLayout *layout;
|
||||||
QLabel *label;
|
QLabel *label;
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
#include "../chat_view/chat_view.h"
|
#include "../chat_view/chat_view.h"
|
||||||
#include "../game_selector.h"
|
#include "../game_selector.h"
|
||||||
#include "user_info_box.h"
|
#include "user_info_box.h"
|
||||||
|
#include "user_list_dialog.h"
|
||||||
#include "user_list_manager.h"
|
#include "user_list_manager.h"
|
||||||
#include "user_list_proxy.h"
|
#include "user_list_proxy.h"
|
||||||
#include "user_list_widget.h"
|
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,13 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
||||||
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
||||||
add(games);
|
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) ────────────────────────────────
|
// ── Buddy / ignore (registered users only) ────────────────────────────────
|
||||||
if (!isSelf && isReg) {
|
if (!isSelf && isReg) {
|
||||||
if (isBuddy) {
|
if (isBuddy) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
#include <functional>
|
||||||
#include <libcockatrice/network/server/remote/user_level.h>
|
#include <libcockatrice/network/server/remote/user_level.h>
|
||||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||||
|
|
@ -149,6 +150,17 @@ public:
|
||||||
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
|
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
|
||||||
void refreshHeader();
|
void refreshHeader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a predicate evaluated on every action-button rebuild. It receives
|
||||||
|
* the name of the user the popup currently shows; when it returns true an
|
||||||
|
* "Invite" button is shown. The popup itself never resolves the invite
|
||||||
|
* link, it just forwards the request.
|
||||||
|
*/
|
||||||
|
void setGameInviteAvailable(std::function<bool(const QString &userName)> available)
|
||||||
|
{
|
||||||
|
gameInviteAvailable = std::move(available);
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void mouseEnteredPopup();
|
void mouseEnteredPopup();
|
||||||
void mouseLeftPopup();
|
void mouseLeftPopup();
|
||||||
|
|
@ -159,6 +171,7 @@ signals:
|
||||||
|
|
||||||
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
|
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
|
||||||
void chatRequested(const QString &userName);
|
void chatRequested(const QString &userName);
|
||||||
|
void inviteRequested(const QString &userName);
|
||||||
void detailsRequested(const QString &userName);
|
void detailsRequested(const QString &userName);
|
||||||
void showGamesRequested(const QString &userName);
|
void showGamesRequested(const QString &userName);
|
||||||
void addBuddyRequested(const QString &userName);
|
void addBuddyRequested(const QString &userName);
|
||||||
|
|
@ -200,6 +213,7 @@ private:
|
||||||
QString currentUser;
|
QString currentUser;
|
||||||
ServerInfo_User currentUserInfo;
|
ServerInfo_User currentUserInfo;
|
||||||
bool currentOnline = false;
|
bool currentOnline = false;
|
||||||
|
std::function<bool(const QString &userName)> gameInviteAvailable;
|
||||||
|
|
||||||
UserInfoHeaderWidget *header;
|
UserInfoHeaderWidget *header;
|
||||||
QWidget *actionArea; ///< rebuilt per user
|
QWidget *actionArea; ///< rebuilt per user
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
#include "user_list_dialog.h"
|
||||||
|
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPlainTextEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QRadioButton>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
|
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
|
||||||
|
nameBanCheckBox = new QCheckBox(tr("ban &user name"));
|
||||||
|
nameBanCheckBox->setChecked(true);
|
||||||
|
nameBanEdit = new QLineEdit(QString::fromStdString(info.name()));
|
||||||
|
nameBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
ipBanCheckBox = new QCheckBox(tr("ban &IP address"));
|
||||||
|
ipBanCheckBox->setChecked(true);
|
||||||
|
ipBanEdit = new QLineEdit(QString::fromStdString(info.address()));
|
||||||
|
ipBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
idBanCheckBox = new QCheckBox(tr("ban client I&D"));
|
||||||
|
idBanCheckBox->setChecked(true);
|
||||||
|
idBanEdit = new QLineEdit(QString::fromStdString(info.clientid()));
|
||||||
|
idBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
if (QString::fromStdString(info.clientid()).isEmpty()) {
|
||||||
|
idBanCheckBox->setChecked(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
QGridLayout *banTypeGrid = new QGridLayout;
|
||||||
|
banTypeGrid->addWidget(nameBanCheckBox, 0, 0);
|
||||||
|
banTypeGrid->addWidget(nameBanEdit, 0, 1);
|
||||||
|
banTypeGrid->addWidget(ipBanCheckBox, 1, 0);
|
||||||
|
banTypeGrid->addWidget(ipBanEdit, 1, 1);
|
||||||
|
banTypeGrid->addWidget(idBanCheckBox, 2, 0);
|
||||||
|
banTypeGrid->addWidget(idBanEdit, 2, 1);
|
||||||
|
QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type"));
|
||||||
|
banTypeGroupBox->setLayout(banTypeGrid);
|
||||||
|
|
||||||
|
permanentRadio = new QRadioButton(tr("&permanent ban"));
|
||||||
|
temporaryRadio = new QRadioButton(tr("&temporary ban"));
|
||||||
|
temporaryRadio->setChecked(true);
|
||||||
|
connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits);
|
||||||
|
daysLabel = new QLabel(tr("&Days:"));
|
||||||
|
daysEdit = new QSpinBox;
|
||||||
|
daysEdit->setMinimum(0);
|
||||||
|
daysEdit->setValue(0);
|
||||||
|
daysEdit->setMaximum(10000);
|
||||||
|
daysLabel->setBuddy(daysEdit);
|
||||||
|
hoursLabel = new QLabel(tr("&Hours:"));
|
||||||
|
hoursEdit = new QSpinBox;
|
||||||
|
hoursEdit->setMinimum(0);
|
||||||
|
hoursEdit->setValue(0);
|
||||||
|
hoursEdit->setMaximum(24);
|
||||||
|
hoursLabel->setBuddy(hoursEdit);
|
||||||
|
minutesLabel = new QLabel(tr("&Minutes:"));
|
||||||
|
minutesEdit = new QSpinBox;
|
||||||
|
minutesEdit->setMinimum(0);
|
||||||
|
minutesEdit->setValue(5);
|
||||||
|
minutesEdit->setMaximum(60);
|
||||||
|
minutesLabel->setBuddy(minutesEdit);
|
||||||
|
QGridLayout *durationLayout = new QGridLayout;
|
||||||
|
durationLayout->addWidget(permanentRadio, 0, 0, 1, 6);
|
||||||
|
durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6);
|
||||||
|
durationLayout->addWidget(daysLabel, 2, 0);
|
||||||
|
durationLayout->addWidget(daysEdit, 2, 1);
|
||||||
|
durationLayout->addWidget(hoursLabel, 2, 2);
|
||||||
|
durationLayout->addWidget(hoursEdit, 2, 3);
|
||||||
|
durationLayout->addWidget(minutesLabel, 2, 4);
|
||||||
|
durationLayout->addWidget(minutesEdit, 2, 5);
|
||||||
|
QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban"));
|
||||||
|
durationGroupBox->setLayout(durationLayout);
|
||||||
|
|
||||||
|
QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\n"
|
||||||
|
"This is only saved for moderators and cannot be seen by the banned person."));
|
||||||
|
reasonEdit = new QPlainTextEdit;
|
||||||
|
|
||||||
|
QLabel *visibleReasonLabel =
|
||||||
|
new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person."));
|
||||||
|
visibleReasonEdit = new QPlainTextEdit;
|
||||||
|
|
||||||
|
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||||
|
|
||||||
|
QPushButton *okButton = new QPushButton(tr("&OK"));
|
||||||
|
okButton->setAutoDefault(true);
|
||||||
|
connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked);
|
||||||
|
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
||||||
|
connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject);
|
||||||
|
|
||||||
|
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||||
|
buttonLayout->addStretch();
|
||||||
|
buttonLayout->addWidget(okButton);
|
||||||
|
buttonLayout->addWidget(cancelButton);
|
||||||
|
|
||||||
|
QVBoxLayout *vbox = new QVBoxLayout;
|
||||||
|
vbox->addWidget(banTypeGroupBox);
|
||||||
|
vbox->addWidget(durationGroupBox);
|
||||||
|
vbox->addWidget(reasonLabel);
|
||||||
|
vbox->addWidget(reasonEdit);
|
||||||
|
vbox->addWidget(visibleReasonLabel);
|
||||||
|
vbox->addWidget(visibleReasonEdit);
|
||||||
|
vbox->addWidget(deleteMessages);
|
||||||
|
vbox->addLayout(buttonLayout);
|
||||||
|
|
||||||
|
setLayout(vbox);
|
||||||
|
setWindowTitle(tr("Ban user from server"));
|
||||||
|
}
|
||||||
|
|
||||||
|
WarningDialog::WarningDialog(const QString &userName, const QString &clientID, QWidget *parent) : QDialog(parent)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
descriptionLabel = new QLabel(tr("Which warning would you like to send?"));
|
||||||
|
nameWarning = new QLineEdit(userName);
|
||||||
|
nameWarning->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
warnClientID = new QLineEdit(clientID);
|
||||||
|
warnClientID->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
warningOption = new QComboBox();
|
||||||
|
warningOption->addItem("", "");
|
||||||
|
|
||||||
|
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||||
|
|
||||||
|
QPushButton *okButton = new QPushButton(tr("&OK"));
|
||||||
|
okButton->setAutoDefault(true);
|
||||||
|
connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked);
|
||||||
|
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
||||||
|
connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject);
|
||||||
|
|
||||||
|
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||||
|
buttonLayout->addStretch();
|
||||||
|
buttonLayout->addWidget(okButton);
|
||||||
|
buttonLayout->addWidget(cancelButton);
|
||||||
|
|
||||||
|
QVBoxLayout *vbox = new QVBoxLayout;
|
||||||
|
vbox->addWidget(descriptionLabel);
|
||||||
|
vbox->addWidget(nameWarning);
|
||||||
|
vbox->addWidget(warningOption);
|
||||||
|
vbox->addWidget(deleteMessages);
|
||||||
|
vbox->addLayout(buttonLayout);
|
||||||
|
setLayout(vbox);
|
||||||
|
setWindowTitle(tr("Warn user for misconduct"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WarningDialog::okClicked()
|
||||||
|
{
|
||||||
|
if (nameWarning->text().simplified().isEmpty()) {
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("User name to send a warning to can not be blank, please specify a user to warn."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warningOption->currentData().toString().simplified().isEmpty()) {
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("Warning to use can not be blank, please select a valid warning to send."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WarningDialog::getName() const
|
||||||
|
{
|
||||||
|
return nameWarning->text().simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WarningDialog::getWarnID() const
|
||||||
|
{
|
||||||
|
return warnClientID->text().simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WarningDialog::getReason() const
|
||||||
|
{
|
||||||
|
return warningOption->currentData().toString().simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
int WarningDialog::getDeleteMessages() const
|
||||||
|
{
|
||||||
|
return deleteMessages->isChecked() ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WarningDialog::addWarningOption(const QString &warning, int startingIl)
|
||||||
|
{
|
||||||
|
if (startingIl > 1) {
|
||||||
|
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning);
|
||||||
|
} else {
|
||||||
|
warningOption->addItem(warning, warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BanDialog::okClicked()
|
||||||
|
{
|
||||||
|
if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) {
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("You have to select a name-based, IP-based, clientId based, or some combination of "
|
||||||
|
"the three to place a ban."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameBanCheckBox->isChecked()) {
|
||||||
|
if (nameBanEdit->text().simplified() == "") {
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("You must have a value in the name ban when selecting the name ban checkbox."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ipBanCheckBox->isChecked()) {
|
||||||
|
if (ipBanEdit->text().simplified() == "") {
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("You must have a value in the ip ban when selecting the ip ban checkbox."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idBanCheckBox->isChecked()) {
|
||||||
|
if (idBanEdit->text().simplified() == "") {
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, tr("Error"),
|
||||||
|
tr("You must have a value in the clientid ban when selecting the clientid ban checkbox."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BanDialog::enableTemporaryEdits(bool enabled)
|
||||||
|
{
|
||||||
|
daysLabel->setEnabled(enabled);
|
||||||
|
daysEdit->setEnabled(enabled);
|
||||||
|
hoursLabel->setEnabled(enabled);
|
||||||
|
hoursEdit->setEnabled(enabled);
|
||||||
|
minutesLabel->setEnabled(enabled);
|
||||||
|
minutesEdit->setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BanDialog::getBanId() const
|
||||||
|
{
|
||||||
|
return idBanCheckBox->isChecked() ? idBanEdit->text() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BanDialog::getBanName() const
|
||||||
|
{
|
||||||
|
return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BanDialog::getBanIP() const
|
||||||
|
{
|
||||||
|
return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int BanDialog::getMinutes() const
|
||||||
|
{
|
||||||
|
return permanentRadio->isChecked() ? 0
|
||||||
|
: (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BanDialog::getReason() const
|
||||||
|
{
|
||||||
|
return reasonEdit->toPlainText();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BanDialog::getVisibleReason() const
|
||||||
|
{
|
||||||
|
return visibleReasonEdit->toPlainText();
|
||||||
|
}
|
||||||
|
|
||||||
|
int BanDialog::getDeleteMessages() const
|
||||||
|
{
|
||||||
|
return deleteMessages->isChecked() ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent)
|
||||||
|
: QDialog(_parent), userName(_userName)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
|
||||||
|
auto *updateButton = new QPushButton(tr("Update Notes"));
|
||||||
|
updateButton->setEnabled(false);
|
||||||
|
connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept);
|
||||||
|
|
||||||
|
notes = new QPlainTextEdit(_notes);
|
||||||
|
notes->setMinimumWidth(500);
|
||||||
|
connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); });
|
||||||
|
|
||||||
|
auto *vbox = new QVBoxLayout;
|
||||||
|
vbox->addWidget(notes);
|
||||||
|
vbox->addWidget(updateButton);
|
||||||
|
|
||||||
|
setLayout(vbox);
|
||||||
|
setWindowTitle(tr("Admin Notes for %1").arg(_userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AdminNotesDialog::getNotes() const
|
||||||
|
{
|
||||||
|
return notes->toPlainText();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
#ifndef COCKATRICE_USER_LIST_DIALOG_H
|
||||||
|
#define COCKATRICE_USER_LIST_DIALOG_H
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||||
|
|
||||||
|
class QComboBox;
|
||||||
|
class QLabel;
|
||||||
|
class QPlainTextEdit;
|
||||||
|
class QRadioButton;
|
||||||
|
class QSpinBox;
|
||||||
|
class QLineEdit;
|
||||||
|
class QCheckBox;
|
||||||
|
|
||||||
|
class BanDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QLabel *daysLabel, *hoursLabel, *minutesLabel;
|
||||||
|
QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages;
|
||||||
|
QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit;
|
||||||
|
QSpinBox *daysEdit, *hoursEdit, *minutesEdit;
|
||||||
|
QRadioButton *permanentRadio, *temporaryRadio;
|
||||||
|
QPlainTextEdit *reasonEdit, *visibleReasonEdit;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void okClicked();
|
||||||
|
void enableTemporaryEdits(bool enabled);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr);
|
||||||
|
[[nodiscard]] QString getBanName() const;
|
||||||
|
[[nodiscard]] QString getBanIP() const;
|
||||||
|
[[nodiscard]] QString getBanId() const;
|
||||||
|
[[nodiscard]] int getMinutes() const;
|
||||||
|
[[nodiscard]] QString getReason() const;
|
||||||
|
[[nodiscard]] QString getVisibleReason() const;
|
||||||
|
[[nodiscard]] int getDeleteMessages() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WarningDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QLabel *descriptionLabel;
|
||||||
|
QLineEdit *nameWarning;
|
||||||
|
QComboBox *warningOption;
|
||||||
|
QLineEdit *warnClientID;
|
||||||
|
QCheckBox *deleteMessages;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void okClicked();
|
||||||
|
|
||||||
|
public:
|
||||||
|
WarningDialog(const QString &userName, const QString &clientID, QWidget *parent = nullptr);
|
||||||
|
[[nodiscard]] QString getName() const;
|
||||||
|
[[nodiscard]] QString getWarnID() const;
|
||||||
|
[[nodiscard]] QString getReason() const;
|
||||||
|
[[nodiscard]] int getDeleteMessages() const;
|
||||||
|
void addWarningOption(const QString &warning, int startingIl = 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
class AdminNotesDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QString userName;
|
||||||
|
QPlainTextEdit *notes;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr);
|
||||||
|
[[nodiscard]] QString getName() const
|
||||||
|
{
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
[[nodiscard]] QString getNotes() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COCKATRICE_USER_LIST_DIALOG_H
|
||||||
|
|
@ -1,335 +1,21 @@
|
||||||
#include "user_list_widget.h"
|
#include "user_list_widget.h"
|
||||||
|
|
||||||
#include "../../../../client/settings/cache_settings.h"
|
#include "../../../../client/settings/cache_settings.h"
|
||||||
#include "../../../card_picture_loader/card_picture_loader.h"
|
|
||||||
#include "../../cards/art_crop_attribution.h"
|
|
||||||
#include "../../interface/pixel_map_generator.h"
|
#include "../../interface/pixel_map_generator.h"
|
||||||
#include "../../interface/theme_manager.h"
|
#include "../../interface/theme_manager.h"
|
||||||
#include "../../interface/widgets/tabs/tab_account.h"
|
|
||||||
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||||
#include "../game_selector.h"
|
|
||||||
#include "user_context_menu.h"
|
#include "user_context_menu.h"
|
||||||
#include "user_list_painter.h"
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QEvent>
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QCursor>
|
|
||||||
#include <QFont>
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QInputDialog>
|
|
||||||
#include <QKeyEvent>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QScrollBar>
|
||||||
#include <QPlainTextEdit>
|
#include <QTimer>
|
||||||
#include <QPushButton>
|
#include <QVBoxLayout>
|
||||||
#include <QRadioButton>
|
|
||||||
#include <QScreen>
|
|
||||||
#include <QSignalBlocker>
|
|
||||||
#include <QSpinBox>
|
|
||||||
#include <QWidget>
|
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
|
||||||
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
|
||||||
#include <libcockatrice/settings/appearance_settings.h>
|
#include <libcockatrice/settings/appearance_settings.h>
|
||||||
#include <libcockatrice/utility/string_limits.h>
|
|
||||||
|
|
||||||
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
|
|
||||||
{
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
|
|
||||||
nameBanCheckBox = new QCheckBox(tr("ban &user name"));
|
|
||||||
nameBanCheckBox->setChecked(true);
|
|
||||||
nameBanEdit = new QLineEdit(QString::fromStdString(info.name()));
|
|
||||||
nameBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
|
||||||
ipBanCheckBox = new QCheckBox(tr("ban &IP address"));
|
|
||||||
ipBanCheckBox->setChecked(true);
|
|
||||||
ipBanEdit = new QLineEdit(QString::fromStdString(info.address()));
|
|
||||||
ipBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
|
||||||
idBanCheckBox = new QCheckBox(tr("ban client I&D"));
|
|
||||||
idBanCheckBox->setChecked(true);
|
|
||||||
idBanEdit = new QLineEdit(QString::fromStdString(info.clientid()));
|
|
||||||
idBanEdit->setMaxLength(MAX_NAME_LENGTH);
|
|
||||||
if (QString::fromStdString(info.clientid()).isEmpty()) {
|
|
||||||
idBanCheckBox->setChecked(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
QGridLayout *banTypeGrid = new QGridLayout;
|
|
||||||
banTypeGrid->addWidget(nameBanCheckBox, 0, 0);
|
|
||||||
banTypeGrid->addWidget(nameBanEdit, 0, 1);
|
|
||||||
banTypeGrid->addWidget(ipBanCheckBox, 1, 0);
|
|
||||||
banTypeGrid->addWidget(ipBanEdit, 1, 1);
|
|
||||||
banTypeGrid->addWidget(idBanCheckBox, 2, 0);
|
|
||||||
banTypeGrid->addWidget(idBanEdit, 2, 1);
|
|
||||||
QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type"));
|
|
||||||
banTypeGroupBox->setLayout(banTypeGrid);
|
|
||||||
|
|
||||||
permanentRadio = new QRadioButton(tr("&permanent ban"));
|
|
||||||
temporaryRadio = new QRadioButton(tr("&temporary ban"));
|
|
||||||
temporaryRadio->setChecked(true);
|
|
||||||
connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits);
|
|
||||||
daysLabel = new QLabel(tr("&Days:"));
|
|
||||||
daysEdit = new QSpinBox;
|
|
||||||
daysEdit->setMinimum(0);
|
|
||||||
daysEdit->setValue(0);
|
|
||||||
daysEdit->setMaximum(10000);
|
|
||||||
daysLabel->setBuddy(daysEdit);
|
|
||||||
hoursLabel = new QLabel(tr("&Hours:"));
|
|
||||||
hoursEdit = new QSpinBox;
|
|
||||||
hoursEdit->setMinimum(0);
|
|
||||||
hoursEdit->setValue(0);
|
|
||||||
hoursEdit->setMaximum(24);
|
|
||||||
hoursLabel->setBuddy(hoursEdit);
|
|
||||||
minutesLabel = new QLabel(tr("&Minutes:"));
|
|
||||||
minutesEdit = new QSpinBox;
|
|
||||||
minutesEdit->setMinimum(0);
|
|
||||||
minutesEdit->setValue(5);
|
|
||||||
minutesEdit->setMaximum(60);
|
|
||||||
minutesLabel->setBuddy(minutesEdit);
|
|
||||||
QGridLayout *durationLayout = new QGridLayout;
|
|
||||||
durationLayout->addWidget(permanentRadio, 0, 0, 1, 6);
|
|
||||||
durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6);
|
|
||||||
durationLayout->addWidget(daysLabel, 2, 0);
|
|
||||||
durationLayout->addWidget(daysEdit, 2, 1);
|
|
||||||
durationLayout->addWidget(hoursLabel, 2, 2);
|
|
||||||
durationLayout->addWidget(hoursEdit, 2, 3);
|
|
||||||
durationLayout->addWidget(minutesLabel, 2, 4);
|
|
||||||
durationLayout->addWidget(minutesEdit, 2, 5);
|
|
||||||
QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban"));
|
|
||||||
durationGroupBox->setLayout(durationLayout);
|
|
||||||
|
|
||||||
QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\nThis is only saved for moderators and "
|
|
||||||
"cannot be seen by the banned person."));
|
|
||||||
reasonEdit = new QPlainTextEdit;
|
|
||||||
|
|
||||||
QLabel *visibleReasonLabel =
|
|
||||||
new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person."));
|
|
||||||
visibleReasonEdit = new QPlainTextEdit;
|
|
||||||
|
|
||||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
|
||||||
|
|
||||||
QPushButton *okButton = new QPushButton(tr("&OK"));
|
|
||||||
okButton->setAutoDefault(true);
|
|
||||||
connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked);
|
|
||||||
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
|
||||||
connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject);
|
|
||||||
|
|
||||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
|
||||||
buttonLayout->addStretch();
|
|
||||||
buttonLayout->addWidget(okButton);
|
|
||||||
buttonLayout->addWidget(cancelButton);
|
|
||||||
|
|
||||||
QVBoxLayout *vbox = new QVBoxLayout;
|
|
||||||
vbox->addWidget(banTypeGroupBox);
|
|
||||||
vbox->addWidget(durationGroupBox);
|
|
||||||
vbox->addWidget(reasonLabel);
|
|
||||||
vbox->addWidget(reasonEdit);
|
|
||||||
vbox->addWidget(visibleReasonLabel);
|
|
||||||
vbox->addWidget(visibleReasonEdit);
|
|
||||||
vbox->addWidget(deleteMessages);
|
|
||||||
vbox->addLayout(buttonLayout);
|
|
||||||
|
|
||||||
setLayout(vbox);
|
|
||||||
setWindowTitle(tr("Ban user from server"));
|
|
||||||
}
|
|
||||||
|
|
||||||
WarningDialog::WarningDialog(const QString userName, const QString clientID, QWidget *parent) : QDialog(parent)
|
|
||||||
{
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
descriptionLabel = new QLabel(tr("Which warning would you like to send?"));
|
|
||||||
nameWarning = new QLineEdit(userName);
|
|
||||||
nameWarning->setMaxLength(MAX_NAME_LENGTH);
|
|
||||||
warnClientID = new QLineEdit(clientID);
|
|
||||||
warnClientID->setMaxLength(MAX_NAME_LENGTH);
|
|
||||||
warningOption = new QComboBox();
|
|
||||||
warningOption->addItem("", "");
|
|
||||||
|
|
||||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
|
||||||
|
|
||||||
QPushButton *okButton = new QPushButton(tr("&OK"));
|
|
||||||
okButton->setAutoDefault(true);
|
|
||||||
connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked);
|
|
||||||
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
|
|
||||||
connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject);
|
|
||||||
|
|
||||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
|
||||||
buttonLayout->addStretch();
|
|
||||||
buttonLayout->addWidget(okButton);
|
|
||||||
buttonLayout->addWidget(cancelButton);
|
|
||||||
|
|
||||||
QVBoxLayout *vbox = new QVBoxLayout;
|
|
||||||
vbox->addWidget(descriptionLabel);
|
|
||||||
vbox->addWidget(nameWarning);
|
|
||||||
vbox->addWidget(warningOption);
|
|
||||||
vbox->addWidget(deleteMessages);
|
|
||||||
vbox->addLayout(buttonLayout);
|
|
||||||
setLayout(vbox);
|
|
||||||
setWindowTitle(tr("Warn user for misconduct"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void WarningDialog::okClicked()
|
|
||||||
{
|
|
||||||
if (nameWarning->text().simplified().isEmpty()) {
|
|
||||||
QMessageBox::critical(this, tr("Error"),
|
|
||||||
tr("User name to send a warning to can not be blank, please specify a user to warn."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (warningOption->currentData().toString().simplified().isEmpty()) {
|
|
||||||
QMessageBox::critical(this, tr("Error"),
|
|
||||||
tr("Warning to use can not be blank, please select a valid warning to send."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WarningDialog::getName() const
|
|
||||||
{
|
|
||||||
return nameWarning->text().simplified();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WarningDialog::getWarnID() const
|
|
||||||
{
|
|
||||||
return warnClientID->text().simplified();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WarningDialog::getReason() const
|
|
||||||
{
|
|
||||||
return warningOption->currentData().toString().simplified();
|
|
||||||
}
|
|
||||||
|
|
||||||
int WarningDialog::getDeleteMessages() const
|
|
||||||
{
|
|
||||||
return deleteMessages->isChecked() ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WarningDialog::addWarningOption(const QString warning, int startingIl)
|
|
||||||
{
|
|
||||||
if (startingIl > 1) {
|
|
||||||
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning);
|
|
||||||
} else {
|
|
||||||
warningOption->addItem(warning, warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BanDialog::okClicked()
|
|
||||||
{
|
|
||||||
if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) {
|
|
||||||
QMessageBox::critical(this, tr("Error"),
|
|
||||||
tr("You have to select a name-based, IP-based, clientId based, or some combination of "
|
|
||||||
"the three to place a ban."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nameBanCheckBox->isChecked()) {
|
|
||||||
if (nameBanEdit->text().simplified() == "") {
|
|
||||||
QMessageBox::critical(this, tr("Error"),
|
|
||||||
tr("You must have a value in the name ban when selecting the name ban checkbox."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ipBanCheckBox->isChecked()) {
|
|
||||||
if (ipBanEdit->text().simplified() == "") {
|
|
||||||
QMessageBox::critical(this, tr("Error"),
|
|
||||||
tr("You must have a value in the ip ban when selecting the ip ban checkbox."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idBanCheckBox->isChecked()) {
|
|
||||||
if (idBanEdit->text().simplified() == "") {
|
|
||||||
QMessageBox::critical(
|
|
||||||
this, tr("Error"),
|
|
||||||
tr("You must have a value in the clientid ban when selecting the clientid ban checkbox."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BanDialog::enableTemporaryEdits(bool enabled)
|
|
||||||
{
|
|
||||||
daysLabel->setEnabled(enabled);
|
|
||||||
daysEdit->setEnabled(enabled);
|
|
||||||
hoursLabel->setEnabled(enabled);
|
|
||||||
hoursEdit->setEnabled(enabled);
|
|
||||||
minutesLabel->setEnabled(enabled);
|
|
||||||
minutesEdit->setEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BanDialog::getBanId() const
|
|
||||||
{
|
|
||||||
return idBanCheckBox->isChecked() ? idBanEdit->text() : QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BanDialog::getBanName() const
|
|
||||||
{
|
|
||||||
return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BanDialog::getBanIP() const
|
|
||||||
{
|
|
||||||
return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
int BanDialog::getMinutes() const
|
|
||||||
{
|
|
||||||
return permanentRadio->isChecked() ? 0
|
|
||||||
: (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BanDialog::getReason() const
|
|
||||||
{
|
|
||||||
return reasonEdit->toPlainText();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BanDialog::getVisibleReason() const
|
|
||||||
{
|
|
||||||
return visibleReasonEdit->toPlainText();
|
|
||||||
}
|
|
||||||
|
|
||||||
int BanDialog::getDeleteMessages() const
|
|
||||||
{
|
|
||||||
return deleteMessages->isChecked() ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent)
|
|
||||||
: QDialog(_parent), userName(_userName)
|
|
||||||
{
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
|
|
||||||
auto *updateButton = new QPushButton(tr("Update Notes"));
|
|
||||||
updateButton->setEnabled(false);
|
|
||||||
connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept);
|
|
||||||
|
|
||||||
notes = new QPlainTextEdit(_notes);
|
|
||||||
notes->setMinimumWidth(500);
|
|
||||||
connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); });
|
|
||||||
|
|
||||||
auto *vbox = new QVBoxLayout;
|
|
||||||
vbox->addWidget(notes);
|
|
||||||
vbox->addWidget(updateButton);
|
|
||||||
|
|
||||||
setLayout(vbox);
|
|
||||||
setWindowTitle(tr("Admin Notes for %1").arg(_userName));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AdminNotesDialog::getNotes() const
|
|
||||||
{
|
|
||||||
return notes->toPlainText();
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace UserListRoles
|
namespace UserListRoles
|
||||||
{
|
{
|
||||||
|
|
@ -659,6 +345,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
&cardArtProvider->cache(), &cardArtParamsMap,
|
&cardArtProvider->cache(), &cardArtParamsMap,
|
||||||
window()); // parented to main window so it floats above siblings
|
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->hide();
|
||||||
userInfoPopup->setWindowOpacity(0.0);
|
userInfoPopup->setWindowOpacity(0.0);
|
||||||
userInfoPopup->installEventFilter(this);
|
userInfoPopup->installEventFilter(this);
|
||||||
|
|
@ -976,6 +667,8 @@ void UserListWidget::connectPopupSignals()
|
||||||
|
|
||||||
// Wire all action signals to UserContextMenu::exec*()
|
// Wire all action signals to UserContextMenu::exec*()
|
||||||
connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
|
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::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
|
||||||
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
|
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
|
||||||
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
|
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@
|
||||||
#include "user_list_painter.h"
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDialog>
|
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QQueue>
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QStyledItemDelegate>
|
#include <QStyledItemDelegate>
|
||||||
#include <QTextEdit>
|
#include <QTextEdit>
|
||||||
|
|
@ -31,80 +29,12 @@ class QTreeWidget;
|
||||||
class ServerInfo_User;
|
class ServerInfo_User;
|
||||||
class AbstractClient;
|
class AbstractClient;
|
||||||
class TabSupervisor;
|
class TabSupervisor;
|
||||||
class QLabel;
|
|
||||||
class QCheckBox;
|
|
||||||
class QSpinBox;
|
|
||||||
class QRadioButton;
|
|
||||||
class QPlainTextEdit;
|
|
||||||
class Response;
|
class Response;
|
||||||
class CommandContainer;
|
class CommandContainer;
|
||||||
class UserContextMenu;
|
class UserContextMenu;
|
||||||
class UserListWidget;
|
class UserListWidget;
|
||||||
class QShowEvent;
|
class QShowEvent;
|
||||||
|
|
||||||
class BanDialog : public QDialog
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
private:
|
|
||||||
QLabel *daysLabel, *hoursLabel, *minutesLabel;
|
|
||||||
QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages;
|
|
||||||
QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit;
|
|
||||||
QSpinBox *daysEdit, *hoursEdit, *minutesEdit;
|
|
||||||
QRadioButton *permanentRadio, *temporaryRadio;
|
|
||||||
QPlainTextEdit *reasonEdit, *visibleReasonEdit;
|
|
||||||
private slots:
|
|
||||||
void okClicked();
|
|
||||||
void enableTemporaryEdits(bool enabled);
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr);
|
|
||||||
[[nodiscard]] QString getBanName() const;
|
|
||||||
[[nodiscard]] QString getBanIP() const;
|
|
||||||
[[nodiscard]] QString getBanId() const;
|
|
||||||
[[nodiscard]] int getMinutes() const;
|
|
||||||
[[nodiscard]] QString getReason() const;
|
|
||||||
[[nodiscard]] QString getVisibleReason() const;
|
|
||||||
[[nodiscard]] int getDeleteMessages() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
class WarningDialog : public QDialog
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
private:
|
|
||||||
QLabel *descriptionLabel;
|
|
||||||
QLineEdit *nameWarning;
|
|
||||||
QComboBox *warningOption;
|
|
||||||
QLineEdit *warnClientID;
|
|
||||||
QCheckBox *deleteMessages;
|
|
||||||
private slots:
|
|
||||||
void okClicked();
|
|
||||||
|
|
||||||
public:
|
|
||||||
WarningDialog(const QString userName, const QString clientID, QWidget *parent = nullptr);
|
|
||||||
[[nodiscard]] QString getName() const;
|
|
||||||
[[nodiscard]] QString getWarnID() const;
|
|
||||||
[[nodiscard]] QString getReason() const;
|
|
||||||
[[nodiscard]] int getDeleteMessages() const;
|
|
||||||
void addWarningOption(const QString warning, int startingIl = 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
class AdminNotesDialog : public QDialog
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString userName;
|
|
||||||
QPlainTextEdit *notes;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr);
|
|
||||||
[[nodiscard]] QString getName() const
|
|
||||||
{
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QString getNotes() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
class UserListItemDelegate : public QStyledItemDelegate
|
class UserListItemDelegate : public QStyledItemDelegate
|
||||||
{
|
{
|
||||||
QTreeWidget *tree;
|
QTreeWidget *tree;
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,48 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
homeTabGroupBox = new QGroupBox;
|
homeTabGroupBox = new QGroupBox;
|
||||||
homeTabGroupBox->setLayout(homeTabGrid);
|
homeTabGroupBox->setLayout(homeTabGrid);
|
||||||
|
|
||||||
|
// Playmat settings
|
||||||
|
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||||
|
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||||
|
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||||
|
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||||
|
if (visIdx >= 0) {
|
||||||
|
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||||
|
}
|
||||||
|
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||||
|
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||||
|
});
|
||||||
|
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||||
|
|
||||||
|
// Playmat mode: Override / Fallback / Deck-only
|
||||||
|
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||||
|
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||||
|
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||||
|
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||||
|
if (modeIdx >= 0) {
|
||||||
|
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||||
|
}
|
||||||
|
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||||
|
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||||
|
});
|
||||||
|
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||||
|
|
||||||
|
// User-level playmat settings: fallback collection.
|
||||||
|
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||||
|
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||||
|
|
||||||
|
auto *playmatGrid = new QGridLayout;
|
||||||
|
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||||
|
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||||
|
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||||
|
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||||
|
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||||
|
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||||
|
|
||||||
|
playmatGroupBox = new QGroupBox;
|
||||||
|
playmatGroupBox->setLayout(playmatGrid);
|
||||||
|
|
||||||
|
// Styling settings
|
||||||
styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
|
styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
|
||||||
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||||
&AppearanceSettings::setStyleUserList);
|
&AppearanceSettings::setStyleUserList);
|
||||||
|
|
@ -259,7 +301,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
cardLayoutGroupBox->setLayout(cardLayoutGrid);
|
cardLayoutGroupBox->setLayout(cardLayoutGrid);
|
||||||
|
|
||||||
// Card counter colors
|
// Card counter colors
|
||||||
|
|
||||||
auto *cardCounterColorsLayout = new QGridLayout;
|
auto *cardCounterColorsLayout = new QGridLayout;
|
||||||
cardCounterColorsLayout->setColumnStretch(1, 1);
|
cardCounterColorsLayout->setColumnStretch(1, 1);
|
||||||
cardCounterColorsLayout->setColumnStretch(3, 1);
|
cardCounterColorsLayout->setColumnStretch(3, 1);
|
||||||
|
|
@ -339,47 +380,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
tableGroupBox = new QGroupBox;
|
tableGroupBox = new QGroupBox;
|
||||||
tableGroupBox->setLayout(tableGrid);
|
tableGroupBox->setLayout(tableGrid);
|
||||||
|
|
||||||
// Playmat settings
|
|
||||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
|
||||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
|
||||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
|
||||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
|
||||||
if (visIdx >= 0) {
|
|
||||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
|
||||||
}
|
|
||||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
|
||||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
|
||||||
});
|
|
||||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
|
||||||
|
|
||||||
// Playmat mode: Override / Fallback / Deck-only
|
|
||||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
|
||||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
|
||||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
|
||||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
|
||||||
if (modeIdx >= 0) {
|
|
||||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
|
||||||
}
|
|
||||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
|
||||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
|
||||||
});
|
|
||||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
|
||||||
|
|
||||||
// User-level playmat settings: fallback collection.
|
|
||||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
|
||||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
|
||||||
|
|
||||||
auto *playmatGrid = new QGridLayout;
|
|
||||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
|
||||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
|
||||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
|
||||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
|
||||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
|
||||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
|
||||||
|
|
||||||
playmatGroupBox = new QGroupBox;
|
|
||||||
playmatGroupBox->setLayout(playmatGrid);
|
|
||||||
|
|
||||||
// putting it all together
|
// putting it all together
|
||||||
auto *mainLayout = new QVBoxLayout;
|
auto *mainLayout = new QVBoxLayout;
|
||||||
mainLayout->addWidget(themeGroupBox);
|
mainLayout->addWidget(themeGroupBox);
|
||||||
|
|
@ -512,6 +512,12 @@ void AppearanceSettingsPage::retranslateUi()
|
||||||
homeTabButtonColorSourceBox.setToolTip(
|
homeTabButtonColorSourceBox.setToolTip(
|
||||||
tr("Automatic: extract from background if present, otherwise use theme default"));
|
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"));
|
stylingGroupBox->setTitle(tr("Styling settings"));
|
||||||
styleUserListCheckBox.setText(tr("Style user list"));
|
styleUserListCheckBox.setText(tr("Style user list"));
|
||||||
|
|
||||||
|
|
@ -554,9 +560,4 @@ void AppearanceSettingsPage::retranslateUi()
|
||||||
tableGroupBox->setTitle(tr("Table grid layout"));
|
tableGroupBox->setTitle(tr("Table grid layout"));
|
||||||
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
||||||
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
|
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..."));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,46 +44,55 @@ private:
|
||||||
QLabel homeTabButtonColorSourceLabel;
|
QLabel homeTabButtonColorSourceLabel;
|
||||||
QComboBox homeTabButtonColorSourceBox;
|
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;
|
QLabel playmatVisibilityLabel;
|
||||||
QComboBox playmatVisibilityCombo;
|
QComboBox playmatVisibilityCombo;
|
||||||
QLabel playmatModeLabel;
|
QLabel playmatModeLabel;
|
||||||
QComboBox playmatModeCombo;
|
QComboBox playmatModeCombo;
|
||||||
QLabel playmatDefaultLabel;
|
QLabel playmatDefaultLabel;
|
||||||
QPushButton playmatDefaultEditButton;
|
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<QLabel *> cardCounterNames;
|
||||||
|
|
||||||
|
QCheckBox horizontalHandCheckBox;
|
||||||
|
QCheckBox leftJustifiedHandCheckBox;
|
||||||
|
|
||||||
|
QCheckBox invertVerticalCoordinateCheckBox;
|
||||||
|
QLabel minPlayersForMultiColumnLayoutLabel;
|
||||||
|
QSpinBox minPlayersForMultiColumnLayoutEdit;
|
||||||
|
|
||||||
QGroupBox *themeGroupBox;
|
QGroupBox *themeGroupBox;
|
||||||
QGroupBox *homeTabGroupBox;
|
QGroupBox *homeTabGroupBox;
|
||||||
|
QGroupBox *playmatGroupBox;
|
||||||
QGroupBox *stylingGroupBox;
|
QGroupBox *stylingGroupBox;
|
||||||
QGroupBox *menuGroupBox;
|
QGroupBox *menuGroupBox;
|
||||||
QGroupBox *printingsGroupBox;
|
QGroupBox *printingsGroupBox;
|
||||||
QGroupBox *cardsGroupBox;
|
QGroupBox *cardsGroupBox;
|
||||||
QGroupBox *cardLayoutGroupBox;
|
QGroupBox *cardLayoutGroupBox;
|
||||||
QGroupBox *handGroupBox;
|
|
||||||
QGroupBox *playmatGroupBox;
|
|
||||||
QGroupBox *tableGroupBox;
|
|
||||||
QGroupBox *cardCountersGroupBox;
|
QGroupBox *cardCountersGroupBox;
|
||||||
QList<QLabel *> cardCounterNames;
|
QGroupBox *handGroupBox;
|
||||||
QSpinBox minPlayersForMultiColumnLayoutEdit;
|
QGroupBox *tableGroupBox;
|
||||||
QSpinBox maxFontSizeForCardsEdit;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AppearanceSettingsPage();
|
AppearanceSettingsPage();
|
||||||
|
|
|
||||||
|
|
@ -425,29 +425,28 @@ void GeneralSettingsPage::updateStartupServerControlsVisibility()
|
||||||
|
|
||||||
void GeneralSettingsPage::retranslateUi()
|
void GeneralSettingsPage::retranslateUi()
|
||||||
{
|
{
|
||||||
|
const auto &settings = SettingsCache::instance();
|
||||||
|
|
||||||
languageGroupBox->setTitle(tr("Language settings"));
|
languageGroupBox->setTitle(tr("Language settings"));
|
||||||
languageLabel.setText(tr("Language:"));
|
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(
|
advertiseTranslationPageLabel.setText(
|
||||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||||
deckPathLabel.setText(tr("Decks directory:"));
|
|
||||||
filtersPathLabel.setText(tr("Filters directory:"));
|
versionGroupBox->setTitle(tr("Version settings"));
|
||||||
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:"));
|
|
||||||
updateReleaseChannelLabel.setText(tr("Update channel"));
|
updateReleaseChannelLabel.setText(tr("Update channel"));
|
||||||
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
|
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"));
|
startupCardUpdateCheckBehaviorLabel.setText(tr("Check for card database updates on startup"));
|
||||||
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexNone, tr("Don't check"));
|
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexNone, tr("Don't check"));
|
||||||
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexPrompt,
|
startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexPrompt,
|
||||||
|
|
@ -456,8 +455,13 @@ void GeneralSettingsPage::retranslateUi()
|
||||||
tr("Always update in the background"));
|
tr("Always update in the background"));
|
||||||
cardUpdateCheckIntervalLabel.setText(tr("Check for card database updates every"));
|
cardUpdateCheckIntervalLabel.setText(tr("Check for card database updates every"));
|
||||||
cardUpdateCheckIntervalSpinBox.setSuffix(tr(" days"));
|
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"));
|
showTipsOnStartup.setText(tr("Show tips on startup"));
|
||||||
startupTabLabel.setText(tr("Startup tab:"));
|
startupTabLabel.setText(tr("Startup tab:"));
|
||||||
startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home"));
|
startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home"));
|
||||||
|
|
@ -473,21 +477,18 @@ void GeneralSettingsPage::retranslateUi()
|
||||||
startupServerLabel.setText(tr("Server:"));
|
startupServerLabel.setText(tr("Server:"));
|
||||||
startupRoomLabel.setText(tr("Room:"));
|
startupRoomLabel.setText(tr("Room:"));
|
||||||
startupRoomNameEdit->setPlaceholderText(tr("Room name"));
|
startupRoomNameEdit->setPlaceholderText(tr("Room name"));
|
||||||
resetAllPathsButton->setText(tr("Reset all paths"));
|
|
||||||
|
|
||||||
const auto &settings = SettingsCache::instance();
|
if (settings.getIsPortableBuild()) {
|
||||||
|
pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)"));
|
||||||
QDate lastCheckDate = settings.updates().getLastCardUpdateCheck();
|
} else {
|
||||||
int daysAgo = lastCheckDate.daysTo(QDate::currentDate());
|
pathsGroupBox->setTitle(tr("Paths"));
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
updateReleaseChannelBox.setCurrentIndex(oldIndex);
|
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"));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,37 @@ private:
|
||||||
QGroupBox *startupGroupBox;
|
QGroupBox *startupGroupBox;
|
||||||
QGroupBox *pathsGroupBox;
|
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 *deckPathEdit;
|
||||||
QLineEdit *filtersPathEdit;
|
QLineEdit *filtersPathEdit;
|
||||||
QLineEdit *replaysPathEdit;
|
QLineEdit *replaysPathEdit;
|
||||||
|
|
@ -51,33 +82,6 @@ private:
|
||||||
QLineEdit *tokenDatabasePathEdit;
|
QLineEdit *tokenDatabasePathEdit;
|
||||||
QPushButton *resetAllPathsButton;
|
QPushButton *resetAllPathsButton;
|
||||||
QLabel *allPathsResetLabel;
|
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
|
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
||||||
|
|
|
||||||
|
|
@ -20,26 +20,7 @@ enum visualDeckStoragePromptForConversionIndex
|
||||||
|
|
||||||
UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
||||||
{
|
{
|
||||||
// general settings and notification settings
|
// general 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);
|
|
||||||
|
|
||||||
doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay());
|
doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay());
|
||||||
connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||||
&InterfaceSettings::setDoubleClickToPlay);
|
&InterfaceSettings::setDoubleClickToPlay);
|
||||||
|
|
@ -103,6 +84,26 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
||||||
generalGroupBox = new QGroupBox;
|
generalGroupBox = new QGroupBox;
|
||||||
generalGroupBox->setLayout(generalGrid);
|
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;
|
auto *notificationsGrid = new QGridLayout;
|
||||||
notificationsGrid->addWidget(¬ificationsEnabledCheckBox, 0, 0);
|
notificationsGrid->addWidget(¬ificationsEnabledCheckBox, 0, 0);
|
||||||
notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0);
|
notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0);
|
||||||
|
|
@ -355,6 +356,7 @@ void UserInterfaceSettingsPage::retranslateUi()
|
||||||
notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar"));
|
notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar"));
|
||||||
specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating"));
|
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"));
|
buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect"));
|
||||||
|
|
||||||
animationGroupBox->setTitle(tr("Animation settings"));
|
animationGroupBox->setTitle(tr("Animation settings"));
|
||||||
enableAllAnimationsButton.setText(tr("&Enable all animations"));
|
enableAllAnimationsButton.setText(tr("&Enable all animations"));
|
||||||
disableAllAnimationsButton.setText(tr("&Disable all animations"));
|
disableAllAnimationsButton.setText(tr("&Disable all animations"));
|
||||||
|
|
@ -362,6 +364,7 @@ void UserInterfaceSettingsPage::retranslateUi()
|
||||||
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
|
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
|
||||||
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
|
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
|
||||||
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
|
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
|
||||||
|
|
||||||
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
|
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
|
||||||
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
|
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
|
||||||
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
|
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
|
||||||
|
|
@ -397,8 +400,8 @@ void UserInterfaceSettingsPage::retranslateUi()
|
||||||
0, CommanderBracketNames::CommanderSpellbookBracketNames);
|
0, CommanderBracketNames::CommanderSpellbookBracketNames);
|
||||||
commanderSpellbookIntegrationBracketNamingSelector.setItemText(
|
commanderSpellbookIntegrationBracketNamingSelector.setItemText(
|
||||||
1, CommanderBracketNames::OfficialCommanderBracketNames);
|
1, CommanderBracketNames::OfficialCommanderBracketNames);
|
||||||
|
|
||||||
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer);
|
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer);
|
||||||
|
|
||||||
replayGroupBox->setTitle(tr("Replay settings"));
|
replayGroupBox->setTitle(tr("Replay settings"));
|
||||||
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
|
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
|
||||||
rewindBufferingMsBox.setSuffix(" ms");
|
rewindBufferingMsBox.setSuffix(" ms");
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,6 @@ private slots:
|
||||||
void updateCommanderSpellbookUiState();
|
void updateCommanderSpellbookUiState();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QCheckBox notificationsEnabledCheckBox;
|
|
||||||
QCheckBox specNotificationsEnabledCheckBox;
|
|
||||||
QCheckBox buddyConnectNotificationsEnabledCheckBox;
|
|
||||||
QCheckBox doubleClickToPlayCheckBox;
|
QCheckBox doubleClickToPlayCheckBox;
|
||||||
QCheckBox clickPlaysAllSelectedCheckBox;
|
QCheckBox clickPlaysAllSelectedCheckBox;
|
||||||
QCheckBox playToStackCheckBox;
|
QCheckBox playToStackCheckBox;
|
||||||
|
|
@ -37,12 +34,18 @@ private:
|
||||||
QCheckBox showTotalSelectionCountCheckBox;
|
QCheckBox showTotalSelectionCountCheckBox;
|
||||||
QCheckBox useTearOffMenusCheckBox;
|
QCheckBox useTearOffMenusCheckBox;
|
||||||
QCheckBox keepGameChatFocusCheckBox;
|
QCheckBox keepGameChatFocusCheckBox;
|
||||||
|
|
||||||
|
QCheckBox notificationsEnabledCheckBox;
|
||||||
|
QCheckBox specNotificationsEnabledCheckBox;
|
||||||
|
QCheckBox buddyConnectNotificationsEnabledCheckBox;
|
||||||
|
|
||||||
QPushButton enableAllAnimationsButton;
|
QPushButton enableAllAnimationsButton;
|
||||||
QPushButton disableAllAnimationsButton;
|
QPushButton disableAllAnimationsButton;
|
||||||
QCheckBox tapAnimationCheckBox;
|
QCheckBox tapAnimationCheckBox;
|
||||||
QCheckBox arrowDrawAnimationCheckBox;
|
QCheckBox arrowDrawAnimationCheckBox;
|
||||||
QCheckBox lifeCounterAnimationsCheckBox;
|
QCheckBox lifeCounterAnimationsCheckBox;
|
||||||
QCheckBox battlefieldFlashCheckBox;
|
QCheckBox battlefieldFlashCheckBox;
|
||||||
|
|
||||||
QCheckBox openDeckInNewTabCheckBox;
|
QCheckBox openDeckInNewTabCheckBox;
|
||||||
QLabel visualDeckStoragePromptForConversionLabel;
|
QLabel visualDeckStoragePromptForConversionLabel;
|
||||||
QComboBox visualDeckStoragePromptForConversionSelector;
|
QComboBox visualDeckStoragePromptForConversionSelector;
|
||||||
|
|
@ -57,8 +60,10 @@ private:
|
||||||
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
|
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
|
||||||
QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer;
|
QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer;
|
||||||
QComboBox commanderSpellbookIntegrationBracketNamingSelector;
|
QComboBox commanderSpellbookIntegrationBracketNamingSelector;
|
||||||
|
|
||||||
QLabel rewindBufferingMsLabel;
|
QLabel rewindBufferingMsLabel;
|
||||||
QSpinBox rewindBufferingMsBox;
|
QSpinBox rewindBufferingMsBox;
|
||||||
|
|
||||||
QGroupBox *generalGroupBox;
|
QGroupBox *generalGroupBox;
|
||||||
QGroupBox *notificationsGroupBox;
|
QGroupBox *notificationsGroupBox;
|
||||||
QGroupBox *animationGroupBox;
|
QGroupBox *animationGroupBox;
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,10 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
||||||
}
|
}
|
||||||
|
|
||||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
|
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
|
||||||
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
|
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
|
||||||
tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false)
|
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
|
||||||
|
tabModeration(nullptr), isLocalGame(false)
|
||||||
{
|
{
|
||||||
setElideMode(Qt::ElideRight);
|
setElideMode(Qt::ElideRight);
|
||||||
setMovable(true);
|
setMovable(true);
|
||||||
|
|
@ -245,6 +246,7 @@ void TabSupervisor::retranslateUi()
|
||||||
aTabLog->setText(tr("Logs"));
|
aTabLog->setText(tr("Logs"));
|
||||||
aTabReport->setText(tr("Report Queue"));
|
aTabReport->setText(tr("Report Queue"));
|
||||||
aTabModeration->setText(tr("Moderation"));
|
aTabModeration->setText(tr("Moderation"));
|
||||||
|
aTabCardArtRules->setText(tr("Card Art Rules"));
|
||||||
|
|
||||||
// tabs
|
// tabs
|
||||||
QList<Tab *> tabs;
|
QList<Tab *> tabs;
|
||||||
|
|
@ -256,6 +258,7 @@ void TabSupervisor::retranslateUi()
|
||||||
tabs.append(tabLog);
|
tabs.append(tabLog);
|
||||||
tabs.append(tabReport);
|
tabs.append(tabReport);
|
||||||
tabs.append(tabModeration);
|
tabs.append(tabModeration);
|
||||||
|
tabs.append(tabCardArtRules);
|
||||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||||
while (roomIterator.hasNext()) {
|
while (roomIterator.hasNext()) {
|
||||||
tabs.append(roomIterator.next().value());
|
tabs.append(roomIterator.next().value());
|
||||||
|
|
@ -520,7 +523,9 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||||
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
|
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
|
||||||
openTabModeration();
|
openTabModeration();
|
||||||
}
|
}
|
||||||
openTabCardArtRules();
|
if (SettingsCache::instance().tabs().getTabCardArtRulesOpen()) {
|
||||||
|
openTabCardArtRules();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
@ -582,6 +587,9 @@ void TabSupervisor::stop()
|
||||||
if (tabModeration) {
|
if (tabModeration) {
|
||||||
tabModeration->close();
|
tabModeration->close();
|
||||||
}
|
}
|
||||||
|
if (tabCardArtRules) {
|
||||||
|
tabCardArtRules->close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<Tab *> tabsToDelete;
|
QList<Tab *> tabsToDelete;
|
||||||
|
|
@ -775,6 +783,7 @@ void TabSupervisor::openTabAdmin()
|
||||||
|
|
||||||
void TabSupervisor::actTabCardArtRules(bool checked)
|
void TabSupervisor::actTabCardArtRules(bool checked)
|
||||||
{
|
{
|
||||||
|
SettingsCache::instance().tabs().setTabCardArtRulesOpen(checked);
|
||||||
if (checked && !tabCardArtRules) {
|
if (checked && !tabCardArtRules) {
|
||||||
openTabCardArtRules();
|
openTabCardArtRules();
|
||||||
setCurrentWidget(tabCardArtRules);
|
setCurrentWidget(tabCardArtRules);
|
||||||
|
|
@ -1091,7 +1100,8 @@ QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) con
|
||||||
// The inviter may be in several games of the same room (hosting one and
|
// The inviter may be in several games of the same room (hosting one and
|
||||||
// spectating another, for example). Return every game so the caller can
|
// spectating another, for example). Return every game so the caller can
|
||||||
// let the user choose which one to invite to.
|
// let the user choose which one to invite to.
|
||||||
for (TabGame *tab : gameTabs) {
|
for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) {
|
||||||
|
TabGame *tab = it.value();
|
||||||
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
|
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
|
||||||
if (metaInfo->proto().room_id() != roomId) {
|
if (metaInfo->proto().room_id() != roomId) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "../../../../client/settings/shortcuts_settings.h"
|
#include "../../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../cards/card_info_display_widget.h"
|
#include "../../cards/card_info_display_widget.h"
|
||||||
#include "../../deck_editor/deck_state_manager.h"
|
#include "../../deck_editor/deck_state_manager.h"
|
||||||
|
#include "../../deck_editor/deck_zone_dialog.h"
|
||||||
#include "../../filters/filter_builder.h"
|
#include "../../filters/filter_builder.h"
|
||||||
#include "../../interface/pixel_map_generator.h"
|
#include "../../interface/pixel_map_generator.h"
|
||||||
#include "../../interface/widgets/cards/card_info_frame_widget.h"
|
#include "../../interface/widgets/cards/card_info_frame_widget.h"
|
||||||
|
|
@ -84,6 +85,7 @@ void TabDeckEditorVisual::createCentralFrame()
|
||||||
connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this,
|
connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this,
|
||||||
&TabDeckEditorVisual::showPrintingSelector);
|
&TabDeckEditorVisual::showPrintingSelector);
|
||||||
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo);
|
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo);
|
||||||
|
tabContainer->visualDatabaseDisplay->setNewZoneCreator([this] { return createNewZone(); });
|
||||||
|
|
||||||
centralFrame->addWidget(tabContainer);
|
centralFrame->addWidget(tabContainer);
|
||||||
setCentralWidget(centralWidget);
|
setCentralWidget(centralWidget);
|
||||||
|
|
@ -269,6 +271,19 @@ bool TabDeckEditorVisual::actSaveDeckAs()
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Prompts for and creates a new custom deck zone. Returns the name of the created zone. */
|
||||||
|
QString TabDeckEditorVisual::createNewZone()
|
||||||
|
{
|
||||||
|
QString boardName;
|
||||||
|
const QString zoneName = DeckZoneDialog::promptForNewZone(this, {}, &boardName, [this](const QString &candidate) {
|
||||||
|
return deckStateManager->validateNewZoneName(candidate);
|
||||||
|
});
|
||||||
|
if (!zoneName.isEmpty()) {
|
||||||
|
deckStateManager->createCustomZone(boardName, zoneName);
|
||||||
|
}
|
||||||
|
return zoneName;
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Refreshes keyboard shortcuts for this tab from settings. */
|
/** @brief Refreshes keyboard shortcuts for this tab from settings. */
|
||||||
void TabDeckEditorVisual::refreshShortcuts()
|
void TabDeckEditorVisual::refreshShortcuts()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,12 @@ public slots:
|
||||||
*/
|
*/
|
||||||
bool actSaveDeckAs() override;
|
bool actSaveDeckAs() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prompts for and creates a new custom deck zone.
|
||||||
|
* @return The name of the created zone, or an empty string if creation was cancelled.
|
||||||
|
*/
|
||||||
|
QString createNewZone();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief Sets the deck for this tab and selects the sub-tab to open on
|
* @brief Sets the deck for this tab and selects the sub-tab to open on
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#include <libcockatrice/card/card_info_comparator.h>
|
#include <libcockatrice/card/card_info_comparator.h>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
|
@ -89,6 +90,19 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
databaseView->setItemDelegate(nullptr);
|
databaseView->setItemDelegate(nullptr);
|
||||||
databaseView->setVisible(false);
|
databaseView->setVisible(false);
|
||||||
|
|
||||||
|
// Without a deck model there is nothing to add cards to, so the zone menu stays hidden.
|
||||||
|
if (deckListModel) {
|
||||||
|
databaseView->setZoneMenuProvider(
|
||||||
|
[deckListModel]() -> QList<QPair<QString, QStringList>> {
|
||||||
|
QList<QPair<QString, QStringList>> result;
|
||||||
|
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||||
|
result.append({boardName, deckListModel->getCustomZoneNames(boardName)});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[this] { return newZoneCreator ? newZoneCreator() : QString(); });
|
||||||
|
}
|
||||||
|
|
||||||
searchEdit->setTreeView(databaseView);
|
searchEdit->setTreeView(databaseView);
|
||||||
searchEdit->installEventFilter(databaseView->getKeySignals());
|
searchEdit->installEventFilter(databaseView->getKeySignals());
|
||||||
|
|
||||||
|
|
@ -195,6 +209,11 @@ void VisualDatabaseDisplayWidget::showEvent(QShowEvent *event)
|
||||||
initializeFilters();
|
initializeFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VisualDatabaseDisplayWidget::setNewZoneCreator(const std::function<QString()> &creator)
|
||||||
|
{
|
||||||
|
newZoneCreator = creator;
|
||||||
|
}
|
||||||
|
|
||||||
void VisualDatabaseDisplayWidget::retranslateUi()
|
void VisualDatabaseDisplayWidget::retranslateUi()
|
||||||
{
|
{
|
||||||
databaseLoadIndicator->setText(tr("Loading database ..."));
|
databaseLoadIndicator->setText(tr("Loading database ..."));
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWheelEvent>
|
#include <QWheelEvent>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <functional>
|
||||||
#include <libcockatrice/models/database/card_database_model.h>
|
#include <libcockatrice/models/database/card_database_model.h>
|
||||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||||
#include <qscrollarea.h>
|
#include <qscrollarea.h>
|
||||||
|
|
@ -46,6 +47,12 @@ public:
|
||||||
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
|
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
|
||||||
void setDeckList(const DeckList &new_deck_list_model);
|
void setDeckList(const DeckList &new_deck_list_model);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the callback used to create a custom zone from the add-to-zone menu.
|
||||||
|
* The callback returns the name of the created zone, or an empty string if creation was cancelled.
|
||||||
|
*/
|
||||||
|
void setNewZoneCreator(const std::function<QString()> &creator);
|
||||||
|
|
||||||
CardDatabaseDisplayModel *getDatabaseDisplayModel()
|
CardDatabaseDisplayModel *getDatabaseDisplayModel()
|
||||||
{
|
{
|
||||||
return databaseDisplayModel;
|
return databaseDisplayModel;
|
||||||
|
|
@ -106,6 +113,7 @@ private:
|
||||||
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
||||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||||
CardDatabaseView *databaseView;
|
CardDatabaseView *databaseView;
|
||||||
|
std::function<QString()> newZoneCreator;
|
||||||
QList<ExactCard> *cards;
|
QList<ExactCard> *cards;
|
||||||
QVBoxLayout *mainLayout;
|
QVBoxLayout *mainLayout;
|
||||||
QScrollArea *scrollArea;
|
QScrollArea *scrollArea;
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@
|
||||||
#include "../visual_deck_storage_widget.h"
|
#include "../visual_deck_storage_widget.h"
|
||||||
#include "deck_preview_deck_tags_display_widget.h"
|
#include "deck_preview_deck_tags_display_widget.h"
|
||||||
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -499,21 +497,6 @@ void DeckPreviewWidget::actDeleteFile()
|
||||||
// The folder widget removes this preview once the row is gone.
|
// The folder widget removes this preview once the row is gone.
|
||||||
}
|
}
|
||||||
|
|
||||||
static 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the deck's file format supports tags.
|
* Checks if the deck's file format supports tags.
|
||||||
* If not, then prompt the user for file conversion.
|
* If not, then prompt the user for file conversion.
|
||||||
|
|
@ -521,45 +504,8 @@ static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||||
*/
|
*/
|
||||||
bool DeckPreviewWidget::promptFileConversionIfRequired()
|
bool DeckPreviewWidget::promptFileConversionIfRequired()
|
||||||
{
|
{
|
||||||
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
|
return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve saved preference if the prompt is disabled
|
|
||||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
|
||||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirmOverwriteIfExists(this, filePath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
model->convertToCockatriceFormat(row());
|
model->convertToCockatriceFormat(row());
|
||||||
return true;
|
return true;
|
||||||
}
|
});
|
||||||
|
|
||||||
// Show the dialog to the user
|
|
||||||
DialogConvertDeckToCodFormat conversionDialog(this);
|
|
||||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
|
||||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
|
||||||
!conversionDialog.dontAskAgain());
|
|
||||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to convert file
|
|
||||||
if (!confirmOverwriteIfExists(this, filePath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
model->convertToCockatriceFormat(row());
|
|
||||||
|
|
||||||
if (conversionDialog.dontAskAgain()) {
|
|
||||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
|
||||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,7 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass()
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool();
|
const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool();
|
||||||
if (matches == deckPreviewWidget->isHidden()) {
|
deckPreviewWidget->setVisible(matches);
|
||||||
deckPreviewWidget->setVisible(matches);
|
|
||||||
}
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
++visibleDeckCount;
|
++visibleDeckCount;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/settings/network_settings.h>
|
#include <libcockatrice/settings/network_settings.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
#include <libcockatrice/utility/cryptoutil.h>
|
||||||
|
|
||||||
QTranslator *translator, *qtTranslator;
|
QTranslator *translator, *qtTranslator;
|
||||||
RNG_Abstract *rng;
|
RNG_Abstract *rng;
|
||||||
|
|
@ -292,7 +293,7 @@ int main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rng = new RNG_SFMT;
|
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
|
||||||
themeManager = new ThemeManager;
|
themeManager = new ThemeManager;
|
||||||
soundEngine = new SoundEngine;
|
soundEngine = new SoundEngine;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ include=("cockatrice/src" \
|
||||||
libcockatrice_* \
|
libcockatrice_* \
|
||||||
"oracle/src" \
|
"oracle/src" \
|
||||||
"servatrice/src" \
|
"servatrice/src" \
|
||||||
|
"cmake/pch" \
|
||||||
"tests")
|
"tests")
|
||||||
exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \
|
exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \
|
||||||
"libcockatrice_utility/libcockatrice/utility/peglib.h" \
|
"libcockatrice_utility/libcockatrice/utility/peglib.h" \
|
||||||
"oracle/src/lzma/" \
|
"oracle/src/lzma/" \
|
||||||
"oracle/src/qt-json/" \
|
|
||||||
"oracle/src/zip/" \
|
"oracle/src/zip/" \
|
||||||
"servatrice/src/smtp/")
|
"servatrice/src/smtp/")
|
||||||
exts=("cpp" "h" "proto")
|
exts=("cpp" "h" "proto")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
|
||||||
|
static constexpr int MAX_DECK_SIZE = 1e5;
|
||||||
|
|
||||||
DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode())
|
DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +115,7 @@ void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml)
|
||||||
{
|
{
|
||||||
QString zoneName = xml->attributes().value("name").toString();
|
QString zoneName = xml->attributes().value("name").toString();
|
||||||
InnerDecklistNode *newZone = getZoneObjFromName(zoneName);
|
InnerDecklistNode *newZone = getZoneObjFromName(zoneName);
|
||||||
newZone->readElement(xml);
|
totalCards += newZone->readElement(xml, MAX_DECK_SIZE - totalCards);
|
||||||
}
|
}
|
||||||
|
|
||||||
DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
|
DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
|
||||||
|
|
@ -125,6 +127,8 @@ DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
|
||||||
const QString &cardProviderId,
|
const QString &cardProviderId,
|
||||||
const bool formatLegal)
|
const bool formatLegal)
|
||||||
{
|
{
|
||||||
|
amount = qMin(amount, MAX_DECK_SIZE - totalCards);
|
||||||
|
totalCards += amount;
|
||||||
auto *zoneNode = getZoneObjFromName(zoneName);
|
auto *zoneNode = getZoneObjFromName(zoneName);
|
||||||
auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber,
|
auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber,
|
||||||
cardProviderId, formatLegal);
|
cardProviderId, formatLegal);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
class DecklistNodeTree
|
class DecklistNodeTree
|
||||||
{
|
{
|
||||||
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
|
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
|
||||||
|
int totalCards = 0;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** @brief Constructs an empty DecklistNodeTree. */
|
/** @brief Constructs an empty DecklistNodeTree. */
|
||||||
|
|
@ -114,6 +115,25 @@ public:
|
||||||
*/
|
*/
|
||||||
QList<const InnerDecklistNode *> getCustomZones(const QString &boardZoneName) const;
|
QList<const InnerDecklistNode *> getCustomZones(const QString &boardZoneName) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks whether a zone name is taken anywhere in the deck.
|
||||||
|
*
|
||||||
|
* Covers the standard board names and any top-level or nested custom zone.
|
||||||
|
* @param zoneName The checked name.
|
||||||
|
* @return true if the name is reserved or already in use.
|
||||||
|
*/
|
||||||
|
bool hasZoneName(const QString &zoneName) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Finds a custom zone anywhere in the deck by name.
|
||||||
|
*
|
||||||
|
* Walks the children of every top-level zone, so a zone nested under any
|
||||||
|
* board (and not just the standard ones) is found.
|
||||||
|
* @param zoneName The zone name to find.
|
||||||
|
* @return The matching zone node, or nullptr if none exists.
|
||||||
|
*/
|
||||||
|
InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Applies a function to every card in the deck tree. This can modify the cards.
|
* @brief Applies a function to every card in the deck tree. This can modify the cards.
|
||||||
*
|
*
|
||||||
|
|
@ -127,8 +147,6 @@ private:
|
||||||
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
|
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
|
||||||
InnerDecklistNode *findBoardZone(const QString &boardZoneName) const;
|
InnerDecklistNode *findBoardZone(const QString &boardZoneName) const;
|
||||||
InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName);
|
InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName);
|
||||||
InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const;
|
|
||||||
bool hasZoneName(const QString &zoneName) const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_DECKLIST_NODE_TREE_H
|
#endif // COCKATRICE_DECKLIST_NODE_TREE_H
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,15 @@ bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml)
|
int AbstractDecklistCardNode::readElement(QXmlStreamReader *xml, int /* limit */)
|
||||||
{
|
{
|
||||||
while (!xml->atEnd()) {
|
while (!xml->atEnd()) {
|
||||||
xml->readNext();
|
xml->readNext();
|
||||||
if (xml->isEndElement() && xml->name().toString() == "card") {
|
if (xml->isEndElement() && xml->name().toString() == "card") {
|
||||||
return false;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
|
void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
|
||||||
|
|
@ -60,4 +60,4 @@ void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml)
|
||||||
if (!getCardProviderId().isEmpty()) {
|
if (!getCardProviderId().isEmpty()) {
|
||||||
xml->writeAttribute("uuid", getCardProviderId());
|
xml->writeAttribute("uuid", getCardProviderId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ public:
|
||||||
*
|
*
|
||||||
* This supports loading deck files from Cockatrice’s XML format.
|
* This supports loading deck files from Cockatrice’s XML format.
|
||||||
*/
|
*/
|
||||||
bool readElement(QXmlStreamReader *xml) override;
|
int readElement(QXmlStreamReader *xml, int limit) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Serialize this node’s properties to XML.
|
* @brief Serialize this node’s properties to XML.
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ public:
|
||||||
* Cockatrice deck XML format.
|
* Cockatrice deck XML format.
|
||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
virtual bool readElement(QXmlStreamReader *xml) = 0;
|
virtual int readElement(QXmlStreamReader *xml, int limit) = 0;
|
||||||
virtual void writeElement(QXmlStreamWriter *xml) = 0;
|
virtual void writeElement(QXmlStreamWriter *xml) = 0;
|
||||||
/// @}
|
/// @}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,13 @@ void InnerDecklistNode::setSortMethod(DeckSortMethod method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QList<QString> &InnerDecklistNode::boardZoneNames()
|
||||||
|
{
|
||||||
|
static const QList<QString> names = {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE),
|
||||||
|
QString(DECK_ZONE_MAYBEBOARD)};
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
QString InnerDecklistNode::getVisibleName() const
|
QString InnerDecklistNode::getVisibleName() const
|
||||||
{
|
{
|
||||||
return visibleNameFromName(name);
|
return visibleNameFromName(name);
|
||||||
|
|
@ -87,6 +94,9 @@ AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber(
|
||||||
|
|
||||||
int InnerDecklistNode::height() const
|
int InnerDecklistNode::height() const
|
||||||
{
|
{
|
||||||
|
if (isEmpty()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return at(0)->height() + 1;
|
return at(0)->height() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,27 +151,31 @@ bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InnerDecklistNode::readElement(QXmlStreamReader *xml)
|
int InnerDecklistNode::readElement(QXmlStreamReader *xml, int limit)
|
||||||
{
|
{
|
||||||
|
int totalCards = 0;
|
||||||
while (!xml->atEnd()) {
|
while (!xml->atEnd()) {
|
||||||
xml->readNext();
|
xml->readNext();
|
||||||
const QString childName = xml->name().toString();
|
const QString childName = xml->name().toString();
|
||||||
if (xml->isStartElement()) {
|
if (xml->isStartElement()) {
|
||||||
if (childName == "zone") {
|
if (childName == "zone") {
|
||||||
auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this);
|
auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this);
|
||||||
newZone->readElement(xml);
|
totalCards += newZone->readElement(xml, limit - totalCards);
|
||||||
} else if (childName == "card") {
|
} else if (childName == "card") {
|
||||||
auto *newCard = new DecklistCardNode(
|
int amount = xml->attributes().value("number").toString().toInt();
|
||||||
xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(),
|
amount = qMin(amount, limit - totalCards);
|
||||||
this, -1, xml->attributes().value("setShortName").toString(),
|
auto *newCard = new DecklistCardNode(xml->attributes().value("name").toString(), amount, this, -1,
|
||||||
xml->attributes().value("collectorNumber").toString(), xml->attributes().value("uuid").toString());
|
xml->attributes().value("setShortName").toString(),
|
||||||
newCard->readElement(xml);
|
xml->attributes().value("collectorNumber").toString(),
|
||||||
|
xml->attributes().value("uuid").toString());
|
||||||
|
totalCards += amount;
|
||||||
|
totalCards += newCard->readElement(xml, limit - totalCards);
|
||||||
}
|
}
|
||||||
} else if (xml->isEndElement() && (childName == "zone")) {
|
} else if (xml->isEndElement() && (childName == "zone")) {
|
||||||
return false;
|
return totalCards;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return totalCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InnerDecklistNode::writeElement(QXmlStreamWriter *xml)
|
void InnerDecklistNode::writeElement(QXmlStreamWriter *xml)
|
||||||
|
|
@ -201,4 +215,4 @@ QVector<QPair<int, int>> InnerDecklistNode::sort(Qt::SortOrder order)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@
|
||||||
|
|
||||||
#include "abstract_deck_list_node.h"
|
#include "abstract_deck_list_node.h"
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
/** @brief Constant for the "main" deck zone name. */
|
/** @brief Constant for the "main" deck zone name. */
|
||||||
#define DECK_ZONE_MAIN "main"
|
#define DECK_ZONE_MAIN "main"
|
||||||
/** @brief Constant for the "sideboard" zone name. */
|
/** @brief Constant for the "sideboard" zone name. */
|
||||||
|
|
@ -118,6 +121,13 @@ public:
|
||||||
*/
|
*/
|
||||||
static QString visibleNameFromName(const QString &_name);
|
static QString visibleNameFromName(const QString &_name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The standard board zone names, in display order.
|
||||||
|
*
|
||||||
|
* @return main, side and maybeboard.
|
||||||
|
*/
|
||||||
|
static const QList<QString> &boardZoneNames();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get this node’s display-friendly name.
|
* @brief Get this node’s display-friendly name.
|
||||||
* @return Human-readable name (zone/group name).
|
* @return Human-readable name (zone/group name).
|
||||||
|
|
@ -216,9 +226,10 @@ public:
|
||||||
/**
|
/**
|
||||||
* @brief Deserialize this node and its children from XML.
|
* @brief Deserialize this node and its children from XML.
|
||||||
* @param xml Reader positioned at this element.
|
* @param xml Reader positioned at this element.
|
||||||
* @return true if parsing succeeded.
|
* @param limit The maximum amount of cards to read
|
||||||
|
* @return the amount of cards found
|
||||||
*/
|
*/
|
||||||
bool readElement(QXmlStreamReader *xml) override;
|
int readElement(QXmlStreamReader *xml, int limit) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Serialize this node and its children to XML.
|
* @brief Serialize this node and its children to XML.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/utility/peglib.h>
|
#include <libcockatrice/utility/peglib.h>
|
||||||
|
|
||||||
static peg::parser search(R"(
|
static peg::parser search(R"(
|
||||||
|
|
@ -19,7 +20,7 @@ SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
|
||||||
QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery
|
QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery
|
||||||
|
|
||||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||||
SetQuery <- ('e'/'set') [:] FlexStringValue
|
SetQuery <- ('e'/'set') SetExpression / ([:] FlexStringValue)
|
||||||
OracleQuery <- 'o' [:] MatcherString
|
OracleQuery <- 'o' [:] MatcherString
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,6 +65,8 @@ RegexMatcherString <- ('\\/' / !'/' .)+
|
||||||
FlexStringValue <- CompactStringSet / String / [(] StringList [)]
|
FlexStringValue <- CompactStringSet / String / [(] StringList [)]
|
||||||
CompactStringSet <- StringListString ([,+] StringListString)+
|
CompactStringSet <- StringListString ([,+] StringListString)+
|
||||||
|
|
||||||
|
SetExpression <- NumericOperator ws? String
|
||||||
|
|
||||||
NumericExpression <- NumericOperator ws? NumericValue
|
NumericExpression <- NumericOperator ws? NumericValue
|
||||||
NumericOperator <- [=:] / <[><!][=]?>
|
NumericOperator <- [=:] / <[><!][=]?>
|
||||||
NumericValue <- [0-9]+
|
NumericValue <- [0-9]+
|
||||||
|
|
@ -101,12 +104,25 @@ static void setupParserRules()
|
||||||
return [=](const CardData &x) -> bool { return matcher(x->getCardType()); };
|
return [=](const CardData &x) -> bool { return matcher(x->getCardType()); };
|
||||||
};
|
};
|
||||||
search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter {
|
search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter {
|
||||||
auto matcher = std::any_cast<StringMatcher>(sv[0]);
|
if (sv.choice() == 1) {
|
||||||
return [=](const CardData &x) -> bool {
|
auto matcher = std::any_cast<StringMatcher>(sv[0]);
|
||||||
QList<QString> sets = x->getSets().keys();
|
return [=](const CardData &x) -> bool {
|
||||||
|
QList<QString> sets = x->getSets().keys();
|
||||||
|
|
||||||
auto matchesSet = [&matcher](const QString &set) { return matcher(set); };
|
auto matchesSet = [&matcher](const QString &set) { return matcher(set); };
|
||||||
return std::any_of(sets.begin(), sets.end(), matchesSet);
|
return std::any_of(sets.begin(), sets.end(), matchesSet);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto matcher = std::any_cast<NumberMatcher>(sv[0]);
|
||||||
|
return [=](const CardData &x) -> bool {
|
||||||
|
const auto &sets = x->getSets().values();
|
||||||
|
auto matchesSet = [&](const PrintingInfo &printing) {
|
||||||
|
return printing.getSet()->getEnabled() && matcher(printing.getSet()->getReleaseDate().toJulianDay());
|
||||||
|
};
|
||||||
|
return std::any_of(sets.begin(), sets.end(), [&](const auto &printings) {
|
||||||
|
return std::any_of(printings.begin(), printings.end(), matchesSet);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
search["Rarity"] = [](const peg::SemanticValues &sv) -> QString {
|
search["Rarity"] = [](const peg::SemanticValues &sv) -> QString {
|
||||||
|
|
@ -247,40 +263,54 @@ static void setupParserRules()
|
||||||
return QString::fromStdString(std::string(sv.sv()));
|
return QString::fromStdString(std::string(sv.sv()));
|
||||||
};
|
};
|
||||||
|
|
||||||
search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
|
search["NumericOperator"] = [](const peg::SemanticValues &sv) -> NumberComparer {
|
||||||
const auto arg = std::any_cast<int>(sv[1]);
|
const auto op = QString::fromStdString(std::string(sv.sv()));
|
||||||
const auto op = std::any_cast<QString>(sv[0]);
|
|
||||||
|
|
||||||
if (op == ">") {
|
if (op == ">") {
|
||||||
return [=](const int s) { return s > arg; };
|
return [=](const int s, const int arg) { return s > arg; };
|
||||||
}
|
}
|
||||||
if (op == ">=") {
|
if (op == ">=") {
|
||||||
return [=](const int s) { return s >= arg; };
|
return [=](const int s, const int arg) { return s >= arg; };
|
||||||
}
|
}
|
||||||
if (op == "<") {
|
if (op == "<") {
|
||||||
return [=](const int s) { return s < arg; };
|
return [=](const int s, const int arg) { return s < arg; };
|
||||||
}
|
}
|
||||||
if (op == "<=") {
|
if (op == "<=") {
|
||||||
return [=](const int s) { return s <= arg; };
|
return [=](const int s, const int arg) { return s <= arg; };
|
||||||
}
|
}
|
||||||
if (op == "=") {
|
if (op == "=") {
|
||||||
return [=](const int s) { return s == arg; };
|
return [=](const int s, const int arg) { return s == arg; };
|
||||||
}
|
}
|
||||||
if (op == ":") {
|
if (op == ":") {
|
||||||
return [=](const int s) { return s == arg; };
|
return [=](const int s, const int arg) { return s == arg; };
|
||||||
}
|
}
|
||||||
if (op == "!=") {
|
if (op == "!=") {
|
||||||
return [=](const int s) { return s != arg; };
|
return [=](const int s, const int arg) { return s != arg; };
|
||||||
}
|
}
|
||||||
return [](int) { return false; };
|
return [](int, int) { return false; };
|
||||||
};
|
};
|
||||||
|
|
||||||
search["NumericValue"] = [](const peg::SemanticValues &sv) -> int {
|
search["NumericValue"] = [](const peg::SemanticValues &sv) -> int {
|
||||||
return QString::fromStdString(std::string(sv.sv())).toInt();
|
return QString::fromStdString(std::string(sv.sv())).toInt();
|
||||||
};
|
};
|
||||||
|
|
||||||
search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString {
|
search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
|
||||||
return QString::fromStdString(std::string(sv.sv()));
|
const auto comparer = std::any_cast<NumberComparer>(sv[0]);
|
||||||
|
const auto arg = std::any_cast<int>(sv[1]);
|
||||||
|
return [=](int s) { return comparer(s, arg); };
|
||||||
|
};
|
||||||
|
|
||||||
|
search["SetExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
|
||||||
|
const auto comparer = std::any_cast<NumberComparer>(sv[0]);
|
||||||
|
const auto setCode = std::any_cast<QString>(sv[1]);
|
||||||
|
const auto allSets = CardDatabaseManager::getInstance()->getSetList();
|
||||||
|
for (auto &set : allSets) {
|
||||||
|
if (set->getShortName() == setCode) {
|
||||||
|
const int releaseDate = set->getReleaseDate().toJulianDay();
|
||||||
|
return [=](int s) { return comparer(s, releaseDate); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [](int) { return false; };
|
||||||
};
|
};
|
||||||
|
|
||||||
search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ typedef CardInfoPtr CardData;
|
||||||
typedef std::function<bool(const CardData &)> Filter;
|
typedef std::function<bool(const CardData &)> Filter;
|
||||||
typedef std::function<bool(const QString &)> StringMatcher;
|
typedef std::function<bool(const QString &)> StringMatcher;
|
||||||
typedef std::function<bool(int)> NumberMatcher;
|
typedef std::function<bool(int)> NumberMatcher;
|
||||||
|
typedef std::function<bool(int, int)> NumberComparer;
|
||||||
|
|
||||||
namespace peg
|
namespace peg
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public:
|
||||||
[[nodiscard]] virtual bool getTabLogOpen() const = 0;
|
[[nodiscard]] virtual bool getTabLogOpen() const = 0;
|
||||||
[[nodiscard]] virtual bool getTabReportOpen() const = 0;
|
[[nodiscard]] virtual bool getTabReportOpen() const = 0;
|
||||||
[[nodiscard]] virtual bool getTabModerationOpen() const = 0;
|
[[nodiscard]] virtual bool getTabModerationOpen() const = 0;
|
||||||
|
[[nodiscard]] virtual bool getTabCardArtRulesOpen() const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
|
#endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h)
|
||||||
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
|
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
|
||||||
|
|
||||||
add_library(
|
add_library(
|
||||||
libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp
|
libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_model_custom_zones.cpp
|
||||||
|
deck_list_sort_filter_proxy_model.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ void DeckListModel::rebuildTree()
|
||||||
for (int j = 0; j < currentZone->size(); j++) {
|
for (int j = 0; j < currentZone->size(); j++) {
|
||||||
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||||
|
|
||||||
//! \todo Better sanity checking.
|
// Non-card children are custom zones; they are mirrored in a single
|
||||||
|
// pass below so each is mirrored exactly once.
|
||||||
if (currentCard == nullptr) {
|
if (currentCard == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -82,8 +83,19 @@ void DeckListModel::rebuildTree()
|
||||||
|
|
||||||
new DecklistModelCardNode(currentCard, groupNode);
|
new DecklistModelCardNode(currentCard, groupNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom zones nested under the board zone are mirrored as-is, with their
|
||||||
|
// cards as direct children (no further grouping).
|
||||||
|
DeckListModelCustomZones::mirrorCustomZones(currentZone, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The shadow tree was built in deck file order. Apply the active sort while
|
||||||
|
// the reset is still open so every consumer (tree view and visual editor)
|
||||||
|
// sees the canonical order from the start. sortShadowTree emits no signals,
|
||||||
|
// which is only valid before endResetModel closes the reset.
|
||||||
|
root->setSortMethod(lastKnownColumn == 0 ? DeckSortMethod::ByNumber : DeckSortMethod::ByName);
|
||||||
|
sortShadowTree(root, lastKnownOrder);
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
|
|
||||||
refreshCardFormatLegalities();
|
refreshCardFormatLegalities();
|
||||||
|
|
@ -154,6 +166,9 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
|
||||||
case DeckRoles::IsLegalRole:
|
case DeckRoles::IsLegalRole:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case DeckRoles::IsCustomZoneRole:
|
||||||
|
return DeckListModelCustomZones::isCustomZone(group);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -190,6 +205,10 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
|
||||||
return card->getFormatLegality();
|
return card->getFormatLegality();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case DeckRoles::IsCustomZoneRole: {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -327,6 +346,13 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom zone rows are managed through the deck tree, never removed as model rows.
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
if (DeckListModelCustomZones::isCustomZone(node->at(row + i))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beginRemoveRows(parent, row, row + count - 1);
|
beginRemoveRows(parent, row, row + count - 1);
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
AbstractDecklistNode *toDelete = node->takeAt(row);
|
AbstractDecklistNode *toDelete = node->takeAt(row);
|
||||||
|
|
@ -337,7 +363,8 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||||
}
|
}
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
|
|
||||||
if (node->empty() && (node != root)) {
|
// Empty criteria groups get pruned, but custom zones stay until explicitly deleted.
|
||||||
|
if (node->empty() && (node != root) && !DeckListModelCustomZones::isCustomZone(node)) {
|
||||||
removeRows(parent.row(), 1, parent.parent());
|
removeRows(parent.row(), 1, parent.parent());
|
||||||
} else {
|
} else {
|
||||||
emitRecursiveUpdates(parent);
|
emitRecursiveUpdates(parent);
|
||||||
|
|
@ -351,7 +378,8 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||||
|
|
||||||
InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent)
|
InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent)
|
||||||
{
|
{
|
||||||
auto *newNode = dynamic_cast<InnerDecklistNode *>(parent->findChild(name));
|
// Group lookups must not resolve a mirrored custom zone that shares the name.
|
||||||
|
auto *newNode = DeckListModelCustomZones::findGroupChild(parent, name);
|
||||||
if (!newNode) {
|
if (!newNode) {
|
||||||
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
|
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
|
||||||
newNode = new InnerDecklistNode(name, parent);
|
newNode = new InnerDecklistNode(name, parent);
|
||||||
|
|
@ -365,24 +393,44 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName,
|
||||||
const QString &providerId,
|
const QString &providerId,
|
||||||
const QString &cardNumber) const
|
const QString &cardNumber) const
|
||||||
{
|
{
|
||||||
InnerDecklistNode *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
|
|
||||||
if (!zoneNode) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName);
|
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria);
|
// 1. Board zone lookup: search the criteria groups, then the custom zones
|
||||||
InnerDecklistNode *groupNode = dynamic_cast<InnerDecklistNode *>(zoneNode->findChild(groupCriteria));
|
// nested under the board.
|
||||||
if (!groupNode) {
|
if (auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName))) {
|
||||||
return nullptr;
|
QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria);
|
||||||
|
if (auto *groupNode = DeckListModelCustomZones::findGroupChild(zoneNode, groupCriteria)) {
|
||||||
|
if (auto *card = dynamic_cast<DecklistModelCardNode *>(
|
||||||
|
groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto *child : *zoneNode) {
|
||||||
|
if (!DeckListModelCustomZones::isCustomZone(child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto *customZone = dynamic_cast<InnerDecklistNode *>(child);
|
||||||
|
if (!customZone) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (auto *card = dynamic_cast<DecklistModelCardNode *>(
|
||||||
|
customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dynamic_cast<DecklistModelCardNode *>(
|
// 2. Custom zone lookup by name (custom zone names are deck-unique).
|
||||||
groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber));
|
if (auto *customZone = DeckListModelCustomZones::findSubZoneByName(root, zoneName)) {
|
||||||
|
return dynamic_cast<DecklistModelCardNode *>(
|
||||||
|
customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
QModelIndex DeckListModel::findCard(const QString &cardName,
|
QModelIndex DeckListModel::findCard(const QString &cardName,
|
||||||
|
|
@ -423,29 +471,95 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerDecklistNode *zoneNode = createNodeIfNeeded(zoneName, root);
|
|
||||||
|
|
||||||
CardInfoPtr cardInfo = card.getCardPtr();
|
CardInfoPtr cardInfo = card.getCardPtr();
|
||||||
PrintingInfo printingInfo = card.getPrinting();
|
PrintingInfo printingInfo = card.getPrinting();
|
||||||
|
|
||||||
QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria);
|
InnerDecklistNode *cardParent = nullptr;
|
||||||
InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode);
|
|
||||||
|
|
||||||
const QModelIndex parentIndex = nodeToIndex(groupNode);
|
auto *boardNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
|
||||||
auto *cardNode = dynamic_cast<DecklistModelCardNode *>(groupNode->findCardChildByNameProviderIdAndNumber(
|
auto *customZoneNode = boardNode ? nullptr : DeckListModelCustomZones::findSubZoneByName(root, zoneName);
|
||||||
|
|
||||||
|
// Mirroring flattens nested deck sub-zones into shadow rows, so a shadow row
|
||||||
|
// index is only usable as a deck-tree position while both sides have the same
|
||||||
|
// direct-children shape. When they diverge, the card is appended to the deck
|
||||||
|
// zone instead of being written out of range.
|
||||||
|
InnerDecklistNode *deckCardParent = nullptr;
|
||||||
|
bool customZoneNeedsAppend = false;
|
||||||
|
|
||||||
|
if (boardNode) {
|
||||||
|
// Board zone: cards are grouped by the active criteria.
|
||||||
|
QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria);
|
||||||
|
cardParent = createNodeIfNeeded(groupCriteria, boardNode);
|
||||||
|
} else if (customZoneNode) {
|
||||||
|
// Custom zone: cards live flat inside the zone.
|
||||||
|
cardParent = customZoneNode;
|
||||||
|
auto *listRoot = deckList->getTree()->getRoot();
|
||||||
|
for (int i = 0; i < listRoot->size(); ++i) {
|
||||||
|
auto *boardZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||||
|
if (!boardZone) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deckCardParent = dynamic_cast<InnerDecklistNode *>(boardZone->findChild(zoneName));
|
||||||
|
if (deckCardParent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A deck custom zone holding nested sub-zones mirrors with flattened rows,
|
||||||
|
// so a shadow row index does not map onto its direct children.
|
||||||
|
if (deckCardParent) {
|
||||||
|
for (int i = 0; i < deckCardParent->size(); ++i) {
|
||||||
|
if (dynamic_cast<InnerDecklistNode *>(deckCardParent->at(i))) {
|
||||||
|
customZoneNeedsAppend = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not present in the shadow tree. The deck tree may still hold a custom
|
||||||
|
// zone that has not been mirrored (callers can add a zone and then a
|
||||||
|
// card without a rebuild). Check before falling back to creating a
|
||||||
|
// top-level zone the deck does not actually have.
|
||||||
|
auto *listRoot = deckList->getTree()->getRoot();
|
||||||
|
bool hasDeckZone = false;
|
||||||
|
for (int i = 0; i < listRoot->size(); ++i) {
|
||||||
|
if (auto *boardZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i))) {
|
||||||
|
// Only real zones count: a card sitting directly under the board
|
||||||
|
// shares the name comparison but is not a zone, and treating it as
|
||||||
|
// one would recurse forever without mirroring anything.
|
||||||
|
if (dynamic_cast<InnerDecklistNode *>(boardZone->findChild(zoneName))) {
|
||||||
|
hasDeckZone = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDeckZone) {
|
||||||
|
rebuildTree();
|
||||||
|
return addCard(card, zoneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown zone: create a top-level zone (legacy behavior).
|
||||||
|
QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria);
|
||||||
|
auto *newZone = createNodeIfNeeded(zoneName, root);
|
||||||
|
cardParent = createNodeIfNeeded(groupCriteria, newZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QModelIndex parentIndex = nodeToIndex(cardParent);
|
||||||
|
auto *cardNode = dynamic_cast<DecklistModelCardNode *>(cardParent->findCardChildByNameProviderIdAndNumber(
|
||||||
card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num")));
|
card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num")));
|
||||||
const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName();
|
const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName();
|
||||||
|
|
||||||
bool cardNodeAdded = false;
|
bool cardNodeAdded = false;
|
||||||
if (!cardNode) {
|
if (!cardNode) {
|
||||||
// Determine the correct index
|
// Determine the correct index
|
||||||
int insertRow = findSortedInsertRow(groupNode, cardInfo);
|
int insertRow = findSortedInsertRow(cardParent, cardInfo);
|
||||||
|
int deckInsertRow = customZoneNeedsAppend ? -1 : insertRow;
|
||||||
|
|
||||||
auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName,
|
auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, deckInsertRow, cardSetName,
|
||||||
printingInfo.getProperty("num"), printingInfo.getProperty("uuid"));
|
printingInfo.getProperty("num"), printingInfo.getProperty("uuid"));
|
||||||
|
|
||||||
beginInsertRows(parentIndex, insertRow, insertRow);
|
beginInsertRows(parentIndex, insertRow, insertRow);
|
||||||
cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow);
|
cardNode = new DecklistModelCardNode(decklistCard, cardParent, insertRow);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
cardNodeAdded = true;
|
cardNodeAdded = true;
|
||||||
|
|
@ -576,21 +690,41 @@ QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const
|
||||||
return createIndex(node->getParent()->indexOf(node), 0, node);
|
return createIndex(node->getParent()->indexOf(node), 0, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sorts a freshly built shadow subtree without emitting model signals.
|
||||||
|
*
|
||||||
|
* Used by rebuildTree while the model reset is still open (emitting layout
|
||||||
|
* changes during a reset is invalid). Reorders every node just like
|
||||||
|
* sortHelper does, but ignores the movement mapping because there are no
|
||||||
|
* persistent indices established yet.
|
||||||
|
*/
|
||||||
|
void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order)
|
||||||
|
{
|
||||||
|
// The mapping is not needed: fresh shadow nodes have no persistent indices yet.
|
||||||
|
(void)DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order);
|
||||||
|
|
||||||
|
for (int i = node->size() - 1; i >= 0; --i) {
|
||||||
|
if (auto *subNode = dynamic_cast<InnerDecklistNode *>(node->at(i))) {
|
||||||
|
sortShadowTree(subNode, order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order)
|
void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order)
|
||||||
{
|
{
|
||||||
// Sort children of node and save the information needed to
|
// Sort children (custom zones always sorted after groups within a board) and
|
||||||
// update the list of persistent indexes.
|
// use the movement mapping to update the list of persistent indices.
|
||||||
QVector<QPair<int, int>> sortResult = node->sort(order);
|
const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order);
|
||||||
|
|
||||||
QModelIndexList from, to;
|
QModelIndexList from, to;
|
||||||
int columns = columnCount();
|
int columns = columnCount();
|
||||||
for (int i = sortResult.size() - 1; i >= 0; --i) {
|
for (const auto &move : mapping) {
|
||||||
const int fromRow = sortResult[i].first;
|
const int preSortRow = move.first;
|
||||||
const int toRow = sortResult[i].second;
|
const int finalRow = move.second;
|
||||||
AbstractDecklistNode *temp = node->at(toRow);
|
AbstractDecklistNode *temp = node->at(finalRow);
|
||||||
for (int j = 0; j < columns; ++j) {
|
for (int j = 0; j < columns; ++j) {
|
||||||
from << createIndex(fromRow, j, temp);
|
from << createIndex(preSortRow, j, temp);
|
||||||
to << createIndex(toRow, j, temp);
|
to << createIndex(finalRow, j, temp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
changePersistentIndexList(from, to);
|
changePersistentIndexList(from, to);
|
||||||
|
|
@ -704,6 +838,15 @@ QList<QString> DeckListModel::getZones() const
|
||||||
return zones;
|
return zones;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList DeckListModel::getCustomZoneNames(const QString &boardZoneName) const
|
||||||
|
{
|
||||||
|
QStringList zoneNames;
|
||||||
|
for (const auto *customZone : deckList->getTree()->getCustomZones(boardZoneName)) {
|
||||||
|
zoneNames.append(customZone->getName());
|
||||||
|
}
|
||||||
|
return zoneNames;
|
||||||
|
}
|
||||||
|
|
||||||
static int maxAllowedForLegality(const FormatRules &format, const QString &legality)
|
static int maxAllowedForLegality(const FormatRules &format, const QString &legality)
|
||||||
{
|
{
|
||||||
for (const AllowedCount &c : format.allowedCounts) {
|
for (const AllowedCount &c : format.allowedCounts) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#ifndef DECKLISTMODEL_H
|
#ifndef DECKLISTMODEL_H
|
||||||
#define DECKLISTMODEL_H
|
#define DECKLISTMODEL_H
|
||||||
|
|
||||||
|
#include "deck_list_model_custom_zones.h"
|
||||||
|
|
||||||
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h>
|
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h>
|
||||||
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h>
|
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
#include <QAbstractItemModel>
|
#include <QAbstractItemModel>
|
||||||
|
|
@ -30,7 +32,8 @@ enum
|
||||||
{
|
{
|
||||||
IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */
|
IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */
|
||||||
DepthRole, /**< Depth level within the deck's grouping hierarchy. */
|
DepthRole, /**< Depth level within the deck's grouping hierarchy. */
|
||||||
IsLegalRole /**< Whether the card is legal in the current deck format. */
|
IsLegalRole, /**< Whether the card is legal in the current deck format. */
|
||||||
|
IsCustomZoneRole /**< Whether the item represents a custom zone nested under a board zone. */
|
||||||
};
|
};
|
||||||
} // namespace DeckRoles
|
} // namespace DeckRoles
|
||||||
|
|
||||||
|
|
@ -391,6 +394,14 @@ public:
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] QList<QString> getZones() const;
|
[[nodiscard]] QList<QString> getZones() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the names of the custom zones nested under the given board zone.
|
||||||
|
*
|
||||||
|
* @param boardZoneName The board zone to query (main/side/maybeboard)
|
||||||
|
* @return The custom zone names, in deck order
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QStringList getCustomZoneNames(const QString &boardZoneName) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSharedPointer<DeckList> deckList; /**< Pointer to the decklist providing the underlying data. */
|
QSharedPointer<DeckList> deckList; /**< Pointer to the decklist providing the underlying data. */
|
||||||
InnerDecklistNode *root; /**< Root node of the model tree. */
|
InnerDecklistNode *root; /**< Root node of the model tree. */
|
||||||
|
|
@ -427,6 +438,7 @@ private:
|
||||||
void emitRecursiveUpdates(const QModelIndex &index);
|
void emitRecursiveUpdates(const QModelIndex &index);
|
||||||
|
|
||||||
void sortHelper(InnerDecklistNode *node, Qt::SortOrder order);
|
void sortHelper(InnerDecklistNode *node, Qt::SortOrder order);
|
||||||
|
void sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order);
|
||||||
|
|
||||||
template <typename T> T getNode(const QModelIndex &index) const
|
template <typename T> T getNode(const QModelIndex &index) const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
#include "deck_list_model_custom_zones.h"
|
||||||
|
|
||||||
|
#include "deck_list_model.h"
|
||||||
|
|
||||||
|
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
namespace DeckListModelCustomZones
|
||||||
|
{
|
||||||
|
|
||||||
|
bool isCustomZone(const AbstractDecklistNode *node)
|
||||||
|
{
|
||||||
|
return dynamic_cast<const DecklistModelSubZoneNode *>(node) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Flattens every card under @p zone into @p shadowZone, preserving order.
|
||||||
|
*
|
||||||
|
* Custom zones mirror as a single row level: cards nested in sub-zones of any
|
||||||
|
* depth are added as direct children of the mirrored zone so no card is left
|
||||||
|
* without a model row.
|
||||||
|
*/
|
||||||
|
void flattenCards(const InnerDecklistNode *zone, InnerDecklistNode *shadowZone)
|
||||||
|
{
|
||||||
|
for (int k = 0; k < zone->size(); k++) {
|
||||||
|
if (auto *zoneCard = dynamic_cast<DecklistCardNode *>(zone->at(k))) {
|
||||||
|
new DecklistModelCardNode(zoneCard, shadowZone);
|
||||||
|
} else if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zone->at(k))) {
|
||||||
|
flattenCards(subZone, shadowZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < deckBoardZone->size(); j++) {
|
||||||
|
auto *customZone = dynamic_cast<const InnerDecklistNode *>(deckBoardZone->at(j));
|
||||||
|
if (!customZone) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *shadowZone = new DecklistModelSubZoneNode(customZone->getName(), shadowBoardZone);
|
||||||
|
flattenCards(customZone, shadowZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < parent->size(); i++) {
|
||||||
|
AbstractDecklistNode *child = parent->at(i);
|
||||||
|
if (isCustomZone(child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto *group = dynamic_cast<InnerDecklistNode *>(child);
|
||||||
|
if (group && group->getName() == name) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < root->size(); i++) {
|
||||||
|
auto *boardZone = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||||
|
if (!boardZone) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < boardZone->size(); j++) {
|
||||||
|
auto *customZone = dynamic_cast<DecklistModelSubZoneNode *>(boardZone->at(j));
|
||||||
|
if (customZone && customZone->getName() == zoneName) {
|
||||||
|
return customZone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sorts a node's children and returns the (preSortRow, finalRow) mapping.
|
||||||
|
*/
|
||||||
|
QList<QPair<int, int>> plainSort(InnerDecklistNode *node, Qt::SortOrder order)
|
||||||
|
{
|
||||||
|
const QVector<QPair<int, int>> sortResult = node->sort(order);
|
||||||
|
|
||||||
|
QList<QPair<int, int>> mapping;
|
||||||
|
mapping.reserve(node->size());
|
||||||
|
for (int i = 0; i < node->size(); ++i) {
|
||||||
|
mapping.append({sortResult[i].first, i});
|
||||||
|
}
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sorts a board zone's children, then stably moves custom zones to the end.
|
||||||
|
*
|
||||||
|
* @return The (preSortRow, finalRow) mapping covering both the sort and the shift.
|
||||||
|
*/
|
||||||
|
QList<QPair<int, int>> boardSort(InnerDecklistNode *node, Qt::SortOrder order)
|
||||||
|
{
|
||||||
|
const QVector<QPair<int, int>> sortResult = node->sort(order);
|
||||||
|
|
||||||
|
QVector<AbstractDecklistNode *> groups;
|
||||||
|
QVector<AbstractDecklistNode *> customZones;
|
||||||
|
QHash<AbstractDecklistNode *, int> preSortRowOf;
|
||||||
|
|
||||||
|
groups.reserve(node->size());
|
||||||
|
customZones.reserve(node->size());
|
||||||
|
|
||||||
|
for (int i = 0; i < node->size(); ++i) {
|
||||||
|
AbstractDecklistNode *child = node->at(i);
|
||||||
|
preSortRowOf.insert(child, sortResult[i].first);
|
||||||
|
if (isCustomZone(child)) {
|
||||||
|
customZones.append(child);
|
||||||
|
} else {
|
||||||
|
groups.append(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<AbstractDecklistNode *> ordered = groups + customZones;
|
||||||
|
for (int i = 0; i < ordered.size(); ++i) {
|
||||||
|
node->replace(i, ordered[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QPair<int, int>> mapping;
|
||||||
|
mapping.reserve(ordered.size());
|
||||||
|
for (int i = 0; i < ordered.size(); ++i) {
|
||||||
|
mapping.append({preSortRowOf.value(ordered[i]), i});
|
||||||
|
}
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QList<QPair<int, int>> sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order)
|
||||||
|
{
|
||||||
|
const bool isBoardZone = (node != root) && (node->getParent() == root);
|
||||||
|
return isBoardZone ? boardSort(node, order) : plainSort(node, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace DeckListModelCustomZones
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
#ifndef DECK_LIST_MODEL_CUSTOM_ZONES_H
|
||||||
|
#define DECK_LIST_MODEL_CUSTOM_ZONES_H
|
||||||
|
|
||||||
|
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class DecklistModelSubZoneNode
|
||||||
|
* @ingroup DeckModels
|
||||||
|
* @brief Model node representing a custom zone nested under a board zone.
|
||||||
|
*
|
||||||
|
* Custom zones group cards by user-defined names (e.g. "Removal", "Utility")
|
||||||
|
* inside a board zone. They are mirrored from the underlying deck tree so that
|
||||||
|
* they can be told apart from criteria group nodes by type.
|
||||||
|
*/
|
||||||
|
class DecklistModelSubZoneNode : public InnerDecklistNode
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using InnerDecklistNode::InnerDecklistNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @namespace DeckListModelCustomZones
|
||||||
|
* @ingroup DeckModels
|
||||||
|
* @brief Tree-level helpers for the deck list model's custom-zone shadow nodes.
|
||||||
|
*
|
||||||
|
* The deck list model keeps a second "shadow" tree of InnerDecklistNode that
|
||||||
|
* mirrors the canonical deck tree for grouping and sorting. Custom zones add a
|
||||||
|
* layer of bookkeeping to that shadow tree: they must be mirrored alongside
|
||||||
|
* criteria groups, always sort after the groups within a board, and be
|
||||||
|
* resolvable by deck-unique name.
|
||||||
|
*
|
||||||
|
* This namespace centralizes every "what is / where is a custom zone" decision
|
||||||
|
* so the model itself only wires the results into Qt model signals.
|
||||||
|
*/
|
||||||
|
namespace DeckListModelCustomZones
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the given node is a custom zone (as opposed to a criteria group).
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool isCustomZone(const AbstractDecklistNode *node);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Finds a criteria-group child of @p parent by name, skipping custom zones.
|
||||||
|
*
|
||||||
|
* The shadow tree keeps criteria groups and mirrored custom zones as siblings
|
||||||
|
* under a board zone, and `InnerDecklistNode::findChild` matches both by name.
|
||||||
|
* Group lookups must not resolve a custom zone that happens to share the group
|
||||||
|
* name (e.g. a zone called "Creature"), so this searches only non-custom
|
||||||
|
* children.
|
||||||
|
*
|
||||||
|
* @param parent The shadow node whose children are searched.
|
||||||
|
* @param name The group name to find.
|
||||||
|
* @return The matching group node, or nullptr if none exists.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Mirrors the custom zones of a deck board zone into its shadow board node.
|
||||||
|
*
|
||||||
|
* Each custom zone becomes a DecklistModelSubZoneNode under @p shadowBoardZone
|
||||||
|
* with its cards as direct (un-grouped) children.
|
||||||
|
*
|
||||||
|
* @param deckBoardZone The board zone in the canonical deck tree.
|
||||||
|
* @param shadowBoardZone The matching board zone in the model's shadow tree.
|
||||||
|
*/
|
||||||
|
void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Finds a custom zone in the shadow tree by deck-unique name.
|
||||||
|
* @param root Root of the shadow tree.
|
||||||
|
* @param zoneName The custom zone name to find.
|
||||||
|
* @return The matching custom zone node, or nullptr if not found.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sorts a shadow node's children, keeping a board's custom zones last.
|
||||||
|
*
|
||||||
|
* Sorting alone would interleave custom zones with criteria groups by name, but
|
||||||
|
* custom zones must always stay after the groups within a board, regardless of
|
||||||
|
* name. This applies the sort and, for board zones, stably moves the custom
|
||||||
|
* zones to the end.
|
||||||
|
*
|
||||||
|
* @param root Root of the shadow tree (used to classify board zones).
|
||||||
|
* @param node The shadow node whose children are reordered.
|
||||||
|
* @param order Sort order to apply.
|
||||||
|
* @return A list of (preSortRow, finalRow) pairs describing how each node moved.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QList<QPair<int, int>>
|
||||||
|
sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order);
|
||||||
|
|
||||||
|
} // namespace DeckListModelCustomZones
|
||||||
|
|
||||||
|
#endif // DECK_LIST_MODEL_CUSTOM_ZONES_H
|
||||||
|
|
@ -90,7 +90,6 @@ set(PROTO_FILES
|
||||||
event_game_log_notice.proto
|
event_game_log_notice.proto
|
||||||
event_game_say.proto
|
event_game_say.proto
|
||||||
event_game_state_changed.proto
|
event_game_state_changed.proto
|
||||||
event_game_state_changed.proto
|
|
||||||
event_join.proto
|
event_join.proto
|
||||||
event_join_room.proto
|
event_join_room.proto
|
||||||
event_kicked.proto
|
event_kicked.proto
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "rng_sfmt.h"
|
#include "rng_sfmt.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
@ -11,10 +10,11 @@
|
||||||
#define UINT64_MAX (~(uint64_t)0)
|
#define UINT64_MAX (~(uint64_t)0)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
RNG_SFMT::RNG_SFMT(QObject *parent) : RNG_Abstract(parent)
|
RNG_SFMT::RNG_SFMT(uint64_t seed, QObject *parent) : RNG_Abstract(parent)
|
||||||
{
|
{
|
||||||
// initialize the random number generator with a 32bit integer seed (timestamp)
|
// initialize the random number generator with a 64bit seed, e.g. from a CSPRNG
|
||||||
sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch());
|
uint32_t seedArray[2] = {static_cast<uint32_t>(seed), static_cast<uint32_t>(seed >> 32)};
|
||||||
|
sfmt_init_by_array(&sfmt, seedArray, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ private:
|
||||||
unsigned int cdf(unsigned int min, unsigned int max);
|
unsigned int cdf(unsigned int min, unsigned int max);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RNG_SFMT(QObject *parent = nullptr);
|
explicit RNG_SFMT(uint64_t seed, QObject *parent = nullptr);
|
||||||
unsigned int rand(int min, int max) override;
|
unsigned int rand(int min, int max) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,11 @@ bool TabsSettings::getTabModerationOpen() const
|
||||||
return getValue("moderation", QString(), QString(), false).toBool();
|
return getValue("moderation", QString(), QString(), false).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TabsSettings::getTabCardArtRulesOpen() const
|
||||||
|
{
|
||||||
|
return getValue("cardArtRules", QString(), QString(), false).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
void TabsSettings::setTabVisualDeckStorageOpen(bool value)
|
void TabsSettings::setTabVisualDeckStorageOpen(bool value)
|
||||||
{
|
{
|
||||||
setValue(value, "visualDeckStorage");
|
setValue(value, "visualDeckStorage");
|
||||||
|
|
@ -150,3 +155,8 @@ void TabsSettings::setTabModerationOpen(bool value)
|
||||||
{
|
{
|
||||||
setValue(value, "moderation");
|
setValue(value, "moderation");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabsSettings::setTabCardArtRulesOpen(bool value)
|
||||||
|
{
|
||||||
|
setValue(value, "cardArtRules");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public:
|
||||||
[[nodiscard]] bool getTabLogOpen() const override;
|
[[nodiscard]] bool getTabLogOpen() const override;
|
||||||
[[nodiscard]] bool getTabReportOpen() const override;
|
[[nodiscard]] bool getTabReportOpen() const override;
|
||||||
[[nodiscard]] bool getTabModerationOpen() const override;
|
[[nodiscard]] bool getTabModerationOpen() const override;
|
||||||
|
[[nodiscard]] bool getTabCardArtRulesOpen() const override;
|
||||||
|
|
||||||
void setStartupTabIndex(int value);
|
void setStartupTabIndex(int value);
|
||||||
void setStartupServerHost(const QString &host);
|
void setStartupServerHost(const QString &host);
|
||||||
|
|
@ -57,6 +58,7 @@ public:
|
||||||
void setTabLogOpen(bool value);
|
void setTabLogOpen(bool value);
|
||||||
void setTabReportOpen(bool value);
|
void setTabReportOpen(bool value);
|
||||||
void setTabModerationOpen(bool value);
|
void setTabModerationOpen(bool value);
|
||||||
|
void setTabCardArtRulesOpen(bool value);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void startupTabIndexChanged(int index);
|
void startupTabIndexChanged(int index);
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,15 @@ set(CMAKE_AUTOUIC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
set(UTILITY_SOURCES
|
set(UTILITY_SOURCES
|
||||||
libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp libcockatrice/utility/passwordhasher.cpp
|
libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
|
||||||
libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/warning_categories.cpp
|
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp
|
||||||
|
libcockatrice/utility/warning_categories.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(UTILITY_HEADERS
|
set(UTILITY_HEADERS
|
||||||
libcockatrice/utility/card_ref.h
|
libcockatrice/utility/card_ref.h
|
||||||
libcockatrice/utility/color.h
|
libcockatrice/utility/color.h
|
||||||
|
libcockatrice/utility/cryptoutil.h
|
||||||
libcockatrice/utility/expression.h
|
libcockatrice/utility/expression.h
|
||||||
libcockatrice/utility/levenshtein.h
|
libcockatrice/utility/levenshtein.h
|
||||||
libcockatrice/utility/macros.h
|
libcockatrice/utility/macros.h
|
||||||
|
|
@ -32,7 +34,9 @@ add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})
|
||||||
|
|
||||||
target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE})
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
|
target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Crypto ${QT_CORE_MODULE})
|
||||||
|
|
||||||
set(ORACLE_LIBS)
|
set(ORACLE_LIBS)
|
||||||
|
|
||||||
|
|
|
||||||
25
libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp
Normal file
25
libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#include "cryptoutil.h"
|
||||||
|
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
|
namespace CryptoUtil
|
||||||
|
{
|
||||||
|
QByteArray randomBytes(int count)
|
||||||
|
{
|
||||||
|
QByteArray bytes(count, '\0');
|
||||||
|
if (RAND_bytes(reinterpret_cast<unsigned char *>(bytes.data()), count) != 1) {
|
||||||
|
// Randomness failure is fatal: never fall back to a predictable source.
|
||||||
|
qFatal("CryptoUtil::randomBytes: RAND_bytes failed");
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 randomUInt64()
|
||||||
|
{
|
||||||
|
quint64 value;
|
||||||
|
if (RAND_bytes(reinterpret_cast<unsigned char *>(&value), sizeof(value)) != 1) {
|
||||||
|
qFatal("CryptoUtil::randomUInt64: RAND_bytes failed");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} // namespace CryptoUtil
|
||||||
13
libcockatrice_utility/libcockatrice/utility/cryptoutil.h
Normal file
13
libcockatrice_utility/libcockatrice/utility/cryptoutil.h
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#ifndef CRYPTOUTIL_H
|
||||||
|
#define CRYPTOUTIL_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
namespace CryptoUtil
|
||||||
|
{
|
||||||
|
QByteArray randomBytes(int count);
|
||||||
|
quint64 randomUInt64();
|
||||||
|
} // namespace CryptoUtil
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#include "passwordhasher.h"
|
#include "passwordhasher.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <libcockatrice/rng/rng_sfmt.h>
|
#include <libcockatrice/utility/cryptoutil.h>
|
||||||
|
|
||||||
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
|
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
|
||||||
{
|
{
|
||||||
|
|
@ -21,12 +21,28 @@ QString PasswordHasher::generateRandomSalt(const int len)
|
||||||
static const char alphanum[] = "0123456789"
|
static const char alphanum[] = "0123456789"
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
"abcdefghijklmnopqrstuvwxyz";
|
"abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const int size = sizeof(alphanum) - 1;
|
||||||
|
|
||||||
|
// Two bytes per character, corrected for modulo bias via rejection sampling.
|
||||||
|
const int bucketSize = 65536 / size;
|
||||||
|
const int limit = bucketSize * size;
|
||||||
|
|
||||||
QString ret;
|
QString ret;
|
||||||
int size = sizeof(alphanum) - 1;
|
ret.reserve(len);
|
||||||
|
QByteArray random = CryptoUtil::randomBytes(len * 2);
|
||||||
|
int bytesUsed = 0;
|
||||||
for (int i = 0; i < len; ++i) {
|
for (int i = 0; i < len; ++i) {
|
||||||
ret.append(alphanum[rng->rand(0, size)]);
|
unsigned int value;
|
||||||
|
do {
|
||||||
|
if (bytesUsed >= random.size()) {
|
||||||
|
random = CryptoUtil::randomBytes(len * 2);
|
||||||
|
bytesUsed = 0;
|
||||||
|
}
|
||||||
|
value = static_cast<unsigned int>(static_cast<unsigned char>(random.at(bytesUsed))) << 8 |
|
||||||
|
static_cast<unsigned int>(static_cast<unsigned char>(random.at(bytesUsed + 1)));
|
||||||
|
bytesUsed += 2;
|
||||||
|
} while (value >= limit);
|
||||||
|
ret.append(alphanum[value / bucketSize]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
@ -34,5 +50,5 @@ QString PasswordHasher::generateRandomSalt(const int len)
|
||||||
|
|
||||||
QString PasswordHasher::generateActivationToken()
|
QString PasswordHasher::generateActivationToken()
|
||||||
{
|
{
|
||||||
return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16);
|
return QString(CryptoUtil::randomBytes(16).toBase64().left(16));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ set(oracle_SOURCES
|
||||||
src/pages.cpp
|
src/pages.cpp
|
||||||
src/pagetemplates.cpp
|
src/pagetemplates.cpp
|
||||||
src/parsehelpers.cpp
|
src/parsehelpers.cpp
|
||||||
src/qt-json/json.cpp
|
src/raw_json_scanner.cpp
|
||||||
../cockatrice/src/client/settings/cache_settings.cpp
|
../cockatrice/src/client/settings/cache_settings.cpp
|
||||||
../cockatrice/src/client/settings/card_counter_settings.cpp
|
../cockatrice/src/client/settings/card_counter_settings.cpp
|
||||||
../cockatrice/src/client/settings/shortcuts_settings.cpp
|
../cockatrice/src/client/settings/shortcuts_settings.cpp
|
||||||
|
|
@ -112,6 +112,8 @@ qt6_add_executable(
|
||||||
MANUAL_FINALIZATION
|
MANUAL_FINALIZATION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(oracle PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Link libraries
|
# Link libraries
|
||||||
# ------------------------
|
# ------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
|
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
|
||||||
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
|
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
|
||||||
#include "parsehelpers.h"
|
#include "parsehelpers.h"
|
||||||
#include "qt-json/json.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QSet>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||||
|
|
@ -42,31 +45,29 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s
|
||||||
return priority;
|
return priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
|
bool OracleImporter::readSetsFromByteArray(QByteArray data)
|
||||||
{
|
{
|
||||||
bool ok;
|
RawJson::ScanError error;
|
||||||
auto setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap();
|
const QList<RawJson::SetRange> ranges = RawJson::scanSetRanges(data, &error);
|
||||||
if (!ok) {
|
if (error.isError()) {
|
||||||
qDebug() << "error: QtJson::Json::parse()";
|
qDebug() << "error: RawJson::scanSetRanges():" << error.message;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<SetToDownload> newSetList;
|
QList<SetToDownload> newSetList;
|
||||||
|
newSetList.reserve(ranges.size());
|
||||||
|
|
||||||
QListIterator it(setsMap.values());
|
for (const RawJson::SetRange &range : ranges) {
|
||||||
|
QString shortName = range.code.toUpper();
|
||||||
while (it.hasNext()) {
|
QString longName = range.name;
|
||||||
QVariantMap map = it.next().toMap();
|
QString setType = range.type;
|
||||||
QString shortName = map.value("code").toString().toUpper();
|
QDate releaseDate = QDate::fromString(range.releaseDate, Qt::ISODate);
|
||||||
QString longName = map.value("name").toString();
|
|
||||||
QList<QVariant> setCards = map.value("cards").toList();
|
|
||||||
QString setType = map.value("type").toString();
|
|
||||||
QDate releaseDate = map.value("releaseDate").toDate();
|
|
||||||
CardSet::Priority priority = getSetPriority(setType, shortName);
|
CardSet::Priority priority = getSetPriority(setType, shortName);
|
||||||
// capitalize set type
|
// capitalize set type
|
||||||
if (setType.length() > 0) {
|
if (setType.length() > 0) {
|
||||||
// basic grammar for words that aren't capitalized, like in "From the Vault"
|
// basic grammar for words that aren't capitalized, like in "From the Vault"
|
||||||
const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"};
|
static const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for",
|
||||||
|
"of", "in", "and", "with", "or"};
|
||||||
QStringList words = setType.split("_");
|
QStringList words = setType.split("_");
|
||||||
setType.clear();
|
setType.clear();
|
||||||
bool first = false;
|
bool first = false;
|
||||||
|
|
@ -74,13 +75,15 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
|
||||||
if (first && noCapitalize.contains(item)) {
|
if (first && noCapitalize.contains(item)) {
|
||||||
setType += item + QString(" ");
|
setType += item + QString(" ");
|
||||||
} else {
|
} else {
|
||||||
setType += item[0].toUpper() + item.mid(1, -1) + QString(" ");
|
setType += item[0].toUpper() + item.mid(1) + QString(" ");
|
||||||
first = true;
|
first = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setType = setType.trimmed();
|
setType = setType.trimmed();
|
||||||
}
|
}
|
||||||
newSetList.append(SetToDownload(shortName, longName, setCards, priority, setType, releaseDate));
|
SetToDownload set(shortName, longName, priority, setType, releaseDate);
|
||||||
|
set.setRawRange(range.dataRange);
|
||||||
|
newSetList.append(set);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(newSetList.begin(), newSetList.end());
|
std::sort(newSetList.begin(), newSetList.end());
|
||||||
|
|
@ -89,6 +92,7 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
allSets = newSetList;
|
allSets = newSetList;
|
||||||
|
rawSetsData = std::move(data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,14 +126,8 @@ static void sortAndReduceColors(QString &colors)
|
||||||
std::sort(colors.begin(), colors.end(),
|
std::sort(colors.begin(), colors.end(),
|
||||||
[](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); });
|
[](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); });
|
||||||
// reduce
|
// reduce
|
||||||
QChar lastChar = '\0';
|
auto last = std::unique(colors.begin(), colors.end());
|
||||||
for (int i = 0; i < colors.size(); ++i) {
|
colors.erase(last, colors.end());
|
||||||
if (colors.at(i) == lastChar) {
|
|
||||||
colors.remove(i, 1);
|
|
||||||
} else {
|
|
||||||
lastChar = colors.at(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CardInfoPtr OracleImporter::addCard(QString name,
|
CardInfoPtr OracleImporter::addCard(QString name,
|
||||||
|
|
@ -142,9 +140,12 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
||||||
// Workaround for card name weirdness
|
// Workaround for card name weirdness
|
||||||
name = name.replace("Æ", "AE");
|
name = name.replace("Æ", "AE");
|
||||||
name = name.replace("’", "'");
|
name = name.replace("’", "'");
|
||||||
if (cards.contains(name)) {
|
auto existingIt = cards.constFind(name);
|
||||||
CardInfoPtr card = cards.value(name);
|
if (existingIt != cards.constEnd()) {
|
||||||
|
CardInfoPtr card = existingIt.value();
|
||||||
card->addToSet(printingInfo.getSet(), printingInfo);
|
card->addToSet(printingInfo.getSet(), printingInfo);
|
||||||
|
// Only merge legalities when the card has none yet, so multi-format
|
||||||
|
// printings don't overwrite each other's legality lists.
|
||||||
if (card->getProperties().filter(formatRegex).empty()) {
|
if (card->getProperties().filter(formatRegex).empty()) {
|
||||||
card->combineLegalities(properties);
|
card->combineLegalities(properties);
|
||||||
}
|
}
|
||||||
|
|
@ -182,8 +183,9 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
||||||
|
|
||||||
// DETECT CARD POSITIONING INFO
|
// DETECT CARD POSITIONING INFO
|
||||||
|
|
||||||
bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" ||
|
QString layoutVal = properties.value("layout");
|
||||||
properties.value("layout") == "planar";
|
bool landscapeOrientation =
|
||||||
|
properties.value("maintype") == "Battle" || layoutVal == "split" || layoutVal == "planar";
|
||||||
|
|
||||||
// cards that enter the field tapped
|
// cards that enter the field tapped
|
||||||
bool cipt = parseCipt(name, text) || landscapeOrientation;
|
bool cipt = parseCipt(name, text) || landscapeOrientation;
|
||||||
|
|
@ -222,12 +224,15 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
||||||
return newCard;
|
return newCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName)
|
static QString getJsonString(const QJsonObject &obj, const QString &key)
|
||||||
{
|
{
|
||||||
return card.contains(propertyName) ? card.value(propertyName).toString() : QString("");
|
// QVariant coerces numbers and booleans to text, while QJsonValue::toString()
|
||||||
|
// returns a null string for them — some MTGJSON fields (manaValue,
|
||||||
|
// convertedManaCost, isOnlineOnly, isRebalanced) carry those types.
|
||||||
|
return obj.value(key).toVariant().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList<QVariant> &cardsList)
|
int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList)
|
||||||
{
|
{
|
||||||
// mtgjson name => xml name
|
// mtgjson name => xml name
|
||||||
static const QMap<QString, QString> cardProperties{
|
static const QMap<QString, QString> cardProperties{
|
||||||
|
|
@ -248,7 +253,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
|
|
||||||
static const QString ptSeparator = "/";
|
static const QString ptSeparator = "/";
|
||||||
static constexpr bool isToken = false;
|
static constexpr bool isToken = false;
|
||||||
static const QList<QString> setsWithCardsWithSameNameButDifferentText = {"UST"};
|
static const QSet<QString> setsWithCardsWithSameNameButDifferentText = {"UST"};
|
||||||
|
|
||||||
int numCards = 0;
|
int numCards = 0;
|
||||||
|
|
||||||
|
|
@ -256,16 +261,16 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
QMap<QString, QPair<QList<SplitCardPart>, QString>> splitCards;
|
QMap<QString, QPair<QList<SplitCardPart>, QString>> splitCards;
|
||||||
|
|
||||||
// Keeps track of all names encountered so far
|
// Keeps track of all names encountered so far
|
||||||
QList<QString> allNameProps;
|
QSet<QString> allNameProps;
|
||||||
|
|
||||||
for (const QVariant &cardVar : cardsList) {
|
for (const QJsonValue &cardVal : cardsList) {
|
||||||
QVariantMap card = cardVar.toMap();
|
QJsonObject card = cardVal.toObject();
|
||||||
|
|
||||||
/* Currently used layouts are:
|
/* Currently used layouts are:
|
||||||
* augment, double_faced_token, flip, host, leveler, meld, normal, planar,
|
* augment, double_faced_token, flip, host, leveler, meld, normal, planar,
|
||||||
* saga, scheme, split, token, transform, vanguard
|
* saga, scheme, split, token, transform, vanguard
|
||||||
*/
|
*/
|
||||||
QString layout = getStringPropertyFromMap(card, "layout");
|
QString layout = getJsonString(card, "layout");
|
||||||
|
|
||||||
// don't import tokens from the json file
|
// don't import tokens from the json file
|
||||||
if (layout == "token") {
|
if (layout == "token") {
|
||||||
|
|
@ -273,9 +278,9 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
}
|
}
|
||||||
|
|
||||||
// normal cards handling
|
// normal cards handling
|
||||||
QString name = getStringPropertyFromMap(card, "name");
|
QString name = getJsonString(card, "name");
|
||||||
QString text = getStringPropertyFromMap(card, "text");
|
QString text = getJsonString(card, "text");
|
||||||
QString faceName = getStringPropertyFromMap(card, "faceName");
|
QString faceName = getJsonString(card, "faceName");
|
||||||
if (faceName.isEmpty()) {
|
if (faceName.isEmpty()) {
|
||||||
faceName = name;
|
faceName = name;
|
||||||
}
|
}
|
||||||
|
|
@ -283,39 +288,34 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
// card properties
|
// card properties
|
||||||
QHash<QString, QString> properties;
|
QHash<QString, QString> properties;
|
||||||
for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) {
|
for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) {
|
||||||
QString mtgjsonProperty = i.key();
|
QString propertyValue = getJsonString(card, i.key());
|
||||||
QString xmlPropertyName = i.value();
|
|
||||||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
|
||||||
if (!propertyValue.isEmpty()) {
|
if (!propertyValue.isEmpty()) {
|
||||||
properties.insert(xmlPropertyName, propertyValue);
|
properties.insert(i.value(), propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// per-set properties
|
// per-set properties
|
||||||
QHash<QString, QString> printingProps;
|
QHash<QString, QString> printingProps;
|
||||||
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
||||||
QString mtgjsonProperty = i.key();
|
QString propertyValue = getJsonString(card, i.key());
|
||||||
QString xmlPropertyName = i.value();
|
|
||||||
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
|
|
||||||
if (!propertyValue.isEmpty()) {
|
if (!propertyValue.isEmpty()) {
|
||||||
printingProps.insert(xmlPropertyName, propertyValue);
|
printingProps.insert(i.value(), propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle flavorNames specially due to double-faced cards
|
// handle flavorNames specially due to double-faced cards
|
||||||
QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName");
|
QString faceFlavorName = getJsonString(card, "faceFlavorName");
|
||||||
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName");
|
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getJsonString(card, "flavorName");
|
||||||
if (!flavorName.isEmpty()) {
|
if (!flavorName.isEmpty()) {
|
||||||
printingProps.insert("flavorName", flavorName);
|
printingProps.insert("flavorName", flavorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identifiers
|
// Identifiers
|
||||||
|
QJsonObject identifiers = card.value("identifiers").toObject();
|
||||||
for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) {
|
for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) {
|
||||||
QString mtgjsonProperty = i.key();
|
QString propertyValue = getJsonString(identifiers, i.key());
|
||||||
QString xmlPropertyName = i.value();
|
|
||||||
QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
|
|
||||||
if (!propertyValue.isEmpty()) {
|
if (!propertyValue.isEmpty()) {
|
||||||
printingProps.insert(xmlPropertyName, propertyValue);
|
printingProps.insert(i.value(), propertyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,21 +331,26 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
allNameProps.contains(faceName) && layout == "normal" && lastChar.isLetter()) {
|
allNameProps.contains(faceName) && layout == "normal" && lastChar.isLetter()) {
|
||||||
numComponent = " (" + QString(lastChar).toLower() + ")";
|
numComponent = " (" + QString(lastChar).toLower() + ")";
|
||||||
}
|
}
|
||||||
allNameProps.append(faceName);
|
allNameProps.insert(faceName);
|
||||||
|
|
||||||
// special handling properties
|
// special handling properties
|
||||||
QString colors = card.value("colors").toStringList().join("");
|
QString colors;
|
||||||
|
for (const QJsonValue &color : card.value("colors").toArray()) {
|
||||||
|
colors += color.toString();
|
||||||
|
}
|
||||||
if (!colors.isEmpty()) {
|
if (!colors.isEmpty()) {
|
||||||
properties.insert("colors", colors);
|
properties.insert("colors", colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// special handling properties
|
QString colorIdentity;
|
||||||
QString colorIdentity = card.value("colorIdentity").toStringList().join("");
|
for (const QJsonValue &color : card.value("colorIdentity").toArray()) {
|
||||||
|
colorIdentity += color.toString();
|
||||||
|
}
|
||||||
if (!colorIdentity.isEmpty()) {
|
if (!colorIdentity.isEmpty()) {
|
||||||
properties.insert("coloridentity", colorIdentity);
|
properties.insert("coloridentity", colorIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &mainCardType = getMainCardType(card.value("types").toStringList());
|
const auto &mainCardType = getMainCardType(card.value("types").toVariant().toStringList());
|
||||||
if (mainCardType.isEmpty()) {
|
if (mainCardType.isEmpty()) {
|
||||||
qDebug() << "warning: no mainCardType for card:" << name;
|
qDebug() << "warning: no mainCardType for card:" << name;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -354,22 +359,22 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
|
|
||||||
// Depending on whether power and/or toughness are present, the format
|
// Depending on whether power and/or toughness are present, the format
|
||||||
// is either P/T (most common), P (no toughness), or /T (no power).
|
// is either P/T (most common), P (no toughness), or /T (no power).
|
||||||
QString power = getStringPropertyFromMap(card, "power");
|
QString power = getJsonString(card, "power");
|
||||||
QString toughness = getStringPropertyFromMap(card, "toughness");
|
QString toughness = getJsonString(card, "toughness");
|
||||||
if (toughness.isEmpty() && !power.isEmpty()) {
|
if (toughness.isEmpty() && !power.isEmpty()) {
|
||||||
properties.insert("pt", power);
|
properties.insert("pt", power);
|
||||||
} else if (!toughness.isEmpty()) {
|
} else if (!toughness.isEmpty()) {
|
||||||
properties.insert("pt", power + ptSeparator + toughness);
|
properties.insert("pt", power + ptSeparator + toughness);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto legalities = card.value("legalities").toMap();
|
auto legalities = card.value("legalities").toObject();
|
||||||
for (auto i = legalities.cbegin(), end = legalities.cend(); i != end; ++i) {
|
for (auto i = legalities.constBegin(), end = legalities.constEnd(); i != end; ++i) {
|
||||||
properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower());
|
properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
// split cards are considered a single card, enqueue for later merging
|
// split cards are considered a single card, enqueue for later merging
|
||||||
if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") {
|
if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") {
|
||||||
auto _faceName = getStringPropertyFromMap(card, "faceName");
|
auto _faceName = getJsonString(card, "faceName");
|
||||||
SplitCardPart split(_faceName, text, properties, printingInfo);
|
SplitCardPart split(_faceName, text, properties, printingInfo);
|
||||||
auto found_iter = splitCards.find(name + numProperty);
|
auto found_iter = splitCards.find(name + numProperty);
|
||||||
if (found_iter == splitCards.end()) {
|
if (found_iter == splitCards.end()) {
|
||||||
|
|
@ -382,11 +387,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
QList<CardRelation *> relatedCards;
|
QList<CardRelation *> relatedCards;
|
||||||
|
|
||||||
// add other face for split cards as card relation
|
// add other face for split cards as card relation
|
||||||
if (!getStringPropertyFromMap(card, "side").isEmpty()) {
|
if (!getJsonString(card, "side").isEmpty()) {
|
||||||
auto faceManaValue = getStringPropertyFromMap(card, "faceManaValue");
|
auto faceManaValue = getJsonString(card, "faceManaValue");
|
||||||
if (faceManaValue.isEmpty()) {
|
if (faceManaValue.isEmpty()) {
|
||||||
// check the old name for the property, for backwards compatibility purposes
|
// check the old name for the property, for backwards compatibility purposes
|
||||||
faceManaValue = getStringPropertyFromMap(card, "faceConvertedManaCost");
|
faceManaValue = getJsonString(card, "faceConvertedManaCost");
|
||||||
}
|
}
|
||||||
properties["cmc"] = faceManaValue;
|
properties["cmc"] = faceManaValue;
|
||||||
|
|
||||||
|
|
@ -406,20 +411,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
name = faceName;
|
name = faceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mtgjon related cards
|
// mtgjson related cards
|
||||||
if (card.contains("relatedCards")) {
|
QJsonObject givenRelated = card.value("relatedCards").toObject();
|
||||||
QVariantMap givenRelated = card.value("relatedCards").toMap();
|
if (!givenRelated.isEmpty()) {
|
||||||
// conjured cards from a spellbook
|
// conjured cards from a spellbook
|
||||||
if (givenRelated.contains("spellbook")) {
|
QJsonArray spellbook = givenRelated.value("spellbook").toArray();
|
||||||
auto spbk = givenRelated.value("spellbook").toStringList();
|
if (!spellbook.isEmpty()) {
|
||||||
for (const QString &spbkName : spbk) {
|
for (const QJsonValue &spbkVal : spellbook) {
|
||||||
relatedCards.append(
|
relatedCards.append(new CardRelation(spbkVal.toString(), CardRelationType::DoesNotAttach, false,
|
||||||
new CardRelation(spbkName, CardRelationType::DoesNotAttach, false, false, 1, true));
|
false, 1, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, printingInfo);
|
CardInfoPtr newCard =
|
||||||
|
addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo);
|
||||||
numCards++;
|
numCards++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -427,7 +433,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
// split cards handling
|
// split cards handling
|
||||||
static const QString splitCardPropSeparator = QString(" // ");
|
static const QString splitCardPropSeparator = QString(" // ");
|
||||||
static const QString splitCardTextSeparator = QString("\n\n---\n\n");
|
static const QString splitCardTextSeparator = QString("\n\n---\n\n");
|
||||||
static const QList<CardRelation *> noRelatedCards = {};
|
|
||||||
|
|
||||||
QList<QPair<QList<SplitCardPart>, QString>> partsAndNames = splitCards.values();
|
QList<QPair<QList<SplitCardPart>, QString>> partsAndNames = splitCards.values();
|
||||||
for (auto [splitCardParts, name] : partsAndNames) {
|
for (auto [splitCardParts, name] : partsAndNames) {
|
||||||
|
|
@ -453,7 +458,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) {
|
if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) {
|
||||||
if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty
|
if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty
|
||||||
properties.insert(prop, thisCardPropertyValue);
|
properties.insert(prop, thisCardPropertyValue);
|
||||||
} else if (prop == "colors") { // the card is both colors
|
} else if (prop == "colors" || prop == "coloridentity") { // the card is both colors
|
||||||
properties.insert(prop, originalPropertyValue + thisCardPropertyValue);
|
properties.insert(prop, originalPropertyValue + thisCardPropertyValue);
|
||||||
} else if (prop == "maintype") { // don't create maintypes with //es in them
|
} else if (prop == "maintype") { // don't create maintypes with //es in them
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -465,20 +470,20 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CardInfoPtr newCard = addCard(name, text, isToken, properties, noRelatedCards, printingInfo);
|
CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo);
|
||||||
numCards++;
|
numCards++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return numCards;
|
return numCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
|
static FormatRulesNameMap buildDefaultMagicFormats()
|
||||||
{
|
{
|
||||||
// Predefined common exceptions
|
// Predefined common exceptions
|
||||||
CardCondition superTypeIsBasic;
|
CardCondition superTypeIsBasic;
|
||||||
superTypeIsBasic.field = "type";
|
superTypeIsBasic.field = "type";
|
||||||
superTypeIsBasic.matchType = "regex";
|
superTypeIsBasic.matchType = "regex";
|
||||||
superTypeIsBasic.value = "\bBasic\b[^—]+\bLand\b";
|
superTypeIsBasic.value = R"(\bBasic\b[^—]+\bLand\b)";
|
||||||
|
|
||||||
ExceptionRule basicLands;
|
ExceptionRule basicLands;
|
||||||
basicLands.conditions.append(superTypeIsBasic);
|
basicLands.conditions.append(superTypeIsBasic);
|
||||||
|
|
@ -491,7 +496,6 @@ FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
|
||||||
ExceptionRule mayContainAnyNumber;
|
ExceptionRule mayContainAnyNumber;
|
||||||
mayContainAnyNumber.conditions.append(anyNumberAllowed);
|
mayContainAnyNumber.conditions.append(anyNumberAllowed);
|
||||||
|
|
||||||
// Map to store default rules
|
|
||||||
FormatRulesNameMap defaultFormatRulesNameMap;
|
FormatRulesNameMap defaultFormatRulesNameMap;
|
||||||
|
|
||||||
// ----------------- Helper lambda to create format -----------------
|
// ----------------- Helper lambda to create format -----------------
|
||||||
|
|
@ -537,10 +541,27 @@ FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
|
||||||
return defaultFormatRulesNameMap;
|
return defaultFormatRulesNameMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FormatRulesNameMap &OracleImporter::createDefaultMagicFormats()
|
||||||
|
{
|
||||||
|
static const FormatRulesNameMap cached = buildDefaultMagicFormats();
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
int OracleImporter::startImport()
|
int OracleImporter::startImport()
|
||||||
{
|
{
|
||||||
static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController();
|
static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController();
|
||||||
|
|
||||||
|
// Pre-allocate the cards hash to avoid rehashing during import. Keys are
|
||||||
|
// distinct card names while raw ranges only count printings (AllPrintings
|
||||||
|
// ~100k printings vs ~35k names), so this over-reserves somewhat; an exact
|
||||||
|
// distinct-name count would require eagerly parsing, which the lazy reader
|
||||||
|
// deliberately avoids. It's a capacity hint, so the overshoot is harmless.
|
||||||
|
int estimatedCards = 0;
|
||||||
|
for (const SetToDownload &curSetToParse : allSets) {
|
||||||
|
estimatedCards += curSetToParse.getRawRange().cardCount;
|
||||||
|
}
|
||||||
|
cards.reserve(estimatedCards);
|
||||||
|
|
||||||
// add an empty set for tokens
|
// add an empty set for tokens
|
||||||
CardSetPtr tokenSet =
|
CardSetPtr tokenSet =
|
||||||
CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens");
|
CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens");
|
||||||
|
|
@ -552,11 +573,44 @@ int OracleImporter::startImport()
|
||||||
CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(),
|
CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(),
|
||||||
curSetToParse.getLongName(), curSetToParse.getSetType(),
|
curSetToParse.getLongName(), curSetToParse.getSetType(),
|
||||||
curSetToParse.getReleaseDate(), curSetToParse.getPriority());
|
curSetToParse.getReleaseDate(), curSetToParse.getPriority());
|
||||||
|
|
||||||
|
// parse only this set's slice of the raw document so the whole JSON tree is
|
||||||
|
// never kept in memory at once
|
||||||
|
const RawJson::SetDataRange &rawRange = curSetToParse.getRawRange();
|
||||||
|
const qsizetype rangeEnd = rawRange.start + rawRange.length;
|
||||||
|
if (rawRange.start < 0 || rawRange.length <= 0 || rangeEnd > rawSetsData.size()) {
|
||||||
|
// rawSetsData is cleared by releaseSetData() while SetToDownload copies
|
||||||
|
// taken from getSets() keep their ranges, and nothing else enforces the
|
||||||
|
// pairing — so never index past the buffer on stale/mismatched ranges.
|
||||||
|
qWarning() << "error: out-of-bounds raw range for set" << curSetToParse.getShortName() << "skipping";
|
||||||
|
++setIndex;
|
||||||
|
emit setIndexChanged(0, setIndex, curSetToParse.getLongName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// sliced() shares the buffer instead of deep-copying the slice; the largest
|
||||||
|
// sets in AllPrintings are tens of MB, so the copy is worth avoiding here.
|
||||||
|
const QByteArray setBytes = rawSetsData.sliced(rawRange.start, rawRange.length);
|
||||||
|
QJsonParseError parseError;
|
||||||
|
const QJsonDocument setDoc = QJsonDocument::fromJson(setBytes, &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << "error: parsing card data for set" << curSetToParse.getShortName() << ":"
|
||||||
|
<< parseError.errorString();
|
||||||
|
++setIndex;
|
||||||
|
// Keep the progress accounting honest: a set that failed to parse
|
||||||
|
// still advanced the index, so report it (with zero imported cards)
|
||||||
|
// rather than letting SaveSetsPage's bar stall per failed set.
|
||||||
|
emit setIndexChanged(0, setIndex, curSetToParse.getLongName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add the set to the database once its slice parsed cleanly;
|
||||||
|
// a set that fails here must not persist as an empty set in cards.xml.
|
||||||
if (!sets.contains(newSet->getShortName())) {
|
if (!sets.contains(newSet->getShortName())) {
|
||||||
sets.insert(newSet->getShortName(), newSet);
|
sets.insert(newSet->getShortName(), newSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards());
|
const QJsonArray setCards = setDoc.object().value("cards").toArray();
|
||||||
|
int numCardsInSet = importCardsFromSet(newSet, setCards);
|
||||||
|
|
||||||
++setIndex;
|
++setIndex;
|
||||||
|
|
||||||
|
|
@ -576,9 +630,16 @@ bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUr
|
||||||
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion);
|
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OracleImporter::releaseSetData()
|
||||||
|
{
|
||||||
|
allSets.clear();
|
||||||
|
rawSetsData.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void OracleImporter::clear()
|
void OracleImporter::clear()
|
||||||
{
|
{
|
||||||
sets.clear();
|
sets.clear();
|
||||||
cards.clear();
|
cards.clear();
|
||||||
allSets.clear();
|
allSets.clear();
|
||||||
|
rawSetsData.clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
#ifndef ORACLEIMPORTER_H
|
#ifndef ORACLEIMPORTER_H
|
||||||
#define ORACLEIMPORTER_H
|
#define ORACLEIMPORTER_H
|
||||||
|
|
||||||
|
#include "raw_json_scanner.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
@ -44,10 +49,12 @@ class SetToDownload
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
QString shortName, longName;
|
QString shortName, longName;
|
||||||
QList<QVariant> cards;
|
|
||||||
QDate releaseDate;
|
QDate releaseDate;
|
||||||
QString setType;
|
QString setType;
|
||||||
CardSet::Priority priority;
|
CardSet::Priority priority;
|
||||||
|
// Byte range of this set's object within the importer's raw JSON text. Parsing
|
||||||
|
// one set at a time keeps peak memory low instead of holding the whole document.
|
||||||
|
RawJson::SetDataRange rawRange;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
const QString &getShortName() const
|
const QString &getShortName() const
|
||||||
|
|
@ -58,10 +65,6 @@ public:
|
||||||
{
|
{
|
||||||
return longName;
|
return longName;
|
||||||
}
|
}
|
||||||
const QList<QVariant> &getCards() const
|
|
||||||
{
|
|
||||||
return cards;
|
|
||||||
}
|
|
||||||
const QString &getSetType() const
|
const QString &getSetType() const
|
||||||
{
|
{
|
||||||
return setType;
|
return setType;
|
||||||
|
|
@ -74,16 +77,23 @@ public:
|
||||||
{
|
{
|
||||||
return priority;
|
return priority;
|
||||||
}
|
}
|
||||||
|
const RawJson::SetDataRange &getRawRange() const
|
||||||
|
{
|
||||||
|
return rawRange;
|
||||||
|
}
|
||||||
SetToDownload(QString _shortName,
|
SetToDownload(QString _shortName,
|
||||||
QString _longName,
|
QString _longName,
|
||||||
QList<QVariant> _cards,
|
|
||||||
CardSet::Priority _priority,
|
CardSet::Priority _priority,
|
||||||
QString _setType = QString(),
|
QString _setType = QString(),
|
||||||
const QDate &_releaseDate = QDate())
|
const QDate &_releaseDate = QDate())
|
||||||
: shortName(std::move(_shortName)), longName(std::move(_longName)), cards(std::move(_cards)),
|
: shortName(std::move(_shortName)), longName(std::move(_longName)), releaseDate(_releaseDate),
|
||||||
releaseDate(_releaseDate), setType(std::move(_setType)), priority(_priority)
|
setType(std::move(_setType)), priority(_priority)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
void setRawRange(const RawJson::SetDataRange &_rawRange)
|
||||||
|
{
|
||||||
|
rawRange = _rawRange;
|
||||||
|
}
|
||||||
bool operator<(const SetToDownload &set) const
|
bool operator<(const SetToDownload &set) const
|
||||||
{
|
{
|
||||||
return longName.compare(set.longName, Qt::CaseInsensitive) < 0;
|
return longName.compare(set.longName, Qt::CaseInsensitive) < 0;
|
||||||
|
|
@ -139,6 +149,12 @@ private:
|
||||||
|
|
||||||
QList<SetToDownload> allSets;
|
QList<SetToDownload> allSets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw JSON text of the source document, retained for lazy per-set
|
||||||
|
* parsing during startImport(). Frees the card data as each set is imported.
|
||||||
|
*/
|
||||||
|
QByteArray rawSetsData;
|
||||||
|
|
||||||
CardInfoPtr addCard(QString name,
|
CardInfoPtr addCard(QString name,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
bool isToken,
|
bool isToken,
|
||||||
|
|
@ -151,11 +167,18 @@ signals:
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit OracleImporter(QObject *parent = nullptr);
|
explicit OracleImporter(QObject *parent = nullptr);
|
||||||
bool readSetsFromByteArray(const QByteArray &data);
|
/**
|
||||||
|
* Scans the given JSON document for set metadata. Takes the data by value so
|
||||||
|
* the wizard can hand over its decompressed buffer without copying it.
|
||||||
|
*/
|
||||||
|
bool readSetsFromByteArray(QByteArray data);
|
||||||
int startImport();
|
int startImport();
|
||||||
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
||||||
int importCardsFromSet(const CardSetPtr ¤tSet, const QList<QVariant> &cardsList);
|
int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList);
|
||||||
FormatRulesNameMap createDefaultMagicFormats();
|
/**
|
||||||
|
* @brief Returns the default format rules. The result is memoized on first use and must be treated as immutable.
|
||||||
|
*/
|
||||||
|
const FormatRulesNameMap &createDefaultMagicFormats();
|
||||||
const CardNameMap &getCardList() const
|
const CardNameMap &getCardList() const
|
||||||
{
|
{
|
||||||
return cards;
|
return cards;
|
||||||
|
|
@ -164,6 +187,11 @@ public:
|
||||||
{
|
{
|
||||||
return allSets;
|
return allSets;
|
||||||
}
|
}
|
||||||
|
const QByteArray &getRawSetsData() const
|
||||||
|
{
|
||||||
|
return rawSetsData;
|
||||||
|
}
|
||||||
|
void releaseSetData();
|
||||||
void clear();
|
void clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -560,6 +560,9 @@ void SaveSetsPage::initializePage()
|
||||||
|
|
||||||
int setsImported = wizard()->importer->startImport();
|
int setsImported = wizard()->importer->startImport();
|
||||||
|
|
||||||
|
// JSON data no longer needed after CardInfo objects are built
|
||||||
|
wizard()->importer->releaseSetData();
|
||||||
|
|
||||||
if (setsImported == 0) {
|
if (setsImported == 0) {
|
||||||
QMessageBox::critical(this, tr("Error"), tr("No set has been imported."));
|
QMessageBox::critical(this, tr("Error"), tr("No set has been imported."));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
Eeli Reilin <eeli@emicode.fi>
|
|
||||||
Luis Gustavo S. Barreto <gustavosbarreto@gmail.com>
|
|
||||||
Stephen Kockentiedt <Stephen@Kockentiedt.name>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
Copyright 2011 Eeli Reilin. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR
|
|
||||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
|
||||||
EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
||||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
|
||||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
||||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
The views and conclusions contained in the software and documentation
|
|
||||||
are those of the authors and should not be interpreted as representing
|
|
||||||
official policies, either expressed or implied, of Eeli Reilin.
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue