Compare commits
98 Commits
oracle-mem
...
2026-01-14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c075deeb2d | ||
|
|
29f60c4a67 | ||
|
|
c553e15036 | ||
|
|
a4eef648bc | ||
|
|
47720ff286 | ||
|
|
289b139be9 | ||
|
|
21d60ec3f1 | ||
|
|
ed1115f4c0 | ||
|
|
cc5e2ab10a | ||
|
|
b19312be70 | ||
|
|
a0d1359860 | ||
|
|
52547bbfe8 | ||
|
|
9ab398f08d | ||
|
|
0deaa9d9b4 | ||
|
|
7c7755b61d | ||
|
|
6340c4a6b7 | ||
|
|
0a2fdb05ad | ||
|
|
b86853b65c | ||
|
|
192dac0396 | ||
|
|
85c9d8a9ff | ||
|
|
ee2699413c | ||
|
|
d50297bbe6 | ||
|
|
489ce416c3 | ||
|
|
731c487ccb | ||
|
|
2d5e8deb75 | ||
|
|
746f2af044 | ||
|
|
f16c552d97 | ||
|
|
72a85b58cf | ||
|
|
b88a98b09a | ||
|
|
4fbb9d9682 | ||
|
|
84aefda486 | ||
|
|
73cc0541f5 | ||
|
|
bcf3939fee | ||
|
|
2e6f1128bb | ||
|
|
bbd8671e6e | ||
|
|
84e6907fa9 | ||
|
|
93a4647b04 | ||
|
|
c1f93b37ab | ||
|
|
e7a1f86cbb | ||
|
|
987fe9c9e2 | ||
|
|
df9a8b2272 | ||
|
|
36d8280765 | ||
|
|
28c800dd37 | ||
|
|
9f90de2242 | ||
|
|
b2dd8eed3f | ||
|
|
0085015ebe | ||
|
|
db3bdb586b | ||
|
|
d722b2569c | ||
|
|
968be8a06f | ||
|
|
daa7db7ce3 | ||
|
|
cb2cf31cec | ||
|
|
ce4a3bf118 | ||
|
|
9d0bb0d51a | ||
|
|
296866a675 | ||
|
|
96c82a0377 | ||
|
|
ca3f6bba02 | ||
|
|
70f9982c29 | ||
|
|
521046fb09 | ||
|
|
421d6b334a | ||
|
|
e7af1bbec9 | ||
|
|
01e8e4d589 | ||
|
|
be17ee1902 | ||
|
|
e557ae0f2a | ||
|
|
e80f13b78e | ||
|
|
c12f4e9d2a | ||
|
|
a0f977e80c | ||
|
|
73a90bdf38 | ||
|
|
7f1d891e26 | ||
|
|
d6db21419c | ||
|
|
367507e054 | ||
|
|
715ee1d6fe | ||
|
|
ad06a81765 | ||
|
|
ebb02b27b2 | ||
|
|
d47dc35885 | ||
|
|
41aca8467a | ||
|
|
cd44392866 | ||
|
|
64bb5355ff | ||
|
|
1198db8891 | ||
|
|
9471adb4f7 | ||
|
|
b29909bdbe | ||
|
|
589e9a15a6 | ||
|
|
c218a66bcd | ||
|
|
8485bbe575 | ||
|
|
5d9d7d3aa5 | ||
|
|
ccdda39e78 | ||
|
|
2e2682aad4 | ||
|
|
a390c8ada7 | ||
|
|
da70344547 | ||
|
|
2b690f8c87 | ||
|
|
c8b419888a | ||
|
|
d3302d521f | ||
|
|
5c1bb27d5c | ||
|
|
dde36183ce | ||
|
|
7c7f2dd8d5 | ||
|
|
edb0a954e2 | ||
|
|
0a239712dd | ||
|
|
95c3434205 | ||
|
|
f0be6972cc |
@@ -122,7 +122,7 @@ if [[ $MAKE_SERVER ]]; then
|
||||
flags+=("-DWITH_SERVER=1")
|
||||
fi
|
||||
if [[ $MAKE_NO_CLIENT ]]; then
|
||||
flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0" "-DWITH_DBCONVERTER=0")
|
||||
flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0")
|
||||
fi
|
||||
if [[ $MAKE_TEST ]]; then
|
||||
flags+=("-DTEST=1")
|
||||
@@ -156,6 +156,18 @@ function ccachestatsverbose() {
|
||||
|
||||
# Compile
|
||||
if [[ $RUNNER_OS == macOS ]]; then
|
||||
# QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action,
|
||||
# which sets a few environment variables
|
||||
if QTDIR=$(find "$GITHUB_WORKSPACE/Qt" -depth -maxdepth 2 -name macos -type d -print -quit); then
|
||||
echo "found QTDIR at $QTDIR"
|
||||
else
|
||||
echo "could not find QTDIR!"
|
||||
exit 2
|
||||
fi
|
||||
# the qtdir is located at Qt/[qtversion]/macos
|
||||
# we use find to get the first subfolder with the name "macos"
|
||||
# this works independent of the qt version as there should be only one version installed on the runner at a time
|
||||
export QTDIR
|
||||
|
||||
if [[ $TARGET_MACOS_VERSION ]]; then
|
||||
# CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version
|
||||
@@ -246,7 +258,7 @@ fi
|
||||
|
||||
if [[ $RUNNER_OS == macOS ]]; then
|
||||
echo "::group::Inspect Mach-O binaries"
|
||||
for app in cockatrice oracle servatrice dbconverter; do
|
||||
for app in cockatrice oracle servatrice; do
|
||||
binary="$GITHUB_WORKSPACE/build/$app/$app.app/Contents/MacOS/$app"
|
||||
echo "Inspecting $app..."
|
||||
vtool -show-build "$binary"
|
||||
|
||||
40
.ci/resolve_latest_aqt_qt_version.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is used to resolve the latest patch version of Qt using aqtinstall.
|
||||
# It interprets wildcards to get the latest patch version. E.g. "6.6.*" -> "6.6.3".
|
||||
|
||||
# This script is meant to be used by the ci enironment.
|
||||
# It uses the runner's GITHUB_OUTPUT env variable.
|
||||
|
||||
# Usage example: .ci/resolve_latest_aqt_qt_version.sh "6.6.*"
|
||||
|
||||
qt_spec=$1
|
||||
if [[ ! $qt_spec ]]; then
|
||||
echo "usage: $0 [version]"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# If version is already specific (no wildcard), use it as-is
|
||||
if [[ $qt_spec != *"*" ]]; then
|
||||
echo "version $qt_spec is already resolved"
|
||||
echo "version=$qt_spec" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! hash aqt; then
|
||||
echo "aqt could not be found, has aqtinstall been installed?"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Resolve latest patch
|
||||
if ! qt_resolved=$(aqt list-qt mac desktop --spec "$qt_spec" --latest-version); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "resolved $qt_spec to $qt_resolved"
|
||||
if [[ ! $qt_resolved ]]; then
|
||||
echo "Error: Could not resolve Qt version for $qt_spec"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=$qt_resolved" >> "$GITHUB_OUTPUT"
|
||||
25
.ci/thin_macos_qtlib.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# The macos binaries from aqt are fat (universal), so we thin them to the target architecture to reduce the size of
|
||||
# the packages and caches using lipo.
|
||||
|
||||
# This script is meant to be used by the ci enironment on macos runners only.
|
||||
# It uses the runner's GITHUB_WORKSPACE env variable.
|
||||
arch=$(uname -m)
|
||||
nproc=$(sysctl -n hw.ncpu)
|
||||
|
||||
function thin() {
|
||||
local libfile=$1
|
||||
if [[ $(file -b --mime-type "$libfile") == application/x-mach-binary* ]]; then
|
||||
echo "Processing $libfile"
|
||||
lipo "$libfile" -thin "$arch" -output "$libfile"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
export -f thin # export to allow use in xargs
|
||||
export arch
|
||||
set -eo pipefail
|
||||
|
||||
echo "::group::Thinning Qt libraries to $arch using $nproc cores"
|
||||
find "$GITHUB_WORKSPACE/Qt" -type f -print0 | xargs -0 -n1 -P"$nproc" -I{} bash -c "thin '{}'"
|
||||
echo "::endgroup::"
|
||||
101
.github/workflows/desktop-build.yml
vendored
@@ -142,7 +142,6 @@ jobs:
|
||||
- distro: Ubuntu
|
||||
version: 22.04
|
||||
package: DEB
|
||||
test: skip # Running tests on all distros is superfluous
|
||||
|
||||
- distro: Ubuntu
|
||||
version: 24.04
|
||||
@@ -166,7 +165,7 @@ jobs:
|
||||
|
||||
- name: Restore compiler cache (ccache)
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
with:
|
||||
@@ -205,7 +204,7 @@ jobs:
|
||||
|
||||
- name: Save compiler cache (ccache)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{env.CACHE}}
|
||||
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||
@@ -213,7 +212,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
id: upload_artifact
|
||||
if: matrix.package != 'skip'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{matrix.distro}}${{matrix.version}}-package
|
||||
path: ${{steps.build.outputs.path}}
|
||||
@@ -263,7 +262,6 @@ jobs:
|
||||
qt_version: 6.6.*
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
cache_qt: false # qt caches take too much space for macOS (1.1Gi)
|
||||
cmake_generator: Ninja
|
||||
use_ccache: 1
|
||||
|
||||
@@ -279,7 +277,6 @@ jobs:
|
||||
qt_version: 6.6.*
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
cache_qt: false
|
||||
cmake_generator: Ninja
|
||||
use_ccache: 1
|
||||
|
||||
@@ -295,7 +292,6 @@ jobs:
|
||||
qt_version: 6.6.*
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
cache_qt: false
|
||||
cmake_generator: Ninja
|
||||
use_ccache: 1
|
||||
|
||||
@@ -308,7 +304,6 @@ jobs:
|
||||
qt_version: 6.6.*
|
||||
qt_arch: clang_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
cache_qt: false
|
||||
cmake_generator: Ninja
|
||||
use_ccache: 1
|
||||
|
||||
@@ -321,7 +316,6 @@ jobs:
|
||||
artifact_name: Windows7-installer
|
||||
qt_version: 5.15.*
|
||||
qt_arch: win64_msvc2019_64
|
||||
cache_qt: true
|
||||
cmake_generator: "Visual Studio 17 2022"
|
||||
cmake_generator_platform: x64
|
||||
|
||||
@@ -335,13 +329,17 @@ jobs:
|
||||
qt_version: 6.6.*
|
||||
qt_arch: win64_msvc2019_64
|
||||
qt_modules: qtimageformats qtmultimedia qtwebsockets
|
||||
cache_qt: true
|
||||
cmake_generator: "Visual Studio 17 2022"
|
||||
cmake_generator_platform: x64
|
||||
|
||||
name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||
needs: configure
|
||||
runs-on: ${{matrix.runner}}
|
||||
env:
|
||||
CCACHE_DIR: ${{github.workspace}}/.cache/
|
||||
# Cache size over the entire repo is 10Gi:
|
||||
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CCACHE_SIZE: 500M
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -356,24 +354,72 @@ jobs:
|
||||
with:
|
||||
msbuild-architecture: x64
|
||||
|
||||
# Using jianmingyong/ccache-action to setup ccache without using brew
|
||||
# It tries to download a binary of ccache from GitHub Release and falls back to building from source if it fails
|
||||
- name: Setup ccache
|
||||
if: matrix.use_ccache == 1
|
||||
uses: jianmingyong/ccache-action@v1
|
||||
with:
|
||||
install-type: "binary"
|
||||
ccache-key-prefix: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}
|
||||
max-size: 500M
|
||||
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
if: matrix.use_ccache == 1 && matrix.os == 'macOS'
|
||||
run: brew install ccache
|
||||
|
||||
- name: Install Qt ${{matrix.qt_version}}
|
||||
- name: Restore compiler cache (ccache)
|
||||
if: matrix.use_ccache == 1
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v5
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
with:
|
||||
path: ${{env.CCACHE_DIR}}
|
||||
key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}}
|
||||
restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-
|
||||
|
||||
- name: Install aqtinstall
|
||||
if: matrix.os == 'macOS'
|
||||
run: pipx install aqtinstall
|
||||
|
||||
# Checking if there's a newer, uncached version of Qt available to install via aqtinstall
|
||||
- name: Resolve latest Qt patch version
|
||||
if: matrix.os == 'macOS'
|
||||
id: resolve_qt_version
|
||||
shell: bash
|
||||
# Ouputs the version of Qt to install via aqtinstall
|
||||
run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}"
|
||||
|
||||
- name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS)
|
||||
if: matrix.os == 'macOS'
|
||||
id: restore_qt
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/Qt
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
|
||||
|
||||
# Using jurplel/install-qt-action to install Qt without using brew
|
||||
# qt build using vcpkg either just fails or takes too long to build
|
||||
- name: Install fat Qt ${{ steps.resolve_qt_version.outputs.version }} (${{ matrix.soc }} macOS)
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
cache: false
|
||||
version: ${{ steps.resolve_qt_version.outputs.version }}
|
||||
arch: ${{matrix.qt_arch}}
|
||||
modules: ${{matrix.qt_modules}}
|
||||
dir: ${{github.workspace}}
|
||||
|
||||
- name: Thin Qt libraries (${{ matrix.soc }} macOS)
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
run: .ci/thin_macos_qtlib.sh
|
||||
|
||||
- name: Cache thin Qt libraries (${{ matrix.soc }} macOS)
|
||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{ github.workspace }}/Qt
|
||||
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
|
||||
|
||||
- name: Install Qt ${{matrix.qt_version}} (Windows)
|
||||
if: matrix.os == 'Windows'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{matrix.qt_version}}
|
||||
arch: ${{matrix.qt_arch}}
|
||||
modules: ${{matrix.qt_modules}}
|
||||
cache: ${{matrix.cache_qt}}
|
||||
cache: true
|
||||
|
||||
- name: Setup vcpkg cache
|
||||
id: vcpkg-cache
|
||||
@@ -403,6 +449,15 @@ jobs:
|
||||
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
||||
run: .ci/compile.sh --server --test --vcpkg
|
||||
|
||||
- name: Save compiler cache (ccache)
|
||||
if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1
|
||||
uses: actions/cache/save@v5
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
with:
|
||||
path: ${{env.CCACHE_DIR}}
|
||||
key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}}
|
||||
|
||||
- name: Sign app bundle
|
||||
if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
|
||||
env:
|
||||
@@ -450,7 +505,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
id: upload_artifact
|
||||
if: matrix.make_package
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{matrix.artifact_name}}
|
||||
path: ${{steps.build.outputs.path}}
|
||||
@@ -458,7 +513,7 @@ jobs:
|
||||
|
||||
- name: Upload pdb database
|
||||
if: matrix.os == 'Windows'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Windows${{matrix.target}}-debug-pdbs
|
||||
path: |
|
||||
|
||||
2
.github/workflows/translations-pull.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Create pull request
|
||||
if: github.event_name != 'pull_request'
|
||||
id: create_pr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
add-paths: |
|
||||
cockatrice/translations/*.ts
|
||||
|
||||
2
.github/workflows/translations-push.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Create pull request
|
||||
if: github.event_name != 'pull_request'
|
||||
id: create_pr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
add-paths: |
|
||||
cockatrice/cockatrice_en@source.ts
|
||||
|
||||
@@ -20,8 +20,6 @@ option(WITH_SERVER "build servatrice" OFF)
|
||||
option(WITH_CLIENT "build cockatrice" ON)
|
||||
# Compile oracle
|
||||
option(WITH_ORACLE "build oracle" ON)
|
||||
# Compile dbconverter
|
||||
option(WITH_DBCONVERTER "build dbconverter" ON)
|
||||
# Compile tests
|
||||
option(TEST "build tests" OFF)
|
||||
# Use vcpkg regardless of OS
|
||||
@@ -356,11 +354,6 @@ if(WITH_ORACLE)
|
||||
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||
endif()
|
||||
|
||||
if(WITH_DBCONVERTER)
|
||||
add_subdirectory(dbconverter)
|
||||
set(CPACK_INSTALL_CMAKE_PROJECTS "Dbconverter;Dbconverter;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||
endif()
|
||||
|
||||
if(TEST)
|
||||
include(CTest)
|
||||
add_subdirectory(tests)
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN mkdir build && cd build && \
|
||||
cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=0 && \
|
||||
cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 && \
|
||||
make -j$(nproc) && \
|
||||
make install
|
||||
|
||||
|
||||
1
Doxyfile
@@ -1068,7 +1068,6 @@ RECURSIVE = YES
|
||||
|
||||
EXCLUDE = build/ \
|
||||
cmake/ \
|
||||
dbconverter/ \
|
||||
vcpkg/ \
|
||||
webclient/
|
||||
|
||||
|
||||
53
README.md
@@ -44,9 +44,10 @@ Latest <kbd>beta</kbd> version:
|
||||
|
||||
# Related Repositories
|
||||
|
||||
- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice
|
||||
- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice
|
||||
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official webpage of the Cockatrice project
|
||||
- [Magic-Token](https://github.com/Cockatrice/Magic-Token): File with MtG token data for use in Cockatrice
|
||||
- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Code to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) for use in Cockatrice
|
||||
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage
|
||||
- [io.github.Cockatrice.cockatrice](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration of our Linux `flatpak` package hosted at [Flathub](https://flathub.org/en/apps/io.github.Cockatrice.cockatrice)
|
||||
|
||||
|
||||
# Community Resources [](https://discord.gg/3Z9yzmA)
|
||||
@@ -57,11 +58,28 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
|
||||
- [Official Discord](https://discord.gg/3Z9yzmA)
|
||||
- [reddit r/Cockatrice](https://reddit.com/r/cockatrice)
|
||||
|
||||
>[!IMPORTANT]
|
||||
>For support regarding specific servers, please contact that server's admin/mods and use their dedicated communication channels rather than contacting the team building the software.
|
||||
> [!IMPORTANT]
|
||||
> For support regarding specific servers, please contact that server's admin/mods and use their dedicated communication channels rather than contacting the team building the software.
|
||||
|
||||
|
||||
# Contribute
|
||||
<p>
|
||||
<a href="#code">Code</a> <b>|</b>
|
||||
<a href="#documentation-">Documentation</a> <b>|</b>
|
||||
<a href="#translation-">Translation</a>
|
||||
</p>
|
||||
|
||||
#### Repository Activity
|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>Kudos to all our amazing contributors ❤️</b></summary>
|
||||
<br>
|
||||
<a href="https://github.com/Cockatrice/Cockatrice/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Cockatrice/Cockatrice" />
|
||||
</a><br>
|
||||
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
|
||||
</details>
|
||||
|
||||
### Code
|
||||
|
||||
@@ -75,18 +93,19 @@ This tag is used for issues that we are looking for somebody to pick up. Often t
|
||||
For both tags, we're willing to provide help to contributors in showing them where and how they can make changes, as well as code reviews for submitted changes.<br>
|
||||
We'll happily advice on how best to implement a feature, or we can show you where the codebase is doing something similar before you get too far along - put a note on an issue you want to discuss more on!
|
||||
|
||||
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
|
||||
|
||||
### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
|
||||
|
||||
There are various places where useful information for different needs are maintained:
|
||||
- [Official Code Documentation](https://cockatrice.github.io/docs/)
|
||||
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported`
|
||||
- [Official Webpage](https://cockatrice.github.io/)
|
||||
- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file`
|
||||
|
||||
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
|
||||
|
||||
<details>
|
||||
<summary><b>Kudos to our amazing contributors ❤️</b></summary>
|
||||
<br>
|
||||
<a href="https://github.com/Cockatrice/Cockatrice/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Cockatrice/Cockatrice" />
|
||||
</a><br>
|
||||
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
|
||||
</details>
|
||||
|
||||
### Translations [](https://explore.transifex.com/cockatrice/cockatrice/)
|
||||
### Translation [](https://explore.transifex.com/cockatrice/cockatrice/)
|
||||
|
||||
Cockatrice uses Transifex to manage translations. You can help us bring <kbd>Cockatrice</kbd>, <kbd>Oracle</kbd> and <kbd>Webatrice</kbd> to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).<br>
|
||||
|
||||
@@ -124,8 +143,8 @@ You can then
|
||||
make package
|
||||
```
|
||||
|
||||
>[!NOTE]
|
||||
>Detailed compiling instructions can be found in the Cockatrice wiki at [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)
|
||||
> [!NOTE]
|
||||
> Detailed compiling instructions can be found in the Cockatrice wiki at [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ tell disk image_name
|
||||
set position of item "Cockatrice.app" to { 139, 214 }
|
||||
set position of item "Oracle.app" to { 139, 414 }
|
||||
set position of item "Servatrice.app" to { 139, 614 }
|
||||
set position of item "dbconverter.app" to { 1400, 1400 }
|
||||
set position of item "Applications" to { 861, 414 }
|
||||
end tell
|
||||
update without registering applications
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Find a compatible Qt version
|
||||
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, WITH_DBCONVERTER, FORCE_USE_QT5
|
||||
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, FORCE_USE_QT5
|
||||
# Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system
|
||||
# Optional Input: QT5_DIR -- Hint as to where Qt5 lives on the system
|
||||
# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt5, Qt6
|
||||
# Output: SERVATRICE_QT_MODULES
|
||||
# Output: COCKATRICE_QT_MODULES
|
||||
# Output: ORACLE_QT_MODULES
|
||||
# Output: DBCONVERTER_QT_MODULES
|
||||
# Output: TEST_QT_MODULES
|
||||
|
||||
set(REQUIRED_QT_COMPONENTS Core)
|
||||
@@ -29,15 +28,12 @@ endif()
|
||||
if(WITH_ORACLE)
|
||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
|
||||
endif()
|
||||
if(WITH_DBCONVERTER)
|
||||
set(_DBCONVERTER_NEEDED Network Widgets)
|
||||
endif()
|
||||
if(TEST)
|
||||
set(_TEST_NEEDED Widgets)
|
||||
endif()
|
||||
|
||||
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
||||
${_DBCONVERTER_NEEDED} ${_TEST_NEEDED}
|
||||
${_TEST_NEEDED}
|
||||
)
|
||||
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
|
||||
|
||||
@@ -63,7 +59,7 @@ if(Qt6_FOUND)
|
||||
endif()
|
||||
else()
|
||||
find_package(
|
||||
Qt5 5.8.0
|
||||
Qt5 5.15.2
|
||||
COMPONENTS ${REQUIRED_QT_COMPONENTS}
|
||||
QUIET HINTS ${Qt5_DIR}
|
||||
)
|
||||
@@ -112,7 +108,6 @@ message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")
|
||||
string(REGEX REPLACE "([^;]+)" "${COCKATRICE_QT_VERSION_NAME}::\\1" SERVATRICE_QT_MODULES "${_SERVATRICE_NEEDED}")
|
||||
string(REGEX REPLACE "([^;]+)" "${COCKATRICE_QT_VERSION_NAME}::\\1" COCKATRICE_QT_MODULES "${_COCKATRICE_NEEDED}")
|
||||
string(REGEX REPLACE "([^;]+)" "${COCKATRICE_QT_VERSION_NAME}::\\1" ORACLE_QT_MODULES "${_ORACLE_NEEDED}")
|
||||
string(REGEX REPLACE "([^;]+)" "${COCKATRICE_QT_VERSION_NAME}::\\1" DB_CONVERTER_QT_MODULES "${_DBCONVERTER_NEEDED}")
|
||||
string(REGEX REPLACE "([^;]+)" "${COCKATRICE_QT_VERSION_NAME}::\\1" TEST_QT_MODULES "${_TEST_NEEDED}")
|
||||
|
||||
# Core-only export (useful for headless libs)
|
||||
|
||||
@@ -213,7 +213,6 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
Delete "$INSTDIR\cockatrice.exe"
|
||||
Delete "$INSTDIR\oracle.exe"
|
||||
Delete "$INSTDIR\dbconverter.exe"
|
||||
Delete "$INSTDIR\servatrice.exe"
|
||||
Delete "$INSTDIR\Qt*.dll"
|
||||
Delete "$INSTDIR\libmysql.dll"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
string(LENGTH "$ENV{MACOS_CERTIFICATE_NAME}" MACOS_CERTIFICATE_NAME_LEN)
|
||||
|
||||
if(APPLE AND MACOS_CERTIFICATE_NAME_LEN GREATER 0)
|
||||
set(APPLICATIONS "cockatrice" "servatrice" "oracle" "dbconverter")
|
||||
set(APPLICATIONS "cockatrice" "servatrice" "oracle")
|
||||
foreach(app_name IN LISTS APPLICATIONS)
|
||||
set(FULL_APP_PATH "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${app_name}.app")
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ set(cockatrice_SOURCES
|
||||
src/client/settings/card_counter_settings.cpp
|
||||
src/client/settings/shortcut_treeview.cpp
|
||||
src/client/settings/shortcuts_settings.cpp
|
||||
src/interface/deck_loader/card_node_function.cpp
|
||||
src/interface/deck_loader/deck_file_format.cpp
|
||||
src/interface/deck_loader/deck_loader.cpp
|
||||
src/interface/deck_loader/loaded_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_connect.cpp
|
||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
|
||||
src/interface/widgets/dialogs/dlg_create_game.cpp
|
||||
@@ -141,28 +144,54 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/cards/card_size_widget.cpp
|
||||
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
||||
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
|
||||
src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
|
||||
src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp
|
||||
src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp
|
||||
src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp
|
||||
src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
|
||||
src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp
|
||||
src/interface/widgets/deck_analytics/mana_base_widget.cpp
|
||||
src/interface/widgets/deck_analytics/mana_curve_widget.cpp
|
||||
src/interface/widgets/deck_analytics/mana_devotion_widget.cpp
|
||||
src/interface/widgets/deck_analytics/resizable_panel.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp
|
||||
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_editor_filter_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_state_manager.cpp
|
||||
src/interface/widgets/general/background_sources.cpp
|
||||
src/interface/widgets/general/display/background_plate_widget.cpp
|
||||
src/interface/widgets/general/display/banner_widget.cpp
|
||||
src/interface/widgets/general/display/bar_widget.cpp
|
||||
src/interface/widgets/general/display/color_bar.cpp
|
||||
src/interface/widgets/general/display/dynamic_font_size_label.cpp
|
||||
src/interface/widgets/general/display/dynamic_font_size_push_button.cpp
|
||||
src/interface/widgets/general/display/labeled_input.cpp
|
||||
src/interface/widgets/general/display/percent_bar_widget.cpp
|
||||
src/interface/widgets/general/display/shadow_background_label.cpp
|
||||
src/interface/widgets/general/display/charts/bars/bar_widget.cpp
|
||||
src/interface/widgets/general/display/charts/bars/color_bar.cpp
|
||||
src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp
|
||||
src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp
|
||||
src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp
|
||||
src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp
|
||||
src/interface/widgets/general/display/charts/pies/color_pie.cpp
|
||||
src/interface/widgets/general/home_styled_button.cpp
|
||||
src/interface/widgets/general/home_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/flow_widget.cpp
|
||||
@@ -174,6 +203,7 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/printing_selector/printing_selector.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_placeholder_widget.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_card_search_widget.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
|
||||
@@ -199,6 +229,8 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp
|
||||
@@ -206,6 +238,7 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp
|
||||
src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp
|
||||
src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.cpp
|
||||
src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp
|
||||
src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp
|
||||
@@ -226,6 +259,7 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_formats.h
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_card_entry.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/card/archidekt_api_response_edition.cpp
|
||||
@@ -282,6 +316,10 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||
src/interface/key_signals.cpp
|
||||
src/interface/logger.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||
)
|
||||
|
||||
add_subdirectory(sounds)
|
||||
|
||||
@@ -15,18 +15,23 @@
|
||||
<file>resources/icons/arrow_top_green.svg</file>
|
||||
<file>resources/icons/arrow_up_green.svg</file>
|
||||
<file>resources/icons/arrow_undo.svg</file>
|
||||
<file>resources/icons/circle_half_stroke.svg</file>
|
||||
<file>resources/icons/clearsearch.svg</file>
|
||||
<file>resources/icons/cogwheel.svg</file>
|
||||
<file>resources/icons/conceded.svg</file>
|
||||
<file>resources/icons/decrement.svg</file>
|
||||
<file>resources/icons/delete.svg</file>
|
||||
<file>resources/icons/dragon.svg</file>
|
||||
<file>resources/icons/dropdown_collapsed.svg</file>
|
||||
<file>resources/icons/dropdown_expanded.svg</file>
|
||||
<file>resources/icons/floppy_disk.svg</file>
|
||||
<file>resources/icons/forgot_password.svg</file>
|
||||
<file>resources/icons/gear.svg</file>
|
||||
<file>resources/icons/increment.svg</file>
|
||||
<file>resources/icons/info.svg</file>
|
||||
<file>resources/icons/lock.svg</file>
|
||||
<file>resources/icons/not_ready_start.svg</file>
|
||||
<file>resources/icons/pen_to_square.svg</file>
|
||||
<file>resources/icons/pencil.svg</file>
|
||||
<file>resources/icons/pin.svg</file>
|
||||
<file>resources/icons/player.svg</file>
|
||||
@@ -34,10 +39,13 @@
|
||||
<file>resources/icons/reload.svg</file>
|
||||
<file>resources/icons/remove_row.svg</file>
|
||||
<file>resources/icons/rename.svg</file>
|
||||
<file>resources/icons/scale_balanced.svg</file>
|
||||
<file>resources/icons/scales.svg</file>
|
||||
<file>resources/icons/scroll.svg</file>
|
||||
<file>resources/icons/search.svg</file>
|
||||
<file>resources/icons/settings.svg</file>
|
||||
<file>resources/icons/share.svg</file>
|
||||
<file>resources/icons/sort_arrow_down.svg</file>
|
||||
<file>resources/icons/spectator.svg</file>
|
||||
<file>resources/icons/swap.svg</file>
|
||||
<file>resources/icons/sync.svg</file>
|
||||
@@ -52,6 +60,8 @@
|
||||
<file>resources/icons/mana/W.svg</file>
|
||||
|
||||
<file>resources/backgrounds/home.png</file>
|
||||
<file>resources/backgrounds/card_triplet.svg</file>
|
||||
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
|
||||
|
||||
<file>resources/config/general.svg</file>
|
||||
<file>resources/config/appearance.svg</file>
|
||||
|
||||
31
cockatrice/resources/backgrounds/card_triplet.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="250"
|
||||
id="svg13"
|
||||
height="231.66667"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs13" />
|
||||
<g
|
||||
transform="matrix(1.705559,0,0,1.705559,-18.310328,-4.2419088)"
|
||||
id="g13">
|
||||
<path
|
||||
d="M 90.069854,3.479957 C 89.356513,1.2235709 86.980392,-0.01102897 84.723451,0.70218215 L 3.4767601,26.377781 C 1.2199188,27.090982 -0.01486587,29.46663 0.69839437,31.723116 L 33.512365,135.52112 c 0.713341,2.25639 3.089462,3.49099 5.346403,2.77777 l 81.246672,-25.6756 c 2.25684,-0.71319 3.49163,-3.08884 2.77837,-5.34533 L 90.074852,3.479957 Z"
|
||||
style="display:none;fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 110.61293,7.4983294 c -0.36657,-2.337853 -2.53055,-3.9150142 -4.86886,-3.5484627 L 21.563382,17.14452 c -2.338314,0.366502 -3.915784,2.529976 -3.549207,4.867929 L 34.876507,129.55893 c 0.366577,2.33786 2.530549,3.91502 4.868863,3.54847 l 84.18069,-13.19466 c 2.33831,-0.3665 3.91578,-2.52997 3.5492,-4.86793 L 110.61093,7.4983294 Z"
|
||||
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 130.53623,15.555064 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 41.067426 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 V 124.41102 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208344 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
|
||||
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path7" />
|
||||
<path
|
||||
d="m 149.43988,26.480639 c 0.38018,-2.335754 -1.1846,-4.508374 -3.52082,-4.88852 L 61.817351,7.9076636 C 59.481136,7.5275576 57.308066,9.0920839 56.927894,11.427736 L 39.439773,118.87426 c -0.380182,2.33576 1.184602,4.50838 3.520816,4.88852 l 84.102711,13.68346 c 2.33622,0.38011 4.50929,-1.18442 4.88946,-3.52007 L 149.43688,26.479639 Z"
|
||||
style="display:inline;fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path10" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="172.65051"
|
||||
id="svg13"
|
||||
height="213.30714"
|
||||
xml:space="preserve"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"><defs
|
||||
id="defs13" /><g
|
||||
transform="matrix(1.705559,0,0,1.705559,-97.653345,-68.741256)"
|
||||
id="g13"><path
|
||||
d="m 151.48519,45.063813 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 62.016385 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 V 153.91977 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208345 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
|
||||
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path7" /><path
|
||||
d="m 154.70135,48.441704 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 65.232545 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 V 157.29767 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208345 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
|
||||
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path7-5" /><path
|
||||
d="m 157.98403,51.75453 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 68.515228 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 v 108.85596 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208342 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
|
||||
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
|
||||
id="path7-6" /></g><path
|
||||
d="m 196.24576,207.42361 c 0,0.11213 0,0.22413 0,0.33621 -0.0498,4.54511 -4.18399,7.63329 -8.72909,7.63329 h -12.19086 c -3.29988,0 -5.97713,2.67727 -5.97713,5.97712 0,0.4234 0.0498,0.83433 0.12449,1.23279 0.26149,1.27014 0.80939,2.49046 1.34485,3.72325 0.75959,1.71843 1.50674,3.4244 1.50674,5.22998 0,3.95986 -2.68971,7.55859 -6.64956,7.72046 -0.43583,0.0128 -0.87166,0.025 -1.31995,0.025 -17.60761,0 -31.878,-14.27038 -31.878,-31.878 0,-17.60761 14.28284,-31.878 31.89046,-31.878 17.60762,0 31.87801,14.27039 31.87801,31.878 z m -47.81703,3.98475 c 0,-2.20407 -1.78067,-3.98475 -3.98473,-3.98475 -2.20407,0 -3.98477,1.78068 -3.98477,3.98475 0,2.20406 1.7807,3.98475 3.98477,3.98475 2.20406,0 3.98473,-1.78069 3.98473,-3.98475 z m 0,-11.95426 c 2.20407,0 3.98477,-1.78068 3.98477,-3.98475 0,-2.20407 -1.7807,-3.98475 -3.98477,-3.98475 -2.20405,0 -3.98473,1.78068 -3.98473,3.98475 0,2.20407 1.78068,3.98475 3.98473,3.98475 z m 19.92376,-11.95424 c 0,-2.20408 -1.78068,-3.98477 -3.98473,-3.98477 -2.20407,0 -3.98477,1.78069 -3.98477,3.98477 0,2.20405 1.7807,3.98474 3.98477,3.98474 2.20405,0 3.98473,-1.78069 3.98473,-3.98474 z m 11.95426,11.95424 c 2.20407,0 3.98475,-1.78068 3.98475,-3.98475 0,-2.20407 -1.78068,-3.98475 -3.98475,-3.98475 -2.20406,0 -3.98475,1.78068 -3.98475,3.98475 0,2.20407 1.78069,3.98475 3.98475,3.98475 z"
|
||||
id="path1"
|
||||
style="display:none;fill:#3b3b3b;fill-opacity:1;stroke:#000000;stroke-width:2.53798;stroke-dasharray:none;stroke-opacity:1" /><path
|
||||
d="M 126.20915,54.574783 82.324247,83.8512 c -5.76807,3.845383 -9.435059,10.089163 -10.029703,16.90777 12.348823,2.53716 22.081191,12.26955 24.638191,24.63819 6.838435,-0.59465 13.062395,-4.26163 16.907775,-10.0297 l 29.2566,-43.904722 c 1.32804,-2.001984 2.04162,-4.340923 2.04162,-6.75915 0,-6.719506 -5.45092,-12.170428 -12.17043,-12.170428 -2.3984,0 -4.75718,0.713573 -6.75915,2.041623 z M 88.052677,131.81933 c 0,-12.26953 -9.930593,-22.20012 -22.200138,-22.20012 -12.269532,0 -22.200126,9.93059 -22.200126,22.20012 0,0.77305 0.03966,1.54609 0.118929,2.2993 0.356787,3.46877 -2.021792,7.21505 -5.510393,7.21505 h -0.951431 c -3.508409,0 -6.342895,2.83447 -6.342895,6.3429 0,3.5084 2.834486,6.34289 6.342895,6.34289 h 28.543021 c 12.269545,0 22.200138,-9.93059 22.200138,-22.20014 z"
|
||||
id="path1-2"
|
||||
style="fill:#989898;fill-opacity:1;stroke-width:0.198215" /></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,6 +1,10 @@
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
#*.info = true
|
||||
#*.warning = true
|
||||
#*.critical = true
|
||||
#*.fatal = true
|
||||
|
||||
# Uncomment a rule to see debug level logs for that category,
|
||||
# or set <category> = false to disable logging
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
@page deck_search_syntax_help Deck Search Syntax Help
|
||||
|
||||
## Deck Search Syntax Help
|
||||
-----
|
||||
|
||||
The search bar recognizes a set of special commands.<br>
|
||||
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all
|
||||
searches are case insensitive.
|
||||
|
||||
<dl>
|
||||
|
||||
<dt>Display Name (The deck name, or the filename if the deck name isn't set):</dt>
|
||||
<dd>[red deck wins](#red deck wins) <small>(Any deck with a display name containing the words red, deck, and wins)</small></dd>
|
||||
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck with a display name containing the exact phrase "red deck wins")</small></dd>
|
||||
@@ -15,15 +17,23 @@ searches are case insensitive.
|
||||
<dd>[n:red n:deck n:wins](#n:red n:deck n:wins) <small>(Any deck with a name containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[n:"red deck wins"](#n:%22red deck wins%22) <small>(Any deck with a name containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt><u>F</u>ile Name:</dt>
|
||||
<dd>[f:aggro](#f:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
|
||||
<dd>[f:red f:deck f:wins](#f:red f:deck f:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[f:"red deck wins"](#f:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
|
||||
<dt><u>F</u>ile <u>N</u>ame:</dt>
|
||||
<dd>[fn:aggro](#fn:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
|
||||
<dd>[fn:red fn:deck fn:wins](#fn:red fn:deck fn:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[fn:"red deck wins"](#fn:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt>Relative <u>P</u>ath (starting from the deck folder):</dt>
|
||||
<dd>[p:aggro](#p:aggro) <small>(Any deck that has "aggro" somewhere in its relative path)</small></dd>
|
||||
<dd>[p:edh/](#p:edh/) <small>(Any deck with "edh/" in its relative path, A.K.A. decks in the "edh" folder)</small></dd>
|
||||
|
||||
<dt><u>F</u>ormat:</dt>
|
||||
<dd>[f:standard](#f:standard) <small>(Any deck with format set to standard)</small></dd>
|
||||
|
||||
<dt><u>C</u>omments:</dt>
|
||||
<dd>[c:good](#c:good) <small>(Any deck with comments containing the word good)</small></dd>
|
||||
<dd>[c:good c:deck](#c:good c:deck) <small>(Any deck with comments containing the words good and deck)</small></dd>
|
||||
<dd>[c:"good deck"](#c:%22good deck%22) <small>(Any deck with comments containing the exact phrase "good deck")</small></dd>
|
||||
|
||||
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
|
||||
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
|
||||
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@page search_syntax_help Search Syntax Help
|
||||
|
||||
## Search Syntax Help
|
||||
-----
|
||||
|
||||
The search bar recognizes a set of special commands similar to some other card databases.<br>
|
||||
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive.
|
||||
|
||||
<dl>
|
||||
|
||||
<dt>Name:</dt>
|
||||
<dd>[birds of paradise](#birds of paradise) <small>(Any card name containing the words birds, of, and paradise)</small></dd>
|
||||
<dd>["birds of paradise"](#%22birds of paradise%22) <small>(Any card name containing the exact phrase "birds of paradise")</small></dd>
|
||||
|
||||
1
cockatrice/resources/icons/circle_half_stroke.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M512 320C512 214 426 128 320 128L320 512C426 512 512 426 512 320zM64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320z"/></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
1
cockatrice/resources/icons/dragon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M352 188.5L300.1 175.5C293.6 173.9 288.8 168.4 288.1 161.7C287.4 155 290.9 148.6 296.8 145.6L337.6 125.2L294.3 92.7C288.8 88.6 286.5 81.4 288.7 74.8C290.9 68.2 297.1 64 304 64L464 64C494.2 64 522.7 78.2 540.8 102.4L598.4 179.2C604.6 187.5 608 197.6 608 208C608 234.5 586.5 256 560 256L538.5 256C521.5 256 505.2 249.3 493.2 237.3L479.9 224L447.9 224L447.9 245.5C447.9 270.3 460.7 293.4 481.7 306.6L588.3 373.2C620.4 393.3 639.9 428.4 639.9 466.3C639.9 526.9 590.8 576.1 530.1 576.1L32.3 576C29 576 25.7 575.6 22.7 574.6C13.5 571.8 6 565 2.3 556C1 552.7 .1 549.1 0 545.3C-.2 541.6 .3 538 1.3 534.6C4.1 525.4 10.9 517.9 19.9 514.2C22.9 513 26.1 512.2 29.4 512L433.3 476C441.6 475.3 448 468.3 448 459.9C448 455.6 446.3 451.5 443.3 448.5L398.9 404.1C368.9 374.1 352 333.4 352 291L352 188.5zM512 136.3C512 136.2 512 136.1 512 136C512 135.9 512 135.8 512 135.7L512 136.3zM510.7 143.7L464.3 132.1C464.1 133.4 464 134.7 464 136C464 149.3 474.7 160 488 160C498.6 160 507.5 153.2 510.7 143.7zM130.9 180.5C147.2 166 171.3 164.3 189.4 176.4L320 263.4L320 290.9C320 323.7 328.4 355.7 344 383.9L112 383.9C105.3 383.9 99.3 379.7 97 373.5C94.7 367.3 96.5 360.2 101.6 355.8L171 296.3L18.4 319.8C11.4 320.9 4.5 317.2 1.5 310.8C-1.5 304.4 .1 296.8 5.4 292L130.9 180.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
cockatrice/resources/icons/floppy_disk.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M160 96C124.7 96 96 124.7 96 160L96 480C96 515.3 124.7 544 160 544L480 544C515.3 544 544 515.3 544 480L544 237.3C544 220.3 537.3 204 525.3 192L448 114.7C436 102.7 419.7 96 402.7 96L160 96zM192 192C192 174.3 206.3 160 224 160L384 160C401.7 160 416 174.3 416 192L416 256C416 273.7 401.7 288 384 288L224 288C206.3 288 192 273.7 192 256L192 192zM320 352C355.3 352 384 380.7 384 416C384 451.3 355.3 480 320 480C284.7 480 256 451.3 256 416C256 380.7 284.7 352 320 352z"/></svg>
|
||||
|
After Width: | Height: | Size: 693 B |
1
cockatrice/resources/icons/gear.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
cockatrice/resources/icons/pen_to_square.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M535.6 85.7C513.7 63.8 478.3 63.8 456.4 85.7L432 110.1L529.9 208L554.3 183.6C576.2 161.7 576.2 126.3 554.3 104.4L535.6 85.7zM236.4 305.7C230.3 311.8 225.6 319.3 222.9 327.6L193.3 416.4C190.4 425 192.7 434.5 199.1 441C205.5 447.5 215 449.7 223.7 446.8L312.5 417.2C320.7 414.5 328.2 409.8 334.4 403.7L496 241.9L398.1 144L236.4 305.7zM160 128C107 128 64 171 64 224L64 480C64 533 107 576 160 576L416 576C469 576 512 533 512 480L512 384C512 366.3 497.7 352 480 352C462.3 352 448 366.3 448 384L448 480C448 497.7 433.7 512 416 512L160 512C142.3 512 128 497.7 128 480L128 224C128 206.3 142.3 192 160 192L256 192C273.7 192 288 177.7 288 160C288 142.3 273.7 128 256 128L160 128z"/></svg>
|
||||
|
After Width: | Height: | Size: 899 B |
1
cockatrice/resources/icons/scale_balanced.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M384 96L512 96C529.7 96 544 110.3 544 128C544 145.7 529.7 160 512 160L398.4 160C393.2 185.8 375.5 207.1 352 217.3L352 512L512 512C529.7 512 544 526.3 544 544C544 561.7 529.7 576 512 576L128 576C110.3 576 96 561.7 96 544C96 526.3 110.3 512 128 512L288 512L288 217.3C264.5 207 246.8 185.7 241.6 160L128 160C110.3 160 96 145.7 96 128C96 110.3 110.3 96 128 96L256 96C270.6 76.6 293.8 64 320 64C346.2 64 369.4 76.6 384 96zM439.6 384L584.4 384L512 259.8L439.6 384zM512 480C449.1 480 396.8 446 386 401.1C383.4 390.1 387 378.8 392.7 369L487.9 205.8C492.9 197.2 502.1 192 512 192C521.9 192 531.1 197.3 536.1 205.8L631.3 369C637 378.8 640.6 390.1 638 401.1C627.2 445.9 574.9 480 512 480zM126.8 259.8L54.4 384L199.3 384L126.8 259.8zM.9 401.1C-1.7 390.1 1.9 378.8 7.6 369L102.8 205.8C107.8 197.2 117 192 126.9 192C136.8 192 146 197.3 151 205.8L246.2 369C251.9 378.8 255.5 390.1 252.9 401.1C242.1 445.9 189.8 480 126.9 480C64 480 11.7 446 .9 401.1z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
cockatrice/resources/icons/scroll.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M32 176C32 134.5 63.6 100.4 104 96.4L104 96L384 96C437 96 480 139 480 192L480 368L304 368C264.2 368 232 400.2 232 440L232 500C232 524.3 212.3 544 188 544C163.7 544 144 524.3 144 500L144 272L80 272C53.5 272 32 250.5 32 224L32 176zM268.8 544C275.9 530.9 280 515.9 280 500L280 440C280 426.7 290.7 416 304 416L552 416C565.3 416 576 426.7 576 440L576 464C576 508.2 540.2 544 496 544L268.8 544zM112 144C94.3 144 80 158.3 80 176L80 224L144 224L144 176C144 158.3 129.7 144 112 144z"/></svg>
|
||||
|
After Width: | Height: | Size: 704 B |
1
cockatrice/resources/icons/sort_arrow_down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M278.6 438.6L182.6 534.6C170.1 547.1 149.8 547.1 137.3 534.6L41.3 438.6C28.8 426.1 28.8 405.8 41.3 393.3C53.8 380.8 74.1 380.8 86.6 393.3L128 434.7L128 128C128 110.3 142.3 96 160 96C177.7 96 192 110.3 192 128L192 434.7L233.4 393.3C245.9 380.8 266.2 380.8 278.7 393.3C291.2 405.8 291.2 426.1 278.7 438.6zM352 544C334.3 544 320 529.7 320 512C320 494.3 334.3 480 352 480L384 480C401.7 480 416 494.3 416 512C416 529.7 401.7 544 384 544L352 544zM352 416C334.3 416 320 401.7 320 384C320 366.3 334.3 352 352 352L448 352C465.7 352 480 366.3 480 384C480 401.7 465.7 416 448 416L352 416zM352 288C334.3 288 320 273.7 320 256C320 238.3 334.3 224 352 224L512 224C529.7 224 544 238.3 544 256C544 273.7 529.7 288 512 288L352 288zM352 160C334.3 160 320 145.7 320 128C320 110.3 334.3 96 352 96L576 96C593.7 96 608 110.3 608 128C608 145.7 593.7 160 576 160L352 160z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -350,11 +350,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;fill:url(#linearGradient3);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.77952756;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -321,11 +321,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 46.656521,12.167234 18.055171,18.054184 a 6.6081919,6.6078288 0 0 1 -0.126303,9.352065 6.6804126,6.6800456 0 0 1 -8.233169,1.011048 l -7.944268,7.943843 6.463762,6.445343 a 6.9331851,6.9328042 0 0 1 5.741536,2.022073 l 28.057729,28.092294 a 6.9962797,6.9958953 0 0 1 -9.894222,9.893685 L 50.719018,66.907526 A 7.0595711,7.0591833 0 0 1 49.18433,59.270613 l -5.741527,-5.741238 -7.944298,7.943843 A 6.716523,6.7161541 0 0 1 25.134866,69.832263 L 7.079684,51.778091 a 6.716523,6.7161541 0 0 1 8.39566,-10.345064 L 36.31101,20.59853 a 6.716523,6.7161541 0 0 1 10.345612,-8.431329 z"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -340,11 +340,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -8,6 +8,7 @@
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <version_string.h>
|
||||
|
||||
DeckStatsInterface::DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent)
|
||||
: QObject(parent), cardDatabase(_cardDatabase)
|
||||
@@ -42,31 +43,32 @@ void DeckStatsInterface::queryFinished(QNetworkReply *reply)
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void DeckStatsInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data)
|
||||
void DeckStatsInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
|
||||
{
|
||||
DeckList deckWithoutTokens;
|
||||
copyDeckWithoutTokens(*deck, deckWithoutTokens);
|
||||
copyDeckWithoutTokens(deck, deckWithoutTokens);
|
||||
|
||||
QUrl params;
|
||||
QUrlQuery urlQuery;
|
||||
urlQuery.addQueryItem("deck", deckWithoutTokens.writeToString_Plain());
|
||||
urlQuery.addQueryItem("decktitle", deck->getName());
|
||||
urlQuery.addQueryItem("decktitle", deck.getName());
|
||||
params.setQuery(urlQuery);
|
||||
data->append(params.query(QUrl::EncodeReserved).toUtf8());
|
||||
data.append(params.query(QUrl::EncodeReserved).toUtf8());
|
||||
}
|
||||
|
||||
void DeckStatsInterface::analyzeDeck(DeckList *deck)
|
||||
void DeckStatsInterface::analyzeDeck(const DeckList &deck)
|
||||
{
|
||||
QByteArray data;
|
||||
getAnalyzeRequestData(deck, &data);
|
||||
getAnalyzeRequestData(deck, data);
|
||||
|
||||
QNetworkRequest request(QUrl("https://deckstats.net/index.php"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
manager->post(request, data);
|
||||
}
|
||||
|
||||
void DeckStatsInterface::copyDeckWithoutTokens(DeckList &source, DeckList &destination)
|
||||
void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList &destination)
|
||||
{
|
||||
auto copyIfNotAToken = [this, &destination](const auto node, const auto card) {
|
||||
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName());
|
||||
|
||||
@@ -28,15 +28,15 @@ private:
|
||||
* closest non-token card instead. So we construct a new deck which has no
|
||||
* tokens.
|
||||
*/
|
||||
void copyDeckWithoutTokens(DeckList &source, DeckList &destination);
|
||||
void copyDeckWithoutTokens(const DeckList &source, DeckList &destination);
|
||||
|
||||
private slots:
|
||||
void queryFinished(QNetworkReply *reply);
|
||||
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
|
||||
void getAnalyzeRequestData(const DeckList &deck, QByteArray &data);
|
||||
|
||||
public:
|
||||
explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
|
||||
void analyzeDeck(DeckList *deck);
|
||||
void analyzeDeck(const DeckList &deck);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <version_string.h>
|
||||
|
||||
TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent)
|
||||
: QObject(parent), cardDatabase(_cardDatabase)
|
||||
@@ -66,32 +67,33 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void TappedOutInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data)
|
||||
void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
|
||||
{
|
||||
DeckList mainboard, sideboard;
|
||||
copyDeckSplitMainAndSide(*deck, mainboard, sideboard);
|
||||
copyDeckSplitMainAndSide(deck, mainboard, sideboard);
|
||||
|
||||
QUrl params;
|
||||
QUrlQuery urlQuery;
|
||||
urlQuery.addQueryItem("name", deck->getName());
|
||||
urlQuery.addQueryItem("name", deck.getName());
|
||||
urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true));
|
||||
urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true));
|
||||
params.setQuery(urlQuery);
|
||||
data->append(params.query(QUrl::EncodeReserved).toUtf8());
|
||||
data.append(params.query(QUrl::EncodeReserved).toUtf8());
|
||||
}
|
||||
|
||||
void TappedOutInterface::analyzeDeck(DeckList *deck)
|
||||
void TappedOutInterface::analyzeDeck(const DeckList &deck)
|
||||
{
|
||||
QByteArray data;
|
||||
getAnalyzeRequestData(deck, &data);
|
||||
getAnalyzeRequestData(deck, data);
|
||||
|
||||
QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
manager->post(request, data);
|
||||
}
|
||||
|
||||
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||
void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||
{
|
||||
auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) {
|
||||
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName());
|
||||
|
||||
@@ -30,14 +30,14 @@ private:
|
||||
QNetworkAccessManager *manager;
|
||||
|
||||
CardDatabase &cardDatabase;
|
||||
void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||
private slots:
|
||||
void queryFinished(QNetworkReply *reply);
|
||||
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
|
||||
void getAnalyzeRequestData(const DeckList &deck, QByteArray &data);
|
||||
|
||||
public:
|
||||
explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
|
||||
void analyzeDeck(DeckList *deck);
|
||||
void analyzeDeck(const DeckList &deck);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#ifndef INTERFACE_JSON_DECK_PARSER_H
|
||||
#define INTERFACE_JSON_DECK_PARSER_H
|
||||
|
||||
#include "../../../interface/deck_loader/card_node_function.h"
|
||||
#include "../../../interface/deck_loader/deck_loader.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
@@ -16,21 +18,21 @@ class IJsonDeckParser
|
||||
public:
|
||||
virtual ~IJsonDeckParser() = default;
|
||||
|
||||
virtual DeckLoader *parse(const QJsonObject &obj) = 0;
|
||||
virtual DeckList parse(const QJsonObject &obj) = 0;
|
||||
};
|
||||
|
||||
class ArchidektJsonParser : public IJsonDeckParser
|
||||
{
|
||||
public:
|
||||
DeckLoader *parse(const QJsonObject &obj) override
|
||||
DeckList parse(const QJsonObject &obj) override
|
||||
{
|
||||
DeckLoader *loader = new DeckLoader(nullptr);
|
||||
DeckList deckList;
|
||||
|
||||
QString deckName = obj.value("name").toString();
|
||||
QString deckDescription = obj.value("description").toString();
|
||||
|
||||
loader->getDeckList()->setName(deckName);
|
||||
loader->getDeckList()->setComments(deckDescription);
|
||||
deckList.setName(deckName);
|
||||
deckList.setComments(deckDescription);
|
||||
|
||||
QString outputText;
|
||||
QTextStream outStream(&outputText);
|
||||
@@ -47,25 +49,25 @@ public:
|
||||
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
|
||||
}
|
||||
|
||||
loader->getDeckList()->loadFromStream_Plain(outStream, false);
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
|
||||
deckList.loadFromStream_Plain(outStream, false);
|
||||
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
|
||||
return loader;
|
||||
return deckList;
|
||||
}
|
||||
};
|
||||
|
||||
class MoxfieldJsonParser : public IJsonDeckParser
|
||||
{
|
||||
public:
|
||||
DeckLoader *parse(const QJsonObject &obj) override
|
||||
DeckList parse(const QJsonObject &obj) override
|
||||
{
|
||||
DeckLoader *loader = new DeckLoader(nullptr);
|
||||
DeckList deckList;
|
||||
|
||||
QString deckName = obj.value("name").toString();
|
||||
QString deckDescription = obj.value("description").toString();
|
||||
|
||||
loader->getDeckList()->setName(deckName);
|
||||
loader->getDeckList()->setComments(deckDescription);
|
||||
deckList.setName(deckName);
|
||||
deckList.setComments(deckDescription);
|
||||
|
||||
QString outputText;
|
||||
QTextStream outStream(&outputText);
|
||||
@@ -94,8 +96,8 @@ public:
|
||||
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
|
||||
}
|
||||
|
||||
loader->getDeckList()->loadFromStream_Plain(outStream, false);
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
|
||||
deckList.loadFromStream_Plain(outStream, false);
|
||||
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
|
||||
QJsonObject commandersObj = obj.value("commanders").toObject();
|
||||
if (!commandersObj.isEmpty()) {
|
||||
@@ -106,12 +108,12 @@ public:
|
||||
QString collectorNumber = cardData.value("cn").toString();
|
||||
QString providerId = cardData.value("scryfall_id").toString();
|
||||
|
||||
loader->getDeckList()->setBannerCard({commanderName, providerId});
|
||||
loader->getDeckList()->addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
|
||||
deckList.setBannerCard({commanderName, providerId});
|
||||
deckList.addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
|
||||
}
|
||||
}
|
||||
|
||||
return loader;
|
||||
return deckList;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QtConcurrent>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <version_string.h>
|
||||
|
||||
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
||||
#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
|
||||
@@ -39,7 +40,9 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
|
||||
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
||||
{
|
||||
auto *nam = new QNetworkAccessManager(this);
|
||||
QNetworkReply *reply = nam->get(QNetworkRequest(url));
|
||||
auto request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
QNetworkReply *reply = nam->get(request);
|
||||
|
||||
if (saveResults) {
|
||||
// This will write out to the file (used for spoiler.xml)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../network/update/client/release_channel.h"
|
||||
#include "card_counter_settings.h"
|
||||
#include "version_string.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QApplication>
|
||||
@@ -198,7 +199,13 @@ SettingsCache::SettingsCache()
|
||||
|
||||
mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool();
|
||||
|
||||
checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
|
||||
if (settings->contains("personal/startupUpdateCheck")) {
|
||||
checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
|
||||
} else if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
|
||||
checkUpdatesOnStartup = false; // do not run auto updater on custom version
|
||||
} else {
|
||||
checkUpdatesOnStartup = true; // default to run auto updater
|
||||
}
|
||||
startupCardUpdateCheckPromptForUpdate =
|
||||
settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool();
|
||||
startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool();
|
||||
@@ -206,7 +213,15 @@ SettingsCache::SettingsCache()
|
||||
lastCardUpdateCheck = settings->value("personal/lastCardUpdateCheck", QDateTime::currentDateTime().date()).toDate();
|
||||
notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool();
|
||||
notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool();
|
||||
updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt();
|
||||
|
||||
if (settings->contains("personal/updatereleasechannel")) {
|
||||
updateReleaseChannel = settings->value("personal/updatereleasechannel").toInt();
|
||||
} else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) {
|
||||
// default to beta if this is a beta release
|
||||
updateReleaseChannel = 1;
|
||||
} else {
|
||||
updateReleaseChannel = 0; // stable
|
||||
}
|
||||
|
||||
lang = settings->value("personal/lang").toString();
|
||||
keepalive = settings->value("personal/keepalive", 3).toInt();
|
||||
@@ -224,6 +239,7 @@ SettingsCache::SettingsCache()
|
||||
|
||||
homeTabBackgroundSource = settings->value("home/background", "themed").toString();
|
||||
homeTabBackgroundShuffleFrequency = settings->value("home/background/shuffleTimer", 0).toInt();
|
||||
homeTabDisplayCardName = settings->value("home/background/displayCardName", true).toBool();
|
||||
|
||||
tabVisualDeckStorageOpen = settings->value("tabs/visualDeckStorage", true).toBool();
|
||||
tabServerOpen = settings->value("tabs/server", true).toBool();
|
||||
@@ -273,6 +289,7 @@ SettingsCache::SettingsCache()
|
||||
focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool();
|
||||
|
||||
showShortcuts = settings->value("menu/showshortcuts", true).toBool();
|
||||
showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool();
|
||||
displayCardNames = settings->value("cards/displaycardnames", true).toBool();
|
||||
roundCardCorners = settings->value("cards/roundcardcorners", true).toBool();
|
||||
overrideAllCardArtWithPersonalPreference =
|
||||
@@ -578,6 +595,13 @@ void SettingsCache::setHomeTabBackgroundShuffleFrequency(int _frequency)
|
||||
emit homeTabBackgroundShuffleFrequencyChanged();
|
||||
}
|
||||
|
||||
void SettingsCache::setHomeTabDisplayCardName(QT_STATE_CHANGED_T _displayCardName)
|
||||
{
|
||||
homeTabDisplayCardName = static_cast<bool>(_displayCardName);
|
||||
settings->setValue("home/background/displayCardName", homeTabDisplayCardName);
|
||||
emit homeTabDisplayCardNameChanged();
|
||||
}
|
||||
|
||||
void SettingsCache::setTabVisualDeckStorageOpen(bool value)
|
||||
{
|
||||
tabVisualDeckStorageOpen = value;
|
||||
@@ -700,6 +724,13 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts)
|
||||
settings->setValue("menu/showshortcuts", showShortcuts);
|
||||
}
|
||||
|
||||
void SettingsCache::setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar)
|
||||
{
|
||||
showGameSelectorFilterToolbar = static_cast<bool>(_showGameSelectorFilterToolbar);
|
||||
settings->setValue("menu/showgameselectorfiltertoolbar", showGameSelectorFilterToolbar);
|
||||
emit showGameSelectorFilterToolbarChanged(showGameSelectorFilterToolbar);
|
||||
}
|
||||
|
||||
void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames)
|
||||
{
|
||||
displayCardNames = static_cast<bool>(_displayCardNames);
|
||||
|
||||
@@ -143,8 +143,10 @@ signals:
|
||||
void themeChanged();
|
||||
void homeTabBackgroundSourceChanged();
|
||||
void homeTabBackgroundShuffleFrequencyChanged();
|
||||
void homeTabDisplayCardNameChanged();
|
||||
void picDownloadChanged();
|
||||
void showStatusBarChanged(bool state);
|
||||
void showGameSelectorFilterToolbarChanged(bool state);
|
||||
void displayCardNamesChanged();
|
||||
void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference);
|
||||
void bumpSetsWithCardsInDeckToTopChanged();
|
||||
@@ -221,6 +223,7 @@ private:
|
||||
bool showTipsOnStartup;
|
||||
QList<int> seenTips;
|
||||
int homeTabBackgroundShuffleFrequency;
|
||||
bool homeTabDisplayCardName;
|
||||
bool mbDownloadSpoilers;
|
||||
int updateReleaseChannel;
|
||||
int maxFontSize;
|
||||
@@ -236,6 +239,7 @@ private:
|
||||
bool annotateTokens;
|
||||
QByteArray tabGameSplitterSizes;
|
||||
bool showShortcuts;
|
||||
bool showGameSelectorFilterToolbar;
|
||||
bool displayCardNames;
|
||||
bool overrideAllCardArtWithPersonalPreference;
|
||||
bool bumpSetsWithCardsInDeckToTop;
|
||||
@@ -411,6 +415,10 @@ public:
|
||||
{
|
||||
return homeTabBackgroundShuffleFrequency;
|
||||
}
|
||||
[[nodiscard]] bool getHomeTabDisplayCardName() const
|
||||
{
|
||||
return homeTabDisplayCardName;
|
||||
}
|
||||
[[nodiscard]] bool getTabVisualDeckStorageOpen() const
|
||||
{
|
||||
return tabVisualDeckStorageOpen;
|
||||
@@ -553,6 +561,10 @@ public:
|
||||
{
|
||||
return showShortcuts;
|
||||
}
|
||||
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const
|
||||
{
|
||||
return showGameSelectorFilterToolbar;
|
||||
}
|
||||
[[nodiscard]] bool getDisplayCardNames() const
|
||||
{
|
||||
return displayCardNames;
|
||||
@@ -995,6 +1007,7 @@ public slots:
|
||||
void setThemeName(const QString &_themeName);
|
||||
void setHomeTabBackgroundSource(const QString &_backgroundSource);
|
||||
void setHomeTabBackgroundShuffleFrequency(int _frequency);
|
||||
void setHomeTabDisplayCardName(QT_STATE_CHANGED_T _displayCardName);
|
||||
void setTabVisualDeckStorageOpen(bool value);
|
||||
void setTabServerOpen(bool value);
|
||||
void setTabAccountOpen(bool value);
|
||||
@@ -1017,6 +1030,7 @@ public slots:
|
||||
void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens);
|
||||
void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes);
|
||||
void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts);
|
||||
void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar);
|
||||
void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames);
|
||||
void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt);
|
||||
void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop);
|
||||
|
||||
@@ -150,12 +150,7 @@ void ShortcutTreeView::currentChanged(const QModelIndex ¤t, const QModelIn
|
||||
*/
|
||||
void ShortcutTreeView::updateSearchString(const QString &searchString)
|
||||
{
|
||||
#if QT_VERSION > QT_VERSION_CHECK(5, 14, 0)
|
||||
const auto skipEmptyParts = Qt::SkipEmptyParts;
|
||||
#else
|
||||
const auto skipEmptyParts = QString::SkipEmptyParts;
|
||||
#endif
|
||||
QStringList searchWords = searchString.split(" ", skipEmptyParts);
|
||||
QStringList searchWords = searchString.split(" ", Qt::SkipEmptyParts);
|
||||
|
||||
auto escapeRegex = [](const QString &s) { return QRegularExpression::escape(s); };
|
||||
std::transform(searchWords.begin(), searchWords.end(), searchWords.begin(), escapeRegex);
|
||||
|
||||
@@ -660,6 +660,12 @@ private:
|
||||
{"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"),
|
||||
parseSequenceString("Ctrl+M"),
|
||||
ShortcutGroup::Drawing)},
|
||||
{"Player/aMulliganSame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Same hand size)"),
|
||||
parseSequenceString("Ctrl+Shift+M"),
|
||||
ShortcutGroup::Drawing)},
|
||||
{"Player/aMulliganMinusOne", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Hand size - 1)"),
|
||||
parseSequenceString("Ctrl+Shift+Alt+M"),
|
||||
ShortcutGroup::Drawing)},
|
||||
{"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"),
|
||||
parseSequenceString("Ctrl+D"),
|
||||
ShortcutGroup::Drawing)},
|
||||
|
||||
@@ -13,7 +13,7 @@ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
|
||||
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
|
||||
|
||||
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
|
||||
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / CommentQuery / GenericQuery
|
||||
|
||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||
|
||||
@@ -22,8 +22,10 @@ CardSearch <- '[[' CardFilterString ']]'
|
||||
CardFilterString <- (!']]'.)*
|
||||
|
||||
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
|
||||
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
|
||||
FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String
|
||||
PathQuery <- [Pp] 'ath'? [:] String
|
||||
FormatQuery <- [Ff] 'ormat'? [:] String
|
||||
CommentQuery <- [Cc] ('omment' 's'?)? [:] String
|
||||
|
||||
GenericQuery <- String
|
||||
|
||||
@@ -118,12 +120,13 @@ static void setupParserRules()
|
||||
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
|
||||
int count = 0;
|
||||
deck->deckLoader->getDeckList()->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
|
||||
auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes();
|
||||
for (auto node : cardNodes) {
|
||||
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
||||
count += node->getNumber();
|
||||
}
|
||||
});
|
||||
}
|
||||
return numberMatcher(count);
|
||||
};
|
||||
};
|
||||
@@ -137,7 +140,7 @@ static void setupParserRules()
|
||||
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->deckLoader->getDeckList()->getName().contains(name, Qt::CaseInsensitive);
|
||||
return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -156,6 +159,22 @@ static void setupParserRules()
|
||||
};
|
||||
};
|
||||
|
||||
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto format = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat();
|
||||
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
|
||||
};
|
||||
};
|
||||
|
||||
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto value = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto comments = deck->deckLoader->getDeck().deckList.getComments();
|
||||
return comments.contains(value, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
|
||||
@@ -343,10 +343,7 @@ void DeckViewScene::rebuildTree()
|
||||
if (!deck)
|
||||
return;
|
||||
|
||||
InnerDecklistNode *listRoot = deck->getRoot();
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
|
||||
for (auto *currentZone : deck->getZoneNodes()) {
|
||||
DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0);
|
||||
if (!container) {
|
||||
container = new DeckViewCardContainer(currentZone->getName());
|
||||
|
||||
@@ -259,7 +259,7 @@ void DeckViewContainer::loadLocalDeck()
|
||||
|
||||
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
||||
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath);
|
||||
DeckLoader deck(this);
|
||||
|
||||
bool success = deck.loadFromFile(filePath, fmt, true);
|
||||
@@ -269,12 +269,12 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||
return;
|
||||
}
|
||||
|
||||
loadDeckFromDeckLoader(&deck);
|
||||
loadDeckFromDeckList(deck.getDeck().deckList);
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadDeckFromDeckLoader(DeckLoader *deck)
|
||||
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||
{
|
||||
QString deckString = deck->getDeckList()->writeToString_Native();
|
||||
QString deckString = deck.writeToString_Native();
|
||||
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size."));
|
||||
@@ -308,8 +308,8 @@ void DeckViewContainer::loadFromClipboard()
|
||||
return;
|
||||
}
|
||||
|
||||
DeckLoader *deck = dlg.getDeckList();
|
||||
loadDeckFromDeckLoader(deck);
|
||||
DeckList deck = dlg.getDeckList();
|
||||
loadDeckFromDeckList(deck);
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadFromWebsite()
|
||||
@@ -320,16 +320,15 @@ void DeckViewContainer::loadFromWebsite()
|
||||
return;
|
||||
}
|
||||
|
||||
DeckLoader *deck = dlg.getDeck();
|
||||
loadDeckFromDeckLoader(deck);
|
||||
DeckList deck = dlg.getDeck();
|
||||
loadDeckFromDeckList(deck);
|
||||
}
|
||||
|
||||
void DeckViewContainer::deckSelectFinished(const Response &r)
|
||||
{
|
||||
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
|
||||
DeckLoader newDeck(this, new DeckList(QString::fromStdString(resp.deck())));
|
||||
CardPictureLoader::cacheCardPixmaps(
|
||||
CardDatabaseManager::query()->getCards(newDeck.getDeckList()->getCardRefList()));
|
||||
DeckList newDeck = DeckList(QString::fromStdString(resp.deck()));
|
||||
CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(newDeck.getCardRefList()));
|
||||
setDeck(newDeck);
|
||||
switchToDeckLoadedView();
|
||||
}
|
||||
@@ -410,8 +409,8 @@ void DeckViewContainer::setSideboardLocked(bool locked)
|
||||
deckView->resetSideboardPlan();
|
||||
}
|
||||
|
||||
void DeckViewContainer::setDeck(DeckLoader &deck)
|
||||
void DeckViewContainer::setDeck(const DeckList &deck)
|
||||
{
|
||||
deckView->setDeck(*deck.getDeckList());
|
||||
deckView->setDeck(deck);
|
||||
switchToDeckLoadedView();
|
||||
}
|
||||
@@ -85,12 +85,12 @@ public:
|
||||
void setReadyStart(bool ready);
|
||||
void readyAndUpdate();
|
||||
void setSideboardLocked(bool locked);
|
||||
void setDeck(DeckLoader &deck);
|
||||
void setDeck(const DeckList &deck);
|
||||
void setVisualDeckStorageExists(bool exists);
|
||||
|
||||
public slots:
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
void loadDeckFromDeckLoader(DeckLoader *deck);
|
||||
void loadDeckFromDeckList(const DeckList &deck);
|
||||
};
|
||||
|
||||
#endif // DECK_VIEW_CONTAINER_H
|
||||
|
||||
@@ -117,11 +117,7 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
|
||||
chooseTokenFromDeckRadioButton->setDisabled(true); // No tokens in deck = no need for option
|
||||
} else {
|
||||
chooseTokenFromDeckRadioButton->setChecked(true);
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>(predefinedTokens.begin(), predefinedTokens.end()));
|
||||
#else
|
||||
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>::fromList(predefinedTokens));
|
||||
#endif
|
||||
}
|
||||
|
||||
auto *tokenChooseLayout = new QVBoxLayout;
|
||||
@@ -223,11 +219,7 @@ void DlgCreateToken::actChooseTokenFromAll(bool checked)
|
||||
void DlgCreateToken::actChooseTokenFromDeck(bool checked)
|
||||
{
|
||||
if (checked) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>(predefinedTokens.begin(), predefinedTokens.end()));
|
||||
#else
|
||||
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>::fromList(predefinedTokens));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,16 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T
|
||||
connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan);
|
||||
addAction(aMulligan);
|
||||
|
||||
// Mulligan same size
|
||||
aMulliganSame = new QAction(this);
|
||||
connect(aMulliganSame, &QAction::triggered, actions, &PlayerActions::actMulliganSameSize);
|
||||
addAction(aMulliganSame);
|
||||
|
||||
// Mulligan -1
|
||||
aMulliganMinusOne = new QAction(this);
|
||||
connect(aMulliganMinusOne, &QAction::triggered, actions, &PlayerActions::actMulliganMinusOne);
|
||||
addAction(aMulliganMinusOne);
|
||||
|
||||
addSeparator();
|
||||
|
||||
mMoveHandMenu = addTearOffMenu(QString());
|
||||
@@ -104,7 +114,9 @@ void HandMenu::retranslateUi()
|
||||
aSortHandByType->setText(tr("Type"));
|
||||
aSortHandByManaValue->setText(tr("Mana Value"));
|
||||
|
||||
aMulligan->setText(tr("Take &mulligan"));
|
||||
aMulligan->setText(tr("Take &mulligan (Choose hand size)"));
|
||||
aMulliganSame->setText(tr("Take mulligan (Same hand size)"));
|
||||
aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)"));
|
||||
|
||||
mMoveHandMenu->setTitle(tr("&Move hand to..."));
|
||||
aMoveHandToTopLibrary->setText(tr("&Top of library"));
|
||||
@@ -128,6 +140,8 @@ void HandMenu::setShortcutsActive()
|
||||
aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType"));
|
||||
aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue"));
|
||||
aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan"));
|
||||
aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame"));
|
||||
aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne"));
|
||||
aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll"));
|
||||
aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll"));
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ private:
|
||||
|
||||
QAction *aViewHand = nullptr;
|
||||
QAction *aMulligan = nullptr;
|
||||
QAction *aMulliganSame = nullptr;
|
||||
QAction *aMulliganMinusOne = nullptr;
|
||||
|
||||
QMenu *mSortHand = nullptr;
|
||||
QAction *aSortHandByName = nullptr;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../player_actions.h"
|
||||
#include "player_menu.h"
|
||||
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
UtilityMenu::UtilityMenu(Player *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
|
||||
@@ -59,21 +60,19 @@ void UtilityMenu::populatePredefinedTokensMenu()
|
||||
clear();
|
||||
setEnabled(false);
|
||||
predefinedTokens.clear();
|
||||
DeckLoader *_deck = player->getDeck();
|
||||
const DeckList &deckList = player->getDeck();
|
||||
|
||||
if (!_deck) {
|
||||
if (deckList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
InnerDecklistNode *tokenZone =
|
||||
dynamic_cast<InnerDecklistNode *>(_deck->getDeckList()->getRoot()->findChild(DECK_ZONE_TOKENS));
|
||||
auto tokenCardNodes = deckList.getCardNodes({DECK_ZONE_TOKENS});
|
||||
|
||||
if (tokenZone) {
|
||||
if (!tokenZone->empty())
|
||||
setEnabled(true);
|
||||
if (!tokenCardNodes.isEmpty()) {
|
||||
setEnabled(true);
|
||||
|
||||
for (int i = 0; i < tokenZone->size(); ++i) {
|
||||
const QString tokenName = tokenZone->at(i)->getName();
|
||||
for (int i = 0; i < tokenCardNodes.size(); ++i) {
|
||||
const QString tokenName = tokenCardNodes[i]->getName();
|
||||
predefinedTokens.append(tokenName);
|
||||
QAction *a = addAction(tokenName);
|
||||
if (i < 10) {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent)
|
||||
: QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)),
|
||||
playerEventHandler(new PlayerEventHandler(this)), playerActions(new PlayerActions(this)), active(false),
|
||||
conceded(false), deck(nullptr), zoneId(0), dialogSemaphore(false)
|
||||
conceded(false), zoneId(0), dialogSemaphore(false)
|
||||
{
|
||||
initializeZones();
|
||||
|
||||
@@ -263,10 +263,9 @@ void Player::deleteCard(CardItem *card)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Does a player need a DeckLoader?
|
||||
void Player::setDeck(DeckLoader &_deck)
|
||||
void Player::setDeck(const DeckList &_deck)
|
||||
{
|
||||
deck = new DeckLoader(this, _deck.getDeckList());
|
||||
deck = _deck;
|
||||
|
||||
emit deckChanged();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "../../game_graphics/board/abstract_graphics_item.h"
|
||||
#include "../../interface/widgets/menus/tearoff_menu.h"
|
||||
#include "../interface/deck_loader/loaded_deck.h"
|
||||
#include "../zones/logic/hand_zone_logic.h"
|
||||
#include "../zones/logic/pile_zone_logic.h"
|
||||
#include "../zones/logic/stack_zone_logic.h"
|
||||
@@ -44,7 +45,6 @@ class ArrowTarget;
|
||||
class CardDatabase;
|
||||
class CardZone;
|
||||
class CommandContainer;
|
||||
class DeckLoader;
|
||||
class GameCommand;
|
||||
class GameEvent;
|
||||
class PlayerInfo;
|
||||
@@ -66,7 +66,7 @@ class Player : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void openDeckEditor(DeckLoader *deck);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
void deckChanged();
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void rearrangeCounters();
|
||||
@@ -130,9 +130,9 @@ public:
|
||||
return playerMenu;
|
||||
}
|
||||
|
||||
void setDeck(DeckLoader &_deck);
|
||||
void setDeck(const DeckList &_deck);
|
||||
|
||||
[[nodiscard]] DeckLoader *getDeck() const
|
||||
[[nodiscard]] const DeckList &getDeck() const
|
||||
{
|
||||
return deck;
|
||||
}
|
||||
@@ -241,7 +241,7 @@ private:
|
||||
bool active;
|
||||
bool conceded;
|
||||
|
||||
DeckLoader *deck;
|
||||
DeckList deck;
|
||||
|
||||
int zoneId;
|
||||
QMap<QString, CardZoneLogic *> zones;
|
||||
|
||||
@@ -218,7 +218,7 @@ void PlayerActions::actAlwaysLookAtTopCard()
|
||||
|
||||
void PlayerActions::actOpenDeckInDeckEditor()
|
||||
{
|
||||
emit player->openDeckEditor(player->getDeck());
|
||||
emit player->openDeckEditor({.deckList = player->getDeck()});
|
||||
}
|
||||
|
||||
void PlayerActions::actViewGraveyard()
|
||||
@@ -310,28 +310,48 @@ void PlayerActions::actMulligan()
|
||||
{
|
||||
int startSize = SettingsCache::instance().getStartingHandSize();
|
||||
int handSize = player->getHandZone()->getCards().size();
|
||||
int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck
|
||||
int deckSize = player->getDeckZone()->getCards().size() + handSize;
|
||||
|
||||
bool ok;
|
||||
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"),
|
||||
tr("Number of cards: (max. %1)").arg(deckSize) + '\n' +
|
||||
tr("0 and lower are in comparison to current hand size"),
|
||||
startSize, -handSize, deckSize, 1, &ok);
|
||||
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
Command_Mulligan cmd;
|
||||
|
||||
if (number < 1) {
|
||||
if (handSize == 0) {
|
||||
return;
|
||||
}
|
||||
cmd.set_number(handSize + number);
|
||||
} else {
|
||||
cmd.set_number(number);
|
||||
number = handSize + number;
|
||||
}
|
||||
|
||||
doMulligan(number);
|
||||
SettingsCache::instance().setStartingHandSize(number);
|
||||
}
|
||||
|
||||
void PlayerActions::actMulliganSameSize()
|
||||
{
|
||||
int handSize = player->getHandZone()->getCards().size();
|
||||
doMulligan(handSize);
|
||||
}
|
||||
|
||||
void PlayerActions::actMulliganMinusOne()
|
||||
{
|
||||
int handSize = player->getHandZone()->getCards().size();
|
||||
int targetSize = qMax(1, handSize - 1);
|
||||
doMulligan(targetSize);
|
||||
}
|
||||
|
||||
void PlayerActions::doMulligan(int number)
|
||||
{
|
||||
if (number < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_Mulligan cmd;
|
||||
cmd.set_number(number);
|
||||
sendGameCommand(cmd);
|
||||
if (startSize != number) {
|
||||
SettingsCache::instance().setStartingHandSize(number);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerActions::actDrawCards()
|
||||
|
||||
@@ -85,6 +85,9 @@ public slots:
|
||||
void actDrawCards();
|
||||
void actUndoDraw();
|
||||
void actMulligan();
|
||||
void actMulliganSameSize();
|
||||
void actMulliganMinusOne();
|
||||
void doMulligan(int number);
|
||||
|
||||
void actPlay();
|
||||
void actPlayFacedown();
|
||||
|
||||
@@ -78,14 +78,7 @@ void PlayerEventHandler::eventShuffle(const Event_Shuffle &event)
|
||||
void PlayerEventHandler::eventRollDie(const Event_RollDie &event)
|
||||
{
|
||||
if (!event.values().empty()) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
QList<uint> rolls(event.values().begin(), event.values().end());
|
||||
#else
|
||||
QList<uint> rolls;
|
||||
for (const auto &value : event.values()) {
|
||||
rolls.append(value);
|
||||
}
|
||||
#endif
|
||||
std::sort(rolls.begin(), rolls.end());
|
||||
emit logRollDie(player, static_cast<int>(event.sides()), rolls);
|
||||
} else if (event.value()) {
|
||||
|
||||
@@ -13,12 +13,20 @@
|
||||
#include <QGraphicsLinearLayout>
|
||||
#include <QGraphicsProxyWidget>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QScrollBar>
|
||||
#include <QStyle>
|
||||
#include <QStyleOption>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qreal kTitleBarHeight = 24.0;
|
||||
constexpr qreal kMinVisibleWidth = 100.0;
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @param _player the player the cards were revealed to.
|
||||
* @param _origZone the zone the cards were revealed from.
|
||||
@@ -241,33 +249,191 @@ void ZoneViewWidget::retranslateUi()
|
||||
pileViewCheckBox.setText(tr("pile view"));
|
||||
}
|
||||
|
||||
void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */)
|
||||
void ZoneViewWidget::stopWindowDrag()
|
||||
{
|
||||
if (!scene())
|
||||
if (!draggingWindow)
|
||||
return;
|
||||
|
||||
int titleBarHeight = 24;
|
||||
draggingWindow = false;
|
||||
ungrabMouse();
|
||||
}
|
||||
|
||||
QPointF scenePos = pos();
|
||||
void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
draggingWindow = true;
|
||||
dragStartItemPos = pos();
|
||||
dragStartScreenPos = event->screenPos();
|
||||
dragView = findDragView(event->widget());
|
||||
|
||||
if (scenePos.x() < 0) {
|
||||
scenePos.setX(0);
|
||||
} else {
|
||||
qreal maxw = scene()->sceneRect().width() - 100;
|
||||
if (scenePos.x() > maxw)
|
||||
scenePos.setX(maxw);
|
||||
// need to grab mouse to receive events and not miss initial movement
|
||||
grabMouse();
|
||||
}
|
||||
|
||||
QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
|
||||
{
|
||||
const QRectF frameRectF = windowFrameRect();
|
||||
|
||||
// query the style for the close button position (handles macOS top-left placement)
|
||||
// Title bar rect MUST be local (0,0-based) for QStyle
|
||||
const QRect titleBarRect(0, 0, static_cast<int>(frameRectF.width()), static_cast<int>(kTitleBarHeight));
|
||||
|
||||
if (styleWidget) {
|
||||
QStyleOptionTitleBar opt;
|
||||
opt.initFrom(styleWidget);
|
||||
opt.rect = titleBarRect;
|
||||
opt.text = windowTitle();
|
||||
opt.icon = styleWidget->windowIcon();
|
||||
opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;
|
||||
|
||||
opt.subControls = QStyle::SC_TitleBarCloseButton;
|
||||
opt.activeSubControls = QStyle::SC_TitleBarCloseButton;
|
||||
opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState;
|
||||
|
||||
if (styleWidget->isActiveWindow()) {
|
||||
opt.state |= QStyle::State_Active;
|
||||
}
|
||||
|
||||
QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton,
|
||||
styleWidget);
|
||||
|
||||
if (r.isValid() && !r.isEmpty()) {
|
||||
// Translate from local-titlebar coords → frame coords
|
||||
r.translate(frameRectF.topLeft().toPoint());
|
||||
return QRectF(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (scenePos.y() < titleBarHeight) {
|
||||
scenePos.setY(titleBarHeight);
|
||||
} else {
|
||||
qreal maxh = scene()->sceneRect().height() - titleBarHeight;
|
||||
if (scenePos.y() > maxh)
|
||||
scenePos.setY(maxh);
|
||||
// Fallback: frame-relative top-right
|
||||
return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight);
|
||||
}
|
||||
|
||||
QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const
|
||||
{
|
||||
QWidget *current = eventWidget;
|
||||
while (current) {
|
||||
if (auto *view = qobject_cast<QGraphicsView *>(current))
|
||||
return view;
|
||||
current = current->parentWidget();
|
||||
}
|
||||
|
||||
if (scenePos != pos())
|
||||
setPos(scenePos);
|
||||
if (scene() && !scene()->views().isEmpty())
|
||||
return scene()->views().constFirst();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QPointF ZoneViewWidget::calcDraggedWindowPos(const QPoint &screenPos,
|
||||
const QPointF &scenePos,
|
||||
const QPointF &buttonDownScenePos) const
|
||||
{
|
||||
if (dragView && dragView->viewport()) {
|
||||
const QPoint vpStart = dragView->viewport()->mapFromGlobal(dragStartScreenPos);
|
||||
const QPoint vpNow = dragView->viewport()->mapFromGlobal(screenPos);
|
||||
const QPointF sceneStart = dragView->mapToScene(vpStart);
|
||||
const QPointF sceneNow = dragView->mapToScene(vpNow);
|
||||
return dragStartItemPos + (sceneNow - sceneStart);
|
||||
}
|
||||
|
||||
return dragStartItemPos + (scenePos - buttonDownScenePos);
|
||||
}
|
||||
|
||||
bool ZoneViewWidget::windowFrameEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::UngrabMouse) {
|
||||
stopWindowDrag();
|
||||
return QGraphicsWidget::windowFrameEvent(event);
|
||||
}
|
||||
|
||||
auto *me = dynamic_cast<QGraphicsSceneMouseEvent *>(event);
|
||||
if (!me)
|
||||
return QGraphicsWidget::windowFrameEvent(event);
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::GraphicsSceneMousePress:
|
||||
if (me->button() == Qt::LeftButton && windowFrameSectionAt(me->pos()) == Qt::TitleBarArea) {
|
||||
// avoid drag on close button
|
||||
if (closeButtonRect(me->widget()).contains(me->pos())) {
|
||||
me->accept();
|
||||
close();
|
||||
return true;
|
||||
}
|
||||
|
||||
startWindowDrag(me);
|
||||
me->accept();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case QEvent::GraphicsSceneMouseMove:
|
||||
if (draggingWindow) {
|
||||
if (!(me->buttons() & Qt::LeftButton)) {
|
||||
stopWindowDrag();
|
||||
} else {
|
||||
setPos(
|
||||
calcDraggedWindowPos(me->screenPos(), me->scenePos(), me->buttonDownScenePos(Qt::LeftButton)));
|
||||
}
|
||||
me->accept();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case QEvent::GraphicsSceneMouseRelease:
|
||||
if (draggingWindow && me->button() == Qt::LeftButton) {
|
||||
stopWindowDrag();
|
||||
me->accept();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QGraphicsWidget::windowFrameEvent(event);
|
||||
}
|
||||
|
||||
void ZoneViewWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
// move if the scene routes moves while dragging
|
||||
if (draggingWindow && (event->buttons() & Qt::LeftButton)) {
|
||||
setPos(calcDraggedWindowPos(event->screenPos(), event->scenePos(), event->buttonDownScenePos(Qt::LeftButton)));
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
QGraphicsWidget::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void ZoneViewWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (draggingWindow && event->button() == Qt::LeftButton) {
|
||||
stopWindowDrag();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
QGraphicsWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
QVariant ZoneViewWidget::itemChange(GraphicsItemChange change, const QVariant &value)
|
||||
{
|
||||
if (change == QGraphicsItem::ItemPositionChange && scene()) {
|
||||
// Keep grab area in main view
|
||||
const QRectF sceneRect = scene()->sceneRect();
|
||||
const QPointF requestedPos = value.toPointF();
|
||||
QPointF desiredPos = requestedPos;
|
||||
|
||||
const qreal minX = sceneRect.left();
|
||||
const qreal maxX = qMax(minX, sceneRect.right() - kMinVisibleWidth);
|
||||
const qreal minY = sceneRect.top() + kTitleBarHeight;
|
||||
const qreal maxY = qMax(minY, sceneRect.bottom() - kTitleBarHeight);
|
||||
|
||||
desiredPos.setX(qBound(minX, desiredPos.x(), maxX));
|
||||
desiredPos.setY(qBound(minY, desiredPos.y(), maxY));
|
||||
return desiredPos;
|
||||
}
|
||||
|
||||
return QGraphicsWidget::itemChange(change, value);
|
||||
}
|
||||
|
||||
void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event)
|
||||
@@ -350,6 +516,7 @@ void ZoneViewWidget::handleScrollBarChange(int value)
|
||||
|
||||
void ZoneViewWidget::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
stopWindowDrag();
|
||||
disconnect(zone, &ZoneViewZone::closed, this, 0);
|
||||
// manually call zone->close in order to remove it from the origZones views
|
||||
zone->close();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* @ingroup GameGraphicsZones
|
||||
* @brief TODO: Document this.
|
||||
*/
|
||||
|
||||
#ifndef ZONEVIEWWIDGET_H
|
||||
#define ZONEVIEWWIDGET_H
|
||||
|
||||
@@ -14,6 +13,7 @@
|
||||
#include <QGraphicsProxyWidget>
|
||||
#include <QGraphicsWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QPointer>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
||||
class QLabel;
|
||||
@@ -28,6 +28,8 @@ class ServerInfo_Card;
|
||||
class QGraphicsSceneMouseEvent;
|
||||
class QGraphicsSceneWheelEvent;
|
||||
class QStyleOption;
|
||||
class QGraphicsView;
|
||||
class QWidget;
|
||||
|
||||
class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget
|
||||
{
|
||||
@@ -52,7 +54,6 @@ private:
|
||||
ZoneViewZone *zone;
|
||||
QGraphicsWidget *zoneContainer;
|
||||
|
||||
QPushButton *closeButton;
|
||||
QScrollBar *scrollBar;
|
||||
ScrollableGraphicsProxyWidget *scrollBarProxy;
|
||||
|
||||
@@ -66,6 +67,33 @@ private:
|
||||
int extraHeight;
|
||||
Player *player;
|
||||
|
||||
bool draggingWindow = false;
|
||||
QPoint dragStartScreenPos;
|
||||
QPointF dragStartItemPos;
|
||||
QPointer<QGraphicsView> dragView;
|
||||
|
||||
void stopWindowDrag();
|
||||
void startWindowDrag(QGraphicsSceneMouseEvent *event);
|
||||
QRectF closeButtonRect(QWidget *styleWidget) const;
|
||||
/**
|
||||
* @brief Resolves the QGraphicsView to use for drag coordinate mapping
|
||||
*
|
||||
* @param eventWidget QWidget that originated the mouse event
|
||||
* @return The resolved QGraphicsView
|
||||
*/
|
||||
QGraphicsView *findDragView(QWidget *eventWidget) const;
|
||||
/**
|
||||
* @brief Calculates the desired widget position while dragging
|
||||
*
|
||||
* @param screenPos Global screen coordinates of the current mouse position
|
||||
* @param scenePos Scene coordinates of the current mouse position
|
||||
* @param buttonDownScenePos Scene coordinates of the initial mouse press position
|
||||
*
|
||||
* @return The new widget position in scene coordinates
|
||||
*/
|
||||
QPointF
|
||||
calcDraggedWindowPos(const QPoint &screenPos, const QPointF &scenePos, const QPointF &buttonDownScenePos) const;
|
||||
|
||||
void resizeScrollbar(qreal newZoneHeight);
|
||||
signals:
|
||||
void closePressed(ZoneViewWidget *zv);
|
||||
@@ -76,7 +104,6 @@ private slots:
|
||||
void resizeToZoneContents(bool forceInitialHeight = false);
|
||||
void handleScrollBarChange(int value);
|
||||
void zoneDeleted();
|
||||
void moveEvent(QGraphicsSceneMoveEvent * /* event */) override;
|
||||
void resizeEvent(QGraphicsSceneResizeEvent * /* event */) override;
|
||||
void expandWindow();
|
||||
|
||||
@@ -101,6 +128,10 @@ public:
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void initStyleOption(QStyleOption *option) const override;
|
||||
bool windowFrameEvent(QEvent *event) override;
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,12 +17,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number,
|
||||
font.setWeight(QFont::Bold);
|
||||
|
||||
QFontMetrics fm(font);
|
||||
double w = 1.3 *
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
|
||||
fm.horizontalAdvance(numStr);
|
||||
#else
|
||||
fm.width(numStr);
|
||||
#endif
|
||||
double w = 1.3 * fm.horizontalAdvance(numStr);
|
||||
double h = fm.height() * 1.3;
|
||||
if (w < h)
|
||||
w = h;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <utility>
|
||||
#include <version_string.h>
|
||||
|
||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||
|
||||
@@ -20,9 +21,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
|
||||
// We need a timeout to ensure requests don't hang indefinitely in case of
|
||||
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
|
||||
// Use Qt's default timeout (30s, as of 2023-02-22)
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout();
|
||||
#endif
|
||||
cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
@@ -86,6 +85,7 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||
}
|
||||
|
||||
QNetworkRequest req(url);
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
if (!picDownload) {
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||
}
|
||||
|
||||
40
cockatrice/src/interface/deck_loader/card_node_function.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "card_node_function.h"
|
||||
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
|
||||
void CardNodeFunction::SetProviderIdToPreferred::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
|
||||
QString providerId = preferredPrinting.getUuid();
|
||||
QString setShortName = preferredPrinting.getSet()->getShortName();
|
||||
QString collectorNumber = preferredPrinting.getProperty("num");
|
||||
|
||||
card->setCardProviderId(providerId);
|
||||
card->setCardCollectorNumber(collectorNumber);
|
||||
card->setCardSetShortName(setShortName);
|
||||
}
|
||||
|
||||
void CardNodeFunction::ClearPrintingData::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
card->setCardSetShortName(nullptr);
|
||||
card->setCardCollectorNumber(nullptr);
|
||||
card->setCardProviderId(nullptr);
|
||||
}
|
||||
|
||||
void CardNodeFunction::ResolveProviderId::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
// Retrieve the providerId based on setName and collectorNumber
|
||||
QString providerId =
|
||||
CardDatabaseManager::getInstance()
|
||||
->query()
|
||||
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
|
||||
.getUuid();
|
||||
|
||||
// Set the providerId on the card
|
||||
card->setCardProviderId(providerId);
|
||||
}
|
||||
39
cockatrice/src/interface/deck_loader/card_node_function.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef COCKATRICE_DECK_FUNCTION_H
|
||||
#define COCKATRICE_DECK_FUNCTION_H
|
||||
|
||||
class DecklistCardNode;
|
||||
class InnerDecklistNode;
|
||||
|
||||
/**
|
||||
* Functions to be used with DeckList::forEachCard
|
||||
*/
|
||||
namespace CardNodeFunction
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Sets the providerId of the card to the preferred printing.
|
||||
*/
|
||||
struct SetProviderIdToPreferred
|
||||
{
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Clears all fields on the card related to the printing
|
||||
*/
|
||||
struct ClearPrintingData
|
||||
{
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Sets the providerId of the card based on its set name and collector number.
|
||||
*/
|
||||
struct ResolveProviderId
|
||||
{
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
|
||||
};
|
||||
|
||||
} // namespace CardNodeFunction
|
||||
|
||||
#endif // COCKATRICE_DECK_FUNCTION_H
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "deck_file_format.h"
|
||||
|
||||
DeckFileFormat::Format DeckFileFormat::getFormatFromName(const QString &fileName)
|
||||
{
|
||||
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
|
||||
return Cockatrice;
|
||||
}
|
||||
return PlainText;
|
||||
}
|
||||
36
cockatrice/src/interface/deck_loader/deck_file_format.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef COCKATRICE_DECK_FILE_FORMAT_H
|
||||
#define COCKATRICE_DECK_FILE_FORMAT_H
|
||||
#include <QString>
|
||||
|
||||
namespace DeckFileFormat
|
||||
{
|
||||
|
||||
/**
|
||||
* The deck file formats that Cockatrice supports.
|
||||
*/
|
||||
enum Format
|
||||
{
|
||||
/**
|
||||
* Plaintext deck files, a format that is intended to be widely supported among different programs.
|
||||
* This format does not support Cockatrice specific features such as banner cards or tags.
|
||||
*/
|
||||
PlainText,
|
||||
|
||||
/**
|
||||
* This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
|
||||
* Stored as .cod files.
|
||||
*/
|
||||
Cockatrice
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines what deck file format the given filename corresponds to.
|
||||
*
|
||||
* @param fileName The filename
|
||||
* @return The deck format
|
||||
*/
|
||||
Format getFormatFromName(const QString &fileName);
|
||||
|
||||
} // namespace DeckFileFormat
|
||||
|
||||
#endif // COCKATRICE_DECK_FILE_FORMAT_H
|
||||
@@ -25,15 +25,11 @@ const QStringList DeckLoader::ACCEPTED_FILE_EXTENSIONS = {"*.cod", "*.dec", "*.d
|
||||
const QStringList DeckLoader::FILE_NAME_FILTERS = {
|
||||
tr("Common deck formats (%1)").arg(ACCEPTED_FILE_EXTENSIONS.join(" ")), tr("All files (*.*)")};
|
||||
|
||||
DeckLoader::DeckLoader(QObject *parent) : QObject(parent), deckList(new DeckList())
|
||||
DeckLoader::DeckLoader(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLoader::DeckLoader(QObject *parent, DeckList *_deckList) : QObject(parent), deckList(_deckList)
|
||||
{
|
||||
}
|
||||
|
||||
bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest)
|
||||
bool DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
@@ -41,18 +37,19 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
|
||||
}
|
||||
|
||||
bool result = false;
|
||||
DeckList deckList = DeckList();
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
result = deckList->loadFromFile_Plain(&file);
|
||||
case DeckFileFormat::PlainText:
|
||||
result = deckList.loadFromFile_Plain(&file);
|
||||
break;
|
||||
case CockatriceFormat: {
|
||||
result = deckList->loadFromFile_Native(&file);
|
||||
case DeckFileFormat::Cockatrice: {
|
||||
result = deckList.loadFromFile_Native(&file);
|
||||
qCInfo(DeckLoaderLog) << "Loaded from" << fileName << "-" << result;
|
||||
if (!result) {
|
||||
qCInfo(DeckLoaderLog) << "Retrying as plain format";
|
||||
file.seek(0);
|
||||
result = deckList->loadFromFile_Plain(&file);
|
||||
fmt = PlainTextFormat;
|
||||
result = deckList.loadFromFile_Plain(&file);
|
||||
fmt = DeckFileFormat::PlainText;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -62,7 +59,8 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
|
||||
}
|
||||
|
||||
if (result) {
|
||||
lastLoadInfo = {
|
||||
loadedDeck.deckList = deckList;
|
||||
loadedDeck.lastLoadInfo = {
|
||||
.fileName = fileName,
|
||||
.fileFormat = fmt,
|
||||
};
|
||||
@@ -77,7 +75,7 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest)
|
||||
bool DeckLoader::loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
|
||||
{
|
||||
auto *watcher = new QFutureWatcher<bool>(this);
|
||||
|
||||
@@ -86,7 +84,7 @@ bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool
|
||||
watcher->deleteLater();
|
||||
|
||||
if (result) {
|
||||
lastLoadInfo = {
|
||||
loadedDeck.lastLoadInfo = {
|
||||
.fileName = fileName,
|
||||
.fileFormat = fmt,
|
||||
};
|
||||
@@ -106,14 +104,14 @@ bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool
|
||||
}
|
||||
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
return deckList->loadFromFile_Plain(&file);
|
||||
case CockatriceFormat: {
|
||||
case DeckFileFormat::PlainText:
|
||||
return loadedDeck.deckList.loadFromFile_Plain(&file);
|
||||
case DeckFileFormat::Cockatrice: {
|
||||
bool result = false;
|
||||
result = deckList->loadFromFile_Native(&file);
|
||||
result = loadedDeck.deckList.loadFromFile_Native(&file);
|
||||
if (!result) {
|
||||
file.seek(0);
|
||||
return deckList->loadFromFile_Plain(&file);
|
||||
return loadedDeck.deckList.loadFromFile_Plain(&file);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -129,9 +127,9 @@ bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool
|
||||
|
||||
bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
|
||||
{
|
||||
bool result = deckList->loadFromString_Native(nativeString);
|
||||
bool result = loadedDeck.deckList.loadFromString_Native(nativeString);
|
||||
if (result) {
|
||||
lastLoadInfo = {
|
||||
loadedDeck.lastLoadInfo = {
|
||||
.remoteDeckId = remoteDeckId,
|
||||
};
|
||||
|
||||
@@ -140,7 +138,7 @@ bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
|
||||
bool DeckLoader::saveToFile(const QString &fileName, DeckFileFormat::Format fmt)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
@@ -149,17 +147,17 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
|
||||
|
||||
bool result = false;
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
result = deckList->saveToFile_Plain(&file);
|
||||
case DeckFileFormat::PlainText:
|
||||
result = loadedDeck.deckList.saveToFile_Plain(&file);
|
||||
break;
|
||||
case CockatriceFormat:
|
||||
result = deckList->saveToFile_Native(&file);
|
||||
case DeckFileFormat::Cockatrice:
|
||||
result = loadedDeck.deckList.saveToFile_Native(&file);
|
||||
qCInfo(DeckLoaderLog) << "Saving to " << fileName << "-" << result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
lastLoadInfo = {
|
||||
loadedDeck.lastLoadInfo = {
|
||||
.fileName = fileName,
|
||||
.fileFormat = fmt,
|
||||
};
|
||||
@@ -172,7 +170,7 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt)
|
||||
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt)
|
||||
{
|
||||
QFileInfo fileInfo(fileName);
|
||||
if (!fileInfo.exists()) {
|
||||
@@ -193,19 +191,19 @@ bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat f
|
||||
|
||||
// Perform file modifications
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
result = deckList->saveToFile_Plain(&file);
|
||||
case DeckFileFormat::PlainText:
|
||||
result = loadedDeck.deckList.saveToFile_Plain(&file);
|
||||
break;
|
||||
case CockatriceFormat:
|
||||
deckList->setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
|
||||
result = deckList->saveToFile_Native(&file);
|
||||
case DeckFileFormat::Cockatrice:
|
||||
loadedDeck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
|
||||
result = loadedDeck.deckList.saveToFile_Native(&file);
|
||||
break;
|
||||
}
|
||||
|
||||
file.close(); // Close the file to ensure changes are flushed
|
||||
|
||||
if (result) {
|
||||
lastLoadInfo = {
|
||||
loadedDeck.lastLoadInfo = {
|
||||
.fileName = fileName,
|
||||
.fileFormat = fmt,
|
||||
};
|
||||
@@ -269,39 +267,35 @@ static QString toDecklistExportString(const DecklistCardNode *card)
|
||||
return cardString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all cards in the list to their decklist export string and joins them into one string
|
||||
*/
|
||||
static QString toDecklistExportString(const QList<const DecklistCardNode *> &cardNodes)
|
||||
{
|
||||
QString result;
|
||||
|
||||
for (auto cardNode : cardNodes) {
|
||||
result += toDecklistExportString(cardNode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export deck to decklist function, called to format the deck in a way to be sent to a server
|
||||
*
|
||||
* @param deckList The decklist to export
|
||||
* @param website The website we're sending the deck to
|
||||
*/
|
||||
QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website)
|
||||
QString DeckLoader::exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website)
|
||||
{
|
||||
// Add the base url
|
||||
QString deckString = "https://" + getDomainForWebsite(website) + "/?";
|
||||
// Create two strings to pass to function
|
||||
QString mainBoardCards, sideBoardCards;
|
||||
|
||||
// Set up the function to call
|
||||
auto formatDeckListForExport = [&mainBoardCards, &sideBoardCards](const auto *node, const auto *card) {
|
||||
// Get the card name
|
||||
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
if (!dbCard || dbCard->getIsToken()) {
|
||||
// If it's a token, we don't care about the card.
|
||||
return;
|
||||
}
|
||||
// export all cards in zone
|
||||
QString mainBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_MAIN}));
|
||||
QString sideBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_SIDE}));
|
||||
|
||||
// Check if it's a sideboard card.
|
||||
if (node->getName() == DECK_ZONE_SIDE) {
|
||||
sideBoardCards += toDecklistExportString(card);
|
||||
} else {
|
||||
// If it's a mainboard card, do the same thing, but for the mainboard card string
|
||||
mainBoardCards += toDecklistExportString(card);
|
||||
}
|
||||
};
|
||||
|
||||
// call our struct function for each card in the deck
|
||||
deckList->forEachCard(formatDeckListForExport);
|
||||
// Remove the extra return at the end of the last cards
|
||||
mainBoardCards.chop(3);
|
||||
sideBoardCards.chop(3);
|
||||
@@ -316,113 +310,7 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi
|
||||
return deckString;
|
||||
}
|
||||
|
||||
// This struct is here to support the forEachCard function call, defined in decklist.
|
||||
// It requires a function to be called for each card, and it will set the providerId to the preferred printing.
|
||||
struct SetProviderIdToPreferred
|
||||
{
|
||||
// Main operator for struct, allowing the foreachcard to work.
|
||||
SetProviderIdToPreferred()
|
||||
{
|
||||
}
|
||||
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
|
||||
QString providerId = preferredPrinting.getUuid();
|
||||
QString setShortName = preferredPrinting.getSet()->getShortName();
|
||||
QString collectorNumber = preferredPrinting.getProperty("num");
|
||||
|
||||
card->setCardProviderId(providerId);
|
||||
card->setCardCollectorNumber(collectorNumber);
|
||||
card->setCardSetShortName(setShortName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This function iterates through each card in the decklist and sets the providerId
|
||||
* on each card based on its set name and collector number.
|
||||
*
|
||||
* @param deckList The decklist to modify
|
||||
*/
|
||||
void DeckLoader::setProviderIdToPreferredPrinting(const DeckList *deckList)
|
||||
{
|
||||
// Set up the struct to call.
|
||||
SetProviderIdToPreferred setProviderIdToPreferred;
|
||||
|
||||
// Call the forEachCard method for each card in the deck
|
||||
deckList->forEachCard(setProviderIdToPreferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the providerId on each card in the decklist based on its set name and collector number.
|
||||
*
|
||||
* @param deckList The decklist to modify
|
||||
*/
|
||||
void DeckLoader::resolveSetNameAndNumberToProviderID(const DeckList *deckList)
|
||||
{
|
||||
auto setProviderId = [](const auto node, const auto card) {
|
||||
Q_UNUSED(node);
|
||||
// Retrieve the providerId based on setName and collectorNumber
|
||||
QString providerId =
|
||||
CardDatabaseManager::getInstance()
|
||||
->query()
|
||||
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
|
||||
.getUuid();
|
||||
|
||||
// Set the providerId on the card
|
||||
card->setCardProviderId(providerId);
|
||||
};
|
||||
|
||||
deckList->forEachCard(setProviderId);
|
||||
}
|
||||
|
||||
// This struct is here to support the forEachCard function call, defined in decklist.
|
||||
// It requires a function to be called for each card, and it will set the providerId.
|
||||
struct ClearSetNameNumberAndProviderId
|
||||
{
|
||||
// Main operator for struct, allowing the foreachcard to work.
|
||||
ClearSetNameNumberAndProviderId()
|
||||
{
|
||||
}
|
||||
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
// Set the providerId on the card
|
||||
card->setCardSetShortName(nullptr);
|
||||
card->setCardCollectorNumber(nullptr);
|
||||
card->setCardProviderId(nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the set name and numbers on each card in the decklist.
|
||||
*
|
||||
* @param deckList The decklist to modify
|
||||
*/
|
||||
void DeckLoader::clearSetNamesAndNumbers(const DeckList *deckList)
|
||||
{
|
||||
auto clearSetNameAndNumber = [](const auto node, auto card) {
|
||||
Q_UNUSED(node)
|
||||
// Set the providerId on the card
|
||||
card->setCardSetShortName(nullptr);
|
||||
card->setCardCollectorNumber(nullptr);
|
||||
card->setCardProviderId(nullptr);
|
||||
};
|
||||
|
||||
deckList->forEachCard(clearSetNameAndNumber);
|
||||
}
|
||||
|
||||
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
|
||||
{
|
||||
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
|
||||
return CockatriceFormat;
|
||||
}
|
||||
return PlainTextFormat;
|
||||
}
|
||||
|
||||
void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, bool addSetNameAndNumber)
|
||||
void DeckLoader::saveToClipboard(const DeckList &deckList, bool addComments, bool addSetNameAndNumber)
|
||||
{
|
||||
QString buffer;
|
||||
QTextStream stream(&buffer);
|
||||
@@ -432,7 +320,7 @@ void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, boo
|
||||
}
|
||||
|
||||
bool DeckLoader::saveToStream_Plain(QTextStream &out,
|
||||
const DeckList *deckList,
|
||||
const DeckList &deckList,
|
||||
bool addComments,
|
||||
bool addSetNameAndNumber)
|
||||
{
|
||||
@@ -441,9 +329,7 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
|
||||
}
|
||||
|
||||
// loop zones
|
||||
for (int i = 0; i < deckList->getRoot()->size(); i++) {
|
||||
const auto *zoneNode = dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i));
|
||||
|
||||
for (auto zoneNode : deckList.getZoneNodes()) {
|
||||
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
|
||||
|
||||
// end of zone
|
||||
@@ -453,14 +339,14 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList)
|
||||
void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList)
|
||||
{
|
||||
if (!deckList->getName().isEmpty()) {
|
||||
out << "// " << deckList->getName() << "\n\n";
|
||||
if (!deckList.getName().isEmpty()) {
|
||||
out << "// " << deckList.getName() << "\n\n";
|
||||
}
|
||||
|
||||
if (!deckList->getComments().isEmpty()) {
|
||||
QStringList commentRows = deckList->getComments().split(QRegularExpression("\n|\r\n|\r"));
|
||||
if (!deckList.getComments().isEmpty()) {
|
||||
QStringList commentRows = deckList.getComments().split(QRegularExpression("\n|\r\n|\r"));
|
||||
for (const QString &row : commentRows) {
|
||||
out << "// " << row << "\n";
|
||||
}
|
||||
@@ -548,7 +434,7 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
|
||||
}
|
||||
}
|
||||
|
||||
bool DeckLoader::convertToCockatriceFormat(QString fileName)
|
||||
bool DeckLoader::convertToCockatriceFormat(const QString &fileName)
|
||||
{
|
||||
// Change the file extension to .cod
|
||||
QFileInfo fileInfo(fileName);
|
||||
@@ -564,12 +450,12 @@ bool DeckLoader::convertToCockatriceFormat(QString fileName)
|
||||
bool result = false;
|
||||
|
||||
// Perform file modifications based on the detected format
|
||||
switch (getFormatFromName(fileName)) {
|
||||
case PlainTextFormat:
|
||||
switch (DeckFileFormat::getFormatFromName(fileName)) {
|
||||
case DeckFileFormat::PlainText:
|
||||
// Save in Cockatrice's native format
|
||||
result = deckList->saveToFile_Native(&file);
|
||||
result = loadedDeck.deckList.saveToFile_Native(&file);
|
||||
break;
|
||||
case CockatriceFormat:
|
||||
case DeckFileFormat::Cockatrice:
|
||||
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
|
||||
result = true;
|
||||
break;
|
||||
@@ -588,39 +474,16 @@ bool DeckLoader::convertToCockatriceFormat(QString fileName)
|
||||
} else {
|
||||
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
|
||||
}
|
||||
lastLoadInfo = {
|
||||
loadedDeck.lastLoadInfo = {
|
||||
.fileName = newFileName,
|
||||
.fileFormat = CockatriceFormat,
|
||||
.fileFormat = DeckFileFormat::Cockatrice,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString DeckLoader::getCardZoneFromName(const QString &cardName, QString currentZoneName)
|
||||
{
|
||||
CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
|
||||
if (card && card->getIsToken()) {
|
||||
return DECK_ZONE_TOKENS;
|
||||
}
|
||||
|
||||
return currentZoneName;
|
||||
}
|
||||
|
||||
QString DeckLoader::getCompleteCardName(const QString &cardName)
|
||||
{
|
||||
if (CardDatabaseManager::getInstance()) {
|
||||
ExactCard temp = CardDatabaseManager::query()->guessCard({cardName});
|
||||
if (temp) {
|
||||
return temp.getName();
|
||||
}
|
||||
}
|
||||
|
||||
return cardName;
|
||||
}
|
||||
|
||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node)
|
||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
||||
{
|
||||
const int totalColumns = 2;
|
||||
|
||||
@@ -680,7 +543,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node)
|
||||
cursor->movePosition(QTextCursor::End);
|
||||
}
|
||||
|
||||
void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList)
|
||||
void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList)
|
||||
{
|
||||
QTextDocument doc;
|
||||
|
||||
@@ -696,19 +559,18 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList)
|
||||
headerCharFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
||||
cursor.insertText(deckList->getName());
|
||||
cursor.insertText(deckList.getName());
|
||||
|
||||
headerCharFormat.setFontPointSize(12);
|
||||
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
||||
cursor.insertText(deckList->getComments());
|
||||
cursor.insertText(deckList.getComments());
|
||||
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
||||
|
||||
for (int i = 0; i < deckList->getRoot()->size(); i++) {
|
||||
for (auto zoneNode : deckList.getZoneNodes()) {
|
||||
cursor.insertHtml("<br><img src=theme:hr.jpg>");
|
||||
// cursor.insertHtml("<hr>");
|
||||
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
||||
|
||||
printDeckListNode(&cursor, dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i)));
|
||||
printDeckListNode(&cursor, zoneNode);
|
||||
}
|
||||
|
||||
doc.print(printer);
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
#ifndef DECK_LOADER_H
|
||||
#define DECK_LOADER_H
|
||||
|
||||
#include "loaded_deck.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QPrinter>
|
||||
#include <QTextCursor>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader")
|
||||
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader");
|
||||
|
||||
class DeckLoader : public QObject
|
||||
class DeckLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
signals:
|
||||
@@ -22,27 +24,6 @@ signals:
|
||||
void loadFinished(bool success);
|
||||
|
||||
public:
|
||||
enum FileFormat
|
||||
{
|
||||
PlainTextFormat,
|
||||
CockatriceFormat
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Information about where the deck was loaded from.
|
||||
*
|
||||
* For local decks, the remoteDeckId field will always be -1.
|
||||
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
|
||||
*/
|
||||
struct LoadInfo
|
||||
{
|
||||
static constexpr int NON_REMOTE_ID = -1;
|
||||
|
||||
QString fileName = "";
|
||||
FileFormat fileFormat = CockatriceFormat;
|
||||
int remoteDeckId = NON_REMOTE_ID;
|
||||
};
|
||||
|
||||
/**
|
||||
* Supported file extensions for decklist files
|
||||
*/
|
||||
@@ -60,47 +41,29 @@ public:
|
||||
};
|
||||
|
||||
private:
|
||||
DeckList *deckList;
|
||||
LoadInfo lastLoadInfo;
|
||||
LoadedDeck loadedDeck;
|
||||
|
||||
public:
|
||||
DeckLoader(QObject *parent);
|
||||
DeckLoader(QObject *parent, DeckList *_deckList);
|
||||
DeckLoader(const DeckLoader &) = delete;
|
||||
DeckLoader &operator=(const DeckLoader &) = delete;
|
||||
|
||||
const LoadInfo &getLastLoadInfo() const
|
||||
{
|
||||
return lastLoadInfo;
|
||||
}
|
||||
|
||||
void setLastLoadInfo(const LoadInfo &info)
|
||||
{
|
||||
lastLoadInfo = info;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasNotBeenLoaded() const
|
||||
{
|
||||
return lastLoadInfo.fileName.isEmpty() && lastLoadInfo.remoteDeckId == LoadInfo::NON_REMOTE_ID;
|
||||
return loadedDeck.lastLoadInfo.isEmpty();
|
||||
}
|
||||
|
||||
static void clearSetNamesAndNumbers(const DeckList *deckList);
|
||||
static FileFormat getFormatFromName(const QString &fileName);
|
||||
|
||||
bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false);
|
||||
bool loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest);
|
||||
bool loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest = false);
|
||||
bool loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest);
|
||||
bool loadFromRemote(const QString &nativeString, int remoteDeckId);
|
||||
bool saveToFile(const QString &fileName, FileFormat fmt);
|
||||
bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt);
|
||||
bool saveToFile(const QString &fileName, DeckFileFormat::Format fmt);
|
||||
bool updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt);
|
||||
|
||||
static QString exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website);
|
||||
static QString exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website);
|
||||
|
||||
static void setProviderIdToPreferredPrinting(const DeckList *deckList);
|
||||
static void resolveSetNameAndNumberToProviderID(const DeckList *deckList);
|
||||
|
||||
static void saveToClipboard(const DeckList *deckList, bool addComments = true, bool addSetNameAndNumber = true);
|
||||
static void saveToClipboard(const DeckList &deckList, bool addComments = true, bool addSetNameAndNumber = true);
|
||||
static bool saveToStream_Plain(QTextStream &out,
|
||||
const DeckList *deckList,
|
||||
const DeckList &deckList,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
|
||||
@@ -109,18 +72,26 @@ public:
|
||||
* @param printer The printer to render the decklist to.
|
||||
* @param deckList
|
||||
*/
|
||||
static void printDeckList(QPrinter *printer, const DeckList *deckList);
|
||||
static void printDeckList(QPrinter *printer, const DeckList &deckList);
|
||||
|
||||
bool convertToCockatriceFormat(QString fileName);
|
||||
bool convertToCockatriceFormat(const QString &fileName);
|
||||
|
||||
DeckList *getDeckList()
|
||||
LoadedDeck &getDeck()
|
||||
{
|
||||
return deckList;
|
||||
return loadedDeck;
|
||||
}
|
||||
const LoadedDeck &getDeck() const
|
||||
{
|
||||
return loadedDeck;
|
||||
}
|
||||
void setDeck(const LoadedDeck &deck)
|
||||
{
|
||||
loadedDeck = deck;
|
||||
}
|
||||
|
||||
private:
|
||||
static void printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node);
|
||||
static void saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList);
|
||||
static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node);
|
||||
static void saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList);
|
||||
|
||||
static void saveToStream_DeckZone(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
@@ -131,9 +102,6 @@ private:
|
||||
QList<DecklistCardNode *> cards,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
|
||||
[[nodiscard]] static QString getCardZoneFromName(const QString &cardName, QString currentZoneName);
|
||||
[[nodiscard]] static QString getCompleteCardName(const QString &cardName);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
11
cockatrice/src/interface/deck_loader/loaded_deck.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "loaded_deck.h"
|
||||
|
||||
bool LoadedDeck::LoadInfo::isEmpty() const
|
||||
{
|
||||
return fileName.isEmpty() && remoteDeckId == NON_REMOTE_ID;
|
||||
}
|
||||
|
||||
bool LoadedDeck::isEmpty() const
|
||||
{
|
||||
return deckList.isEmpty() && lastLoadInfo.isEmpty();
|
||||
}
|
||||
39
cockatrice/src/interface/deck_loader/loaded_deck.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef COCKATRICE_LOADED_DECK_H
|
||||
#define COCKATRICE_LOADED_DECK_H
|
||||
|
||||
#include "deck_file_format.h"
|
||||
#include "libcockatrice/deck_list/deck_list.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* @brief Represents a deck that was loaded from somewhere.
|
||||
* Contains the DeckList itself, as well as info about where it was loaded from.
|
||||
*/
|
||||
struct LoadedDeck
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Information about where the deck was loaded from.
|
||||
*
|
||||
* For local decks, the remoteDeckId field will always be -1.
|
||||
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
|
||||
*/
|
||||
struct LoadInfo
|
||||
{
|
||||
static constexpr int NON_REMOTE_ID = -1;
|
||||
|
||||
QString fileName = "";
|
||||
DeckFileFormat::Format fileFormat = DeckFileFormat::Cockatrice;
|
||||
int remoteDeckId = NON_REMOTE_ID;
|
||||
|
||||
bool isEmpty() const;
|
||||
};
|
||||
|
||||
DeckList deckList; ///< The decklist itself
|
||||
LoadInfo lastLoadInfo = {}; ///< info about where the deck was loaded from
|
||||
|
||||
bool isEmpty() const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_LOADED_DECK_H
|
||||
@@ -57,17 +57,10 @@ void Logger::openLogfileSession()
|
||||
return;
|
||||
}
|
||||
fileStream.setDevice(&fileHandle);
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
fileStream << "Log session started at " << QDateTime::currentDateTime().toString() << Qt::endl;
|
||||
fileStream << getClientVersion() << Qt::endl;
|
||||
fileStream << getSystemArchitecture() << Qt::endl;
|
||||
fileStream << getClientInstallInfo() << Qt::endl;
|
||||
#else
|
||||
fileStream << "Log session started at " << QDateTime::currentDateTime().toString() << endl;
|
||||
fileStream << getClientVersion() << endl;
|
||||
fileStream << getSystemArchitecture() << endl;
|
||||
fileStream << getClientInstallInfo() << endl;
|
||||
#endif
|
||||
logToFileEnabled = true;
|
||||
}
|
||||
|
||||
@@ -77,11 +70,7 @@ void Logger::closeLogfileSession()
|
||||
return;
|
||||
|
||||
logToFileEnabled = false;
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << Qt::endl;
|
||||
#else
|
||||
fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << endl;
|
||||
#endif
|
||||
fileHandle.close();
|
||||
}
|
||||
|
||||
@@ -103,11 +92,7 @@ void Logger::internalLog(const QString &message)
|
||||
std::cerr << message.toStdString() << std::endl; // Print to stdout
|
||||
|
||||
if (logToFileEnabled) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
fileStream << message << Qt::endl; // Print to fileStream
|
||||
#else
|
||||
fileStream << message << endl; // Print to fileStream
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,30 +8,10 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QResizeEvent>
|
||||
#include <QSize>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
|
||||
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
layout->setSpacing(5); // Small spacing between icons
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
|
||||
setLayout(layout);
|
||||
|
||||
// Define the full WUBRG set (White, Blue, Black, Red, Green)
|
||||
QString fullColorIdentity = "WUBRG";
|
||||
|
||||
if (card) {
|
||||
manaCost = card->getColors(); // Get mana cost string
|
||||
QStringList symbols = parseColorIdentity(manaCost); // Parse mana cost string
|
||||
|
||||
populateManaSymbolWidgets();
|
||||
}
|
||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageDrawUnusedColorIdentitiesChanged, this,
|
||||
&ColorIdentityWidget::toggleUnusedVisibility);
|
||||
}
|
||||
|
||||
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, QString _manaCost)
|
||||
: QWidget(parent), card(nullptr), manaCost(_manaCost)
|
||||
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity)
|
||||
: QWidget(parent), colorIdentity(_colorIdentity)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
layout->setSpacing(5); // Small spacing between icons
|
||||
@@ -45,12 +25,21 @@ ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, QString _manaCost)
|
||||
&ColorIdentityWidget::toggleUnusedVisibility);
|
||||
}
|
||||
|
||||
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const CardInfoPtr &card)
|
||||
: ColorIdentityWidget(parent, card->getColors())
|
||||
{
|
||||
}
|
||||
|
||||
void ColorIdentityWidget::populateManaSymbolWidgets()
|
||||
{
|
||||
// Define the full WUBRG set (White, Blue, Black, Red, Green)
|
||||
QString fullColorIdentity = "WUBRG";
|
||||
QStringList symbols = parseColorIdentity(manaCost); // Parse mana cost string
|
||||
QStringList symbols = parseColorIdentity(colorIdentity); // Parse mana cost string
|
||||
|
||||
// clear old layout
|
||||
QtUtils::clearLayoutRec(layout);
|
||||
|
||||
// populate mana symbols
|
||||
if (SettingsCache::instance().getVisualDeckStorageDrawUnusedColorIdentities()) {
|
||||
for (const QString symbol : fullColorIdentity) {
|
||||
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
|
||||
@@ -66,13 +55,6 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
|
||||
|
||||
void ColorIdentityWidget::toggleUnusedVisibility()
|
||||
{
|
||||
if (layout != nullptr) {
|
||||
QLayoutItem *item;
|
||||
while ((item = layout->takeAt(0)) != nullptr) {
|
||||
item->widget()->deleteLater(); // Delete the widget
|
||||
delete item; // Delete the layout item
|
||||
}
|
||||
}
|
||||
populateManaSymbolWidgets();
|
||||
}
|
||||
|
||||
@@ -97,12 +79,12 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
QStringList ColorIdentityWidget::parseColorIdentity(const QString &cmc)
|
||||
QStringList ColorIdentityWidget::parseColorIdentity(const QString &manaString)
|
||||
{
|
||||
QStringList symbols;
|
||||
|
||||
// Handle split costs (e.g., "3U // 4UU")
|
||||
QStringList splitCosts = cmc.split(" // ");
|
||||
QStringList splitCosts = manaString.split(" // ");
|
||||
for (const QString &part : splitCosts) {
|
||||
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
|
||||
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
|
||||
|
||||
@@ -15,19 +15,19 @@ class ColorIdentityWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ColorIdentityWidget(QWidget *parent, CardInfoPtr card);
|
||||
explicit ColorIdentityWidget(QWidget *parent, QString manaCost);
|
||||
explicit ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity = "");
|
||||
explicit ColorIdentityWidget(QWidget *parent, const CardInfoPtr &card);
|
||||
|
||||
void populateManaSymbolWidgets();
|
||||
|
||||
QStringList parseColorIdentity(const QString &manaString);
|
||||
static QStringList parseColorIdentity(const QString &manaString);
|
||||
|
||||
public slots:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void toggleUnusedVisibility();
|
||||
|
||||
private:
|
||||
CardInfoPtr card;
|
||||
QString manaCost;
|
||||
QString colorIdentity;
|
||||
QHBoxLayout *layout;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,34 @@ CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
|
||||
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
|
||||
}
|
||||
|
||||
// Just here so it can get overwritten in subclasses.
|
||||
void CardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// User Interaction
|
||||
// =====================================================================================================================
|
||||
|
||||
void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
QWidget::mousePressEvent(event);
|
||||
if (selectionModel) {
|
||||
selectionModel->clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
|
||||
{
|
||||
emit cardClicked(event, card);
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::onHover(const ExactCard &card)
|
||||
{
|
||||
emit cardHovered(card);
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
|
||||
{
|
||||
auto proxyModel = qobject_cast<QAbstractProxyModel *>(selectionModel->model());
|
||||
@@ -76,23 +104,18 @@ void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected,
|
||||
}
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::clearAllDisplayWidgets()
|
||||
{
|
||||
for (auto idx : indexToWidgetMap.keys()) {
|
||||
auto displayWidget = indexToWidgetMap.value(idx);
|
||||
removeFromLayout(displayWidget);
|
||||
indexToWidgetMap.remove(idx);
|
||||
delete displayWidget;
|
||||
}
|
||||
}
|
||||
// =====================================================================================================================
|
||||
// Display Widget Management
|
||||
// =====================================================================================================================
|
||||
|
||||
QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex index)
|
||||
{
|
||||
if (indexToWidgetMap.contains(index)) {
|
||||
return indexToWidgetMap[index];
|
||||
}
|
||||
auto cardName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString();
|
||||
auto cardProviderId = deckListModel->data(index.sibling(index.row(), 4), Qt::EditRole).toString();
|
||||
auto cardName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
auto cardProviderId =
|
||||
index.sibling(index.row(), DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::EditRole).toString();
|
||||
|
||||
auto widget = new CardInfoPictureWithTextOverlayWidget(getLayoutParent(), true);
|
||||
widget->setScaleFactor(cardSizeWidget->getSlider()->value());
|
||||
@@ -114,7 +137,7 @@ void CardGroupDisplayWidget::updateCardDisplays()
|
||||
|
||||
// This doesn't really matter since overwrite the whole lessThan function to just compare dynamically anyway.
|
||||
proxy.setSortRole(Qt::EditRole);
|
||||
proxy.sort(1, Qt::AscendingOrder);
|
||||
proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
|
||||
|
||||
// 1. trackedIndex is a source index → map it to proxy space
|
||||
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
|
||||
@@ -133,6 +156,20 @@ void CardGroupDisplayWidget::updateCardDisplays()
|
||||
}
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::clearAllDisplayWidgets()
|
||||
{
|
||||
for (auto idx : indexToWidgetMap.keys()) {
|
||||
auto displayWidget = indexToWidgetMap.value(idx);
|
||||
removeFromLayout(displayWidget);
|
||||
indexToWidgetMap.remove(idx);
|
||||
delete displayWidget;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// DeckListModel Signal Responses
|
||||
// =====================================================================================================================
|
||||
|
||||
void CardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first, int last)
|
||||
{
|
||||
if (!trackedIndex.isValid()) {
|
||||
@@ -177,27 +214,4 @@ void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSort
|
||||
|
||||
clearAllDisplayWidgets();
|
||||
updateCardDisplays();
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
QWidget::mousePressEvent(event);
|
||||
if (selectionModel) {
|
||||
selectionModel->clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
|
||||
{
|
||||
emit cardClicked(event, card);
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::onHover(const ExactCard &card)
|
||||
{
|
||||
emit cardHovered(card);
|
||||
}
|
||||
|
||||
void CardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa
|
||||
layout->addWidget(text, 0, Qt::AlignCenter);
|
||||
setLayout(layout);
|
||||
|
||||
setFrameStyle(QFrame::Panel | QFrame::Raised);
|
||||
setFrameStyle(static_cast<int>(QFrame::Panel) | QFrame::Raised);
|
||||
|
||||
int pixmapHeight = QGuiApplication::primaryScreen()->geometry().height() / 3;
|
||||
int pixmapWidth = static_cast<int>(pixmapHeight / aspectRatio);
|
||||
|
||||
@@ -39,10 +39,6 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
enlargedPixmapWidget = new CardInfoPictureEnlargedWidget(this->window());
|
||||
enlargedPixmapWidget->hide();
|
||||
connect(this, &QObject::destroyed, enlargedPixmapWidget, &CardInfoPictureEnlargedWidget::deleteLater);
|
||||
|
||||
hoverTimer = new QTimer(this);
|
||||
hoverTimer->setSingleShot(true);
|
||||
connect(hoverTimer, &QTimer::timeout, this, &CardInfoPictureWidget::showEnlargedPixmap);
|
||||
@@ -51,7 +47,7 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
|
||||
originalPos = this->pos();
|
||||
|
||||
// Create the animation
|
||||
animation = new QPropertyAnimation(this, "pos");
|
||||
animation = new QPropertyAnimation(this, "pos", this);
|
||||
animation->setDuration(200); // 200ms animation duration
|
||||
animation->setEasingCurve(QEasingCurve::OutQuad);
|
||||
|
||||
@@ -277,7 +273,7 @@ void CardInfoPictureWidget::leaveEvent(QEvent *event)
|
||||
|
||||
if (hoverToZoomEnabled) {
|
||||
hoverTimer->stop();
|
||||
enlargedPixmapWidget->hide();
|
||||
destroyEnlargedPixmapWidget();
|
||||
}
|
||||
|
||||
if (raiseOnEnter) {
|
||||
@@ -294,7 +290,7 @@ void CardInfoPictureWidget::moveEvent(QMoveEvent *event)
|
||||
QWidget::moveEvent(event);
|
||||
|
||||
hoverTimer->stop();
|
||||
enlargedPixmapWidget->hide();
|
||||
destroyEnlargedPixmapWidget();
|
||||
|
||||
if (animation->state() == QAbstractAnimation::Running) {
|
||||
return;
|
||||
@@ -310,7 +306,7 @@ void CardInfoPictureWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
QWidget::mouseMoveEvent(event);
|
||||
|
||||
if (hoverToZoomEnabled && enlargedPixmapWidget->isVisible()) {
|
||||
if (hoverToZoomEnabled && enlargedPixmapWidget && enlargedPixmapWidget->isVisible()) {
|
||||
const QPoint cursorPos = QCursor::pos();
|
||||
const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry();
|
||||
const QSize widgetSize = enlargedPixmapWidget->size();
|
||||
@@ -344,7 +340,7 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event)
|
||||
|
||||
void CardInfoPictureWidget::hideEvent(QHideEvent *event)
|
||||
{
|
||||
enlargedPixmapWidget->hide();
|
||||
destroyEnlargedPixmapWidget();
|
||||
QWidget::hideEvent(event);
|
||||
}
|
||||
|
||||
@@ -444,12 +440,19 @@ QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu()
|
||||
* If card information is available, the enlarged pixmap is loaded, positioned near the cursor,
|
||||
* and displayed.
|
||||
*/
|
||||
void CardInfoPictureWidget::showEnlargedPixmap() const
|
||||
void CardInfoPictureWidget::showEnlargedPixmap()
|
||||
{
|
||||
if (!exactCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy creation of the enlarged widget
|
||||
if (!enlargedPixmapWidget) {
|
||||
enlargedPixmapWidget = new CardInfoPictureEnlargedWidget(const_cast<CardInfoPictureWidget *>(this)->window());
|
||||
enlargedPixmapWidget->hide();
|
||||
connect(this, &QObject::destroyed, enlargedPixmapWidget, &CardInfoPictureEnlargedWidget::deleteLater);
|
||||
}
|
||||
|
||||
const QSize enlargedSize(static_cast<int>(size().width() * 2), static_cast<int>(size().width() * aspectRatio * 2));
|
||||
enlargedPixmapWidget->setCardPixmap(exactCard, enlargedSize);
|
||||
|
||||
@@ -460,7 +463,6 @@ void CardInfoPictureWidget::showEnlargedPixmap() const
|
||||
int newX = cursorPos.x() + enlargedPixmapOffset;
|
||||
int newY = cursorPos.y() + enlargedPixmapOffset;
|
||||
|
||||
// Adjust if out of bounds
|
||||
if (newX + widgetSize.width() > screenGeometry.right()) {
|
||||
newX = cursorPos.x() - widgetSize.width() - enlargedPixmapOffset;
|
||||
}
|
||||
@@ -472,3 +474,11 @@ void CardInfoPictureWidget::showEnlargedPixmap() const
|
||||
|
||||
enlargedPixmapWidget->show();
|
||||
}
|
||||
|
||||
void CardInfoPictureWidget::destroyEnlargedPixmapWidget()
|
||||
{
|
||||
if (enlargedPixmapWidget) {
|
||||
enlargedPixmapWidget->deleteLater();
|
||||
enlargedPixmapWidget = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ protected:
|
||||
{
|
||||
return resizedPixmap;
|
||||
}
|
||||
void showEnlargedPixmap() const;
|
||||
void showEnlargedPixmap();
|
||||
void destroyEnlargedPixmapWidget();
|
||||
|
||||
private:
|
||||
ExactCard exactCard;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "card_group_display_widgets/flat_card_group_display_widget.h"
|
||||
#include "card_group_display_widgets/overlapped_card_group_display_widget.h"
|
||||
#include "libcockatrice/card/database/card_database_manager.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
@@ -22,6 +23,7 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
||||
displayType(_displayType), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity),
|
||||
cardSizeWidget(_cardSizeWidget)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
@@ -45,6 +47,20 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
||||
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &DeckCardZoneDisplayWidget::onCategoryRemoval);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// User Interaction
|
||||
// =====================================================================================================================
|
||||
|
||||
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
|
||||
{
|
||||
emit cardClicked(event, card, zoneName);
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::onHover(const ExactCard &card)
|
||||
{
|
||||
emit cardHovered(card);
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
|
||||
{
|
||||
for (auto &range : selected) {
|
||||
@@ -68,24 +84,17 @@ void DeckCardZoneDisplayWidget::onSelectionChanged(const QItemSelection &selecte
|
||||
}
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget)
|
||||
{
|
||||
cardGroupLayout->removeWidget(displayWidget);
|
||||
displayWidget->setParent(nullptr);
|
||||
for (auto idx : indexToWidgetMap.keys()) {
|
||||
if (!idx.isValid()) {
|
||||
indexToWidgetMap.remove(idx);
|
||||
}
|
||||
}
|
||||
delete displayWidget;
|
||||
}
|
||||
// =====================================================================================================================
|
||||
// Display Widget Management
|
||||
// =====================================================================================================================
|
||||
|
||||
void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex index)
|
||||
{
|
||||
auto categoryName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString();
|
||||
if (indexToWidgetMap.contains(index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto categoryName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
if (displayType == DisplayType::Overlap) {
|
||||
auto *displayWidget = new OverlappedCardGroupDisplayWidget(
|
||||
cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria,
|
||||
@@ -120,7 +129,7 @@ void DeckCardZoneDisplayWidget::displayCards()
|
||||
QSortFilterProxyModel proxy;
|
||||
proxy.setSourceModel(deckListModel);
|
||||
proxy.setSortRole(Qt::EditRole);
|
||||
proxy.sort(1, Qt::AscendingOrder);
|
||||
proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
|
||||
|
||||
// 1. trackedIndex is a source index → map it to proxy space
|
||||
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
|
||||
@@ -139,6 +148,45 @@ void DeckCardZoneDisplayWidget::displayCards()
|
||||
}
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
|
||||
{
|
||||
displayType = _displayType;
|
||||
QLayoutItem *item;
|
||||
while ((item = cardGroupLayout->takeAt(0)) != nullptr) {
|
||||
if (item->widget()) {
|
||||
item->widget()->deleteLater();
|
||||
} else if (item->layout()) {
|
||||
item->layout()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
indexToWidgetMap.clear();
|
||||
|
||||
// We gotta wait for all the deleteLater's to finish so we fire after the next event cycle
|
||||
|
||||
auto timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
connect(timer, &QTimer::timeout, this, [this]() { displayCards(); });
|
||||
timer->start();
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget)
|
||||
{
|
||||
cardGroupLayout->removeWidget(displayWidget);
|
||||
displayWidget->setParent(nullptr);
|
||||
for (auto idx : indexToWidgetMap.keys()) {
|
||||
if (!idx.isValid()) {
|
||||
indexToWidgetMap.remove(idx);
|
||||
}
|
||||
}
|
||||
delete displayWidget;
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// DeckListModel Signal Responses
|
||||
// =====================================================================================================================
|
||||
|
||||
void DeckCardZoneDisplayWidget::onCategoryAddition(const QModelIndex &parent, int first, int last)
|
||||
{
|
||||
if (!trackedIndex.isValid()) {
|
||||
@@ -171,48 +219,6 @@ void DeckCardZoneDisplayWidget::onCategoryRemoval(const QModelIndex &parent, int
|
||||
}
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
for (QObject *child : layout->children()) {
|
||||
QWidget *widget = qobject_cast<QWidget *>(child);
|
||||
if (widget) {
|
||||
widget->setMaximumWidth(width());
|
||||
}
|
||||
}
|
||||
}
|
||||
void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card)
|
||||
{
|
||||
emit cardClicked(event, card, zoneName);
|
||||
}
|
||||
void DeckCardZoneDisplayWidget::onHover(const ExactCard &card)
|
||||
{
|
||||
emit cardHovered(card);
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
|
||||
{
|
||||
displayType = _displayType;
|
||||
QLayoutItem *item;
|
||||
while ((item = cardGroupLayout->takeAt(0)) != nullptr) {
|
||||
if (item->widget()) {
|
||||
item->widget()->deleteLater();
|
||||
} else if (item->layout()) {
|
||||
item->layout()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
indexToWidgetMap.clear();
|
||||
|
||||
// We gotta wait for all the deleteLater's to finish so we fire after the next event cycle
|
||||
|
||||
auto timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
connect(timer, &QTimer::timeout, this, [this]() { displayCards(); });
|
||||
timer->start();
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::onActiveGroupCriteriaChanged(QString _activeGroupCriteria)
|
||||
{
|
||||
activeGroupCriteria = _activeGroupCriteria;
|
||||
@@ -229,10 +235,13 @@ QList<QString> DeckCardZoneDisplayWidget::getGroupCriteriaValueList()
|
||||
{
|
||||
QList<QString> groupCriteriaValues;
|
||||
|
||||
QList<ExactCard> cardsInZone = deckListModel->getCardsForZone(zoneName);
|
||||
QList<const DecklistCardNode *> nodes = deckListModel->getCardNodesForZone(zoneName);
|
||||
|
||||
for (const ExactCard &cardInZone : cardsInZone) {
|
||||
groupCriteriaValues.append(cardInZone.getInfo().getProperty(activeGroupCriteria));
|
||||
for (auto node : nodes) {
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||
if (info) {
|
||||
groupCriteriaValues.append(info->getProperty(activeGroupCriteria));
|
||||
}
|
||||
}
|
||||
|
||||
groupCriteriaValues.removeDuplicates();
|
||||
|
||||
@@ -40,7 +40,6 @@ public:
|
||||
QPersistentModelIndex trackedIndex;
|
||||
QString zoneName;
|
||||
void addCardsToOverlapWidget();
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* @param outlineColor The color of the outline around the text.
|
||||
* @param fontSize The font size of the overlay text.
|
||||
* @param alignment The alignment of the text within the overlay.
|
||||
* @param _deckLoader The Deck Loader holding the Deck associated with this preview.
|
||||
*
|
||||
* Sets the widget's size policy and default border style.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "abstract_analytics_panel_widget.h"
|
||||
|
||||
#include "deck_list_statistics_analyzer.h"
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
|
||||
: QWidget(parent), analyzer(analyzer)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
|
||||
bannerAndSettingsContainer = new QWidget(this);
|
||||
|
||||
bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer);
|
||||
bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout);
|
||||
bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100);
|
||||
bannerWidget->setMaximumHeight(100);
|
||||
|
||||
bannerAndSettingsLayout->addWidget(bannerWidget, 1);
|
||||
|
||||
// config button
|
||||
configureButton = new QPushButton(this);
|
||||
configureButton->setIcon(QPixmap("theme:icons/cogwheel"));
|
||||
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
|
||||
bannerAndSettingsLayout->addWidget(configureButton, 0);
|
||||
|
||||
layout->addWidget(bannerAndSettingsContainer);
|
||||
|
||||
connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay);
|
||||
}
|
||||
|
||||
bool AbstractAnalyticsPanelWidget::applyConfigFromDialog()
|
||||
{
|
||||
QDialog *dlg = createConfigDialog(this);
|
||||
if (!dlg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = dlg->exec() == QDialog::Accepted;
|
||||
if (ok) {
|
||||
// dialog must expose its final config as JSON
|
||||
auto newCfg = extractConfigFromDialog(dlg);
|
||||
loadConfig(newCfg);
|
||||
updateDisplay();
|
||||
}
|
||||
dlg->deleteLater();
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
|
||||
#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
|
||||
|
||||
#include "../general/display/banner_widget.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QJsonObject>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class DeckListStatisticsAnalyzer;
|
||||
|
||||
class AbstractAnalyticsPanelWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public slots:
|
||||
virtual void updateDisplay() = 0;
|
||||
// Widgets must return a config dialog
|
||||
virtual QDialog *createConfigDialog(QWidget *parent) = 0;
|
||||
|
||||
public:
|
||||
explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
|
||||
|
||||
void setDisplayTitle(const QString &title)
|
||||
{
|
||||
displayTitle = title;
|
||||
if (bannerWidget) {
|
||||
bannerWidget->setText(displayTitle);
|
||||
}
|
||||
}
|
||||
|
||||
QString displayTitleText() const
|
||||
{
|
||||
return displayTitle;
|
||||
}
|
||||
|
||||
virtual QJsonObject saveConfig() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
virtual void loadConfig(const QJsonObject &)
|
||||
{
|
||||
}
|
||||
|
||||
// Unified helper to run config dialog and update widget
|
||||
bool applyConfigFromDialog();
|
||||
|
||||
// Dialog → JSON must be supplied by each subclass
|
||||
virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0;
|
||||
|
||||
protected:
|
||||
DeckListStatisticsAnalyzer *analyzer;
|
||||
QVBoxLayout *layout;
|
||||
QWidget *bannerAndSettingsContainer;
|
||||
QHBoxLayout *bannerAndSettingsLayout;
|
||||
QString displayTitle;
|
||||
BannerWidget *bannerWidget;
|
||||
QPushButton *configureButton;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "add_analytics_panel_dialog.h"
|
||||
|
||||
#include "analytics_panel_widget_factory.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Add Analytics Panel"));
|
||||
|
||||
layout = new QVBoxLayout(this);
|
||||
|
||||
typeCombo = new QComboBox(this);
|
||||
|
||||
// Populate using descriptors
|
||||
const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets();
|
||||
|
||||
for (const auto &desc : widgets) {
|
||||
// Show translated title to user
|
||||
typeCombo->addItem(desc.title, desc.type);
|
||||
}
|
||||
|
||||
layout->addWidget(typeCombo);
|
||||
|
||||
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
|
||||
#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
|
||||
|
||||
#include "analytics_panel_widget_factory.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class AddAnalyticsPanelDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AddAnalyticsPanelDialog(QWidget *parent);
|
||||
|
||||
QString selectedType() const
|
||||
{
|
||||
return typeCombo->currentData().toString();
|
||||
}
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
QComboBox *typeCombo;
|
||||
QDialogButtonBox *buttons;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "analytics_panel_widget_factory.h"
|
||||
|
||||
#include "abstract_analytics_panel_widget.h"
|
||||
|
||||
AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance()
|
||||
{
|
||||
static AnalyticsPanelWidgetFactory f;
|
||||
return f;
|
||||
}
|
||||
|
||||
void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc)
|
||||
{
|
||||
widgets.insert(desc.type, desc);
|
||||
}
|
||||
|
||||
AbstractAnalyticsPanelWidget *
|
||||
AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const
|
||||
{
|
||||
auto it = widgets.find(type);
|
||||
if (it == widgets.end())
|
||||
return nullptr;
|
||||
|
||||
auto w = it->creator(parent, analyzer);
|
||||
|
||||
w->setDisplayTitle(it->title);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
QList<AnalyticsPanelWidgetFactory::Descriptor> AnalyticsPanelWidgetFactory::availableWidgets() const
|
||||
{
|
||||
return widgets.values();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
|
||||
#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
|
||||
class AbstractAnalyticsPanelWidget;
|
||||
class DeckListStatisticsAnalyzer;
|
||||
|
||||
class AnalyticsPanelWidgetFactory
|
||||
{
|
||||
public:
|
||||
using Creator = std::function<AbstractAnalyticsPanelWidget *(QWidget *, DeckListStatisticsAnalyzer *)>;
|
||||
|
||||
struct Descriptor
|
||||
{
|
||||
QString type; // stable ID ("manaProdDevotion")
|
||||
QString title; // translated, user-facing
|
||||
Creator creator;
|
||||
};
|
||||
|
||||
static AnalyticsPanelWidgetFactory &instance();
|
||||
|
||||
// NEW: richer registration
|
||||
void registerWidget(const Descriptor &desc);
|
||||
|
||||
AbstractAnalyticsPanelWidget *
|
||||
create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const;
|
||||
|
||||
// NEW: expose widgets to UI
|
||||
QList<Descriptor> availableWidgets() const;
|
||||
|
||||
private:
|
||||
AnalyticsPanelWidgetFactory() = default; // Ensure private constructor
|
||||
AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete;
|
||||
AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete;
|
||||
|
||||
QMap<QString, Descriptor> widgets;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
#include "analytics_panel_widget_registrar.h"
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
|
||||
#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
|
||||
|
||||
#include "analytics_panel_widget_factory.h"
|
||||
|
||||
class AnalyticsPanelWidgetRegistrar
|
||||
{
|
||||
public:
|
||||
AnalyticsPanelWidgetRegistrar(const QString &type,
|
||||
const QString &title,
|
||||
AnalyticsPanelWidgetFactory::Creator creator)
|
||||
{
|
||||
AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator});
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "draw_probability_config.h"
|
||||
|
||||
QJsonObject DrawProbabilityConfig::toJson() const
|
||||
{
|
||||
QJsonObject o;
|
||||
o["criteria"] = criteria;
|
||||
o["atLeast"] = atLeast;
|
||||
o["quantity"] = quantity;
|
||||
o["drawn"] = drawn;
|
||||
return o;
|
||||
}
|
||||
DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o)
|
||||
{
|
||||
DrawProbabilityConfig cfg;
|
||||
if (o.contains("criteria")) {
|
||||
cfg.criteria = o["criteria"].toString();
|
||||
}
|
||||
if (o.contains("atLeast")) {
|
||||
cfg.atLeast = o["atLeast"].toBool(true);
|
||||
}
|
||||
if (o.contains("quantity")) {
|
||||
cfg.quantity = o["quantity"].toInt(1);
|
||||
}
|
||||
if (o.contains("drawn")) {
|
||||
cfg.drawn = o["drawn"].toInt(7);
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H
|
||||
#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
struct DrawProbabilityConfig
|
||||
{
|
||||
QString criteria = "name"; // name, type, subtype, cmc
|
||||
bool atLeast = true; // true = at least, false = exactly
|
||||
int quantity = 1; // N
|
||||
int drawn = 7; // M
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
static DrawProbabilityConfig fromJson(const QJsonObject &o);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "draw_probability_config_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QSpinBox>
|
||||
|
||||
DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
form = new QFormLayout(this);
|
||||
|
||||
// Criteria
|
||||
labelCriteria = new QLabel(this);
|
||||
criteria = new QComboBox(this);
|
||||
criteria->addItem(QString(), "name");
|
||||
criteria->addItem(QString(), "type");
|
||||
criteria->addItem(QString(), "subtype");
|
||||
criteria->addItem(QString(), "cmc");
|
||||
form->addRow(labelCriteria, criteria);
|
||||
|
||||
// Exactness
|
||||
labelExactness = new QLabel(this);
|
||||
exactness = new QComboBox(this);
|
||||
exactness->addItem(QString(), true);
|
||||
exactness->addItem(QString(), false);
|
||||
form->addRow(labelExactness, exactness);
|
||||
|
||||
// Quantity
|
||||
labelQuantity = new QLabel(this);
|
||||
quantity = new QSpinBox(this);
|
||||
quantity->setRange(1, 60);
|
||||
form->addRow(labelQuantity, quantity);
|
||||
|
||||
// Drawn
|
||||
labelDrawn = new QLabel(this);
|
||||
drawn = new QSpinBox(this);
|
||||
drawn->setRange(1, 60);
|
||||
drawn->setValue(7);
|
||||
form->addRow(labelDrawn, drawn);
|
||||
|
||||
// Button box
|
||||
auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
form->addWidget(bb);
|
||||
|
||||
connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept);
|
||||
connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void DrawProbabilityConfigDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Draw Probability Settings"));
|
||||
|
||||
labelCriteria->setText(tr("Criteria:"));
|
||||
criteria->setItemText(0, tr("Card Name"));
|
||||
criteria->setItemText(1, tr("Type"));
|
||||
criteria->setItemText(2, tr("Subtype"));
|
||||
criteria->setItemText(3, tr("Mana Value"));
|
||||
|
||||
labelExactness->setText(tr("Exactness:"));
|
||||
exactness->setItemText(0, tr("At least"));
|
||||
exactness->setItemText(1, tr("Exactly"));
|
||||
|
||||
labelQuantity->setText(tr("Quantity (N):"));
|
||||
labelDrawn->setText(tr("Cards drawn (M):"));
|
||||
|
||||
// i18n-friendly suffixes
|
||||
quantity->setSuffix(tr(" cards"));
|
||||
drawn->setSuffix(tr(" cards"));
|
||||
}
|
||||
|
||||
void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config)
|
||||
{
|
||||
cfg = _config;
|
||||
|
||||
criteria->setCurrentIndex(criteria->findData(_config.criteria));
|
||||
exactness->setCurrentIndex(exactness->findData(_config.atLeast));
|
||||
quantity->setValue(_config.quantity);
|
||||
drawn->setValue(_config.drawn);
|
||||
}
|
||||
|
||||
void DrawProbabilityConfigDialog::accept()
|
||||
{
|
||||
cfg.criteria = criteria->currentData().toString();
|
||||
cfg.atLeast = exactness->currentData().toBool();
|
||||
cfg.quantity = quantity->value();
|
||||
cfg.drawn = drawn->value();
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "draw_probability_config.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFormLayout>
|
||||
|
||||
class QComboBox;
|
||||
class QSpinBox;
|
||||
class QLabel;
|
||||
|
||||
class DrawProbabilityConfigDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
void setFromConfig(const DrawProbabilityConfig &_config);
|
||||
DrawProbabilityConfig result() const
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
protected:
|
||||
void accept() override;
|
||||
|
||||
private:
|
||||
DrawProbabilityConfig cfg;
|
||||
|
||||
QFormLayout *form;
|
||||
|
||||
// Widgets
|
||||
QComboBox *criteria;
|
||||
QComboBox *exactness;
|
||||
QSpinBox *quantity;
|
||||
QSpinBox *drawn;
|
||||
|
||||
QLabel *labelCriteria;
|
||||
QLabel *labelExactness;
|
||||
QLabel *labelQuantity;
|
||||
QLabel *labelDrawn;
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "draw_probability_widget.h"
|
||||
|
||||
#include "draw_probability_config_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QMap>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QWidget>
|
||||
#include <QtMath>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
||||
DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
|
||||
: AbstractAnalyticsPanelWidget(parent, analyzer)
|
||||
{
|
||||
controls = new QWidget(this);
|
||||
controlLayout = new QHBoxLayout(controls);
|
||||
|
||||
labelPrefix = new QLabel(this);
|
||||
controlLayout->addWidget(labelPrefix);
|
||||
|
||||
criteriaCombo = new QComboBox(this);
|
||||
// Give these things item-data so we can translate the actual user-facing strings
|
||||
criteriaCombo->addItem(QString(), "name");
|
||||
criteriaCombo->addItem(QString(), "type");
|
||||
criteriaCombo->addItem(QString(), "subtype");
|
||||
criteriaCombo->addItem(QString(), "cmc");
|
||||
controlLayout->addWidget(criteriaCombo);
|
||||
|
||||
exactnessCombo = new QComboBox(this);
|
||||
exactnessCombo->addItem(QString(), true); // At least
|
||||
exactnessCombo->addItem(QString(), false); // Exactly
|
||||
controlLayout->addWidget(exactnessCombo);
|
||||
|
||||
quantitySpin = new QSpinBox(this);
|
||||
quantitySpin->setRange(1, 60);
|
||||
controlLayout->addWidget(quantitySpin);
|
||||
|
||||
labelMiddle = new QLabel(this);
|
||||
controlLayout->addWidget(labelMiddle);
|
||||
|
||||
drawnSpin = new QSpinBox(this);
|
||||
drawnSpin->setRange(1, 60);
|
||||
drawnSpin->setValue(7);
|
||||
controlLayout->addWidget(drawnSpin);
|
||||
|
||||
labelSuffix = new QLabel(this);
|
||||
controlLayout->addWidget(labelSuffix);
|
||||
|
||||
labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
|
||||
controlLayout->addStretch(1);
|
||||
layout->addWidget(controls);
|
||||
|
||||
// Table
|
||||
resultTable = new QTableWidget(this);
|
||||
resultTable->setColumnCount(3);
|
||||
resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
layout->addWidget(resultTable);
|
||||
|
||||
// Connections
|
||||
connect(criteriaCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
|
||||
config.criteria = criteriaCombo->currentData().toString();
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
connect(exactnessCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
|
||||
config.atLeast = exactnessCombo->currentData().toBool();
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
connect(quantitySpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
config.quantity = v;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
connect(drawnSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
config.drawn = v;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
retranslateUi();
|
||||
applyConfigToToolbar();
|
||||
updateFilterOptions();
|
||||
}
|
||||
|
||||
void DrawProbabilityWidget::retranslateUi()
|
||||
{
|
||||
bannerWidget->setText(tr("Draw Probability"));
|
||||
|
||||
labelPrefix->setText(tr("Probability of drawing"));
|
||||
|
||||
criteriaCombo->setItemText(0, tr("Card Name"));
|
||||
criteriaCombo->setItemText(1, tr("Type"));
|
||||
criteriaCombo->setItemText(2, tr("Subtype"));
|
||||
criteriaCombo->setItemText(3, tr("Mana Value"));
|
||||
|
||||
exactnessCombo->setItemText(0, tr("At least"));
|
||||
exactnessCombo->setItemText(1, tr("Exactly"));
|
||||
|
||||
labelMiddle->setText(tr("card(s) having drawn at least"));
|
||||
labelSuffix->setText(tr("cards"));
|
||||
|
||||
resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")});
|
||||
}
|
||||
|
||||
QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent)
|
||||
{
|
||||
auto *dlg = new DrawProbabilityConfigDialog(parent);
|
||||
dlg->setFromConfig(config);
|
||||
return dlg;
|
||||
}
|
||||
|
||||
QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const
|
||||
{
|
||||
auto *dp = qobject_cast<DrawProbabilityConfigDialog *>(dlg);
|
||||
return dp ? dp->result().toJson() : QJsonObject{};
|
||||
}
|
||||
|
||||
void DrawProbabilityWidget::applyConfigToToolbar()
|
||||
{
|
||||
auto setComboByData = [](QComboBox *combo, const QVariant &value) {
|
||||
int idx = combo->findData(value);
|
||||
if (idx >= 0) {
|
||||
combo->setCurrentIndex(idx);
|
||||
}
|
||||
};
|
||||
|
||||
setComboByData(criteriaCombo, config.criteria);
|
||||
setComboByData(exactnessCombo, config.atLeast);
|
||||
|
||||
quantitySpin->setValue(config.quantity);
|
||||
drawnSpin->setValue(config.drawn);
|
||||
}
|
||||
|
||||
void DrawProbabilityWidget::updateDisplay()
|
||||
{
|
||||
updateFilterOptions();
|
||||
}
|
||||
|
||||
void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg)
|
||||
{
|
||||
config = DrawProbabilityConfig::fromJson(cfg);
|
||||
applyConfigToToolbar();
|
||||
updateFilterOptions();
|
||||
}
|
||||
|
||||
void DrawProbabilityWidget::updateFilterOptions()
|
||||
{
|
||||
if (!analyzer->getModel()->getDeckList()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString criteria = config.criteria;
|
||||
const bool atLeast = config.atLeast;
|
||||
const int quantity = config.quantity;
|
||||
const int drawn = config.drawn;
|
||||
|
||||
QMap<QString, int> categoryCounts;
|
||||
int totalDeckCards = 0;
|
||||
|
||||
const auto nodes = analyzer->getModel()->getCardNodes();
|
||||
for (auto *node : nodes) {
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
|
||||
totalDeckCards += node->getNumber();
|
||||
|
||||
QStringList categories;
|
||||
if (criteria == "name") {
|
||||
categories << info->getName();
|
||||
} else if (criteria == "type") {
|
||||
categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts);
|
||||
} else if (criteria == "subtype") {
|
||||
categories = info->getCardType().split(' ', Qt::SkipEmptyParts);
|
||||
} else if (criteria == "cmc") {
|
||||
categories << QString::number(info->getCmc().toInt());
|
||||
}
|
||||
|
||||
for (const QString &cat : categories) {
|
||||
categoryCounts[cat] += node->getNumber();
|
||||
}
|
||||
}
|
||||
|
||||
resultTable->setRowCount(categoryCounts.size());
|
||||
|
||||
int row = 0;
|
||||
for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) {
|
||||
const QString &cat = it.key();
|
||||
const int copies = it.value();
|
||||
|
||||
double probability = 0.0;
|
||||
if (atLeast) {
|
||||
for (int k = quantity; k <= drawn && k <= copies; ++k) {
|
||||
probability += hypergeometricProbability(totalDeckCards, copies, drawn, k);
|
||||
}
|
||||
} else {
|
||||
probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity);
|
||||
}
|
||||
|
||||
resultTable->setItem(row, 0, new QTableWidgetItem(cat));
|
||||
resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies)));
|
||||
resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2)));
|
||||
}
|
||||
}
|
||||
|
||||
double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k)
|
||||
{
|
||||
if (k < 0 || k > n || K > N || n > N) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double logP = 0.0;
|
||||
for (int i = 1; i <= k; ++i) {
|
||||
logP += qLn(double(K - k + i) / i);
|
||||
}
|
||||
for (int i = 1; i <= n - k; ++i) {
|
||||
logP += qLn(double(N - K - (n - k) + i) / i);
|
||||
}
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
logP -= qLn(double(N - n + i) / i);
|
||||
}
|
||||
|
||||
return qExp(logP);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H
|
||||
#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H
|
||||
|
||||
#include "../../abstract_analytics_panel_widget.h"
|
||||
#include "../../deck_list_statistics_analyzer.h"
|
||||
#include "draw_probability_config.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidget>
|
||||
|
||||
class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
|
||||
|
||||
QDialog *createConfigDialog(QWidget *parent) override;
|
||||
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
|
||||
void applyConfigToToolbar();
|
||||
|
||||
public slots:
|
||||
void updateDisplay() override;
|
||||
void loadConfig(const QJsonObject &cfg) override;
|
||||
void retranslateUi();
|
||||
|
||||
private slots:
|
||||
void updateFilterOptions();
|
||||
|
||||
private:
|
||||
DrawProbabilityConfig config;
|
||||
|
||||
QWidget *controls;
|
||||
QHBoxLayout *controlLayout;
|
||||
QLabel *labelPrefix;
|
||||
QLabel *labelMiddle;
|
||||
QLabel *labelSuffix;
|
||||
QLineEdit *cardNameEdit;
|
||||
QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value
|
||||
QComboBox *filterCombo; // The actual value
|
||||
QComboBox *exactnessCombo; // At least / Exactly
|
||||
QSpinBox *quantitySpin; // N
|
||||
QSpinBox *drawnSpin; // M
|
||||
|
||||
QSpinBox *manaValueSpin;
|
||||
|
||||
QTableWidget *resultTable;
|
||||
|
||||
double hypergeometricProbability(int N, int K, int n, int k);
|
||||
double calculateProbability(int totalCards, int copies, int drawn, bool atLeast);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "mana_base_config.h"
|
||||
|
||||
QJsonObject ManaBaseConfig::toJson() const
|
||||
{
|
||||
QJsonObject jsonObject;
|
||||
QJsonArray jsonArray;
|
||||
jsonObject["displayType"] = displayType;
|
||||
for (auto &filter : filters) {
|
||||
jsonArray.append(filter);
|
||||
}
|
||||
jsonObject["filters"] = jsonArray;
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o)
|
||||
|
||||
{
|
||||
ManaBaseConfig config;
|
||||
|
||||
if (o.contains("displayType")) {
|
||||
config.displayType = o["displayType"].toString();
|
||||
}
|
||||
|
||||
if (o.contains("filters")) {
|
||||
config.filters.clear();
|
||||
for (auto v : o["filters"].toArray()) {
|
||||
config.filters << v.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
#ifndef COCKATRICE_MANA_BASE_CONFIG_H
|
||||
#define COCKATRICE_MANA_BASE_CONFIG_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStringList>
|
||||
|
||||
struct ManaBaseConfig
|
||||
{
|
||||
QString displayType; // "pie" or "bar" or "combinedBar"
|
||||
QStringList filters; // which colors to show, empty = all
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
static ManaBaseConfig fromJson(const QJsonObject &o);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_MANA_BASE_CONFIG_H
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "mana_base_config_dialog.h"
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
|
||||
ManaBaseConfig initial,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), config(initial)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
|
||||
displayTypeLabel = new QLabel(this);
|
||||
layout->addWidget(displayTypeLabel);
|
||||
|
||||
displayType = new QComboBox(this);
|
||||
layout->addWidget(displayType);
|
||||
|
||||
filterLabel = new QLabel(this);
|
||||
layout->addWidget(filterLabel);
|
||||
|
||||
filterList = new QListWidget(this);
|
||||
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
|
||||
layout->addWidget(filterList);
|
||||
|
||||
QStringList colors = analyzer->getManaBase().keys();
|
||||
colors.sort();
|
||||
filterList->addItems(colors);
|
||||
|
||||
// select initial filters
|
||||
for (int i = 0; i < filterList->count(); ++i) {
|
||||
if (config.filters.contains(filterList->item(i)->text()))
|
||||
filterList->item(i)->setSelected(true);
|
||||
}
|
||||
|
||||
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void ManaBaseConfigDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Mana Base Configuration"));
|
||||
|
||||
displayTypeLabel->setText(tr("Display type:"));
|
||||
|
||||
displayType->clear();
|
||||
displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")});
|
||||
|
||||
filterLabel->setText(tr("Filter Colors (optional):"));
|
||||
|
||||
buttons->button(QDialogButtonBox::Ok)->setText(tr("OK"));
|
||||
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
|
||||
}
|
||||
|
||||
void ManaBaseConfigDialog::accept()
|
||||
{
|
||||
config.displayType = displayType->currentText();
|
||||
config.filters.clear();
|
||||
for (auto *item : filterList->selectedItems()) {
|
||||
config.filters << item->text();
|
||||
}
|
||||
QDialog::accept();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H
|
||||
#define COCKATRICE_MANA_BASE_ADD_DIALOG_H
|
||||
|
||||
#include "../../deck_list_statistics_analyzer.h"
|
||||
#include "mana_base_config.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class ManaBaseConfigDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
|
||||
void accept() override;
|
||||
|
||||
ManaBaseConfig result() const
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
private:
|
||||
ManaBaseConfig config;
|
||||
QVBoxLayout *layout;
|
||||
QLabel *displayTypeLabel;
|
||||
QComboBox *displayType;
|
||||
QLabel *filterLabel;
|
||||
QListWidget *filterList;
|
||||
QDialogButtonBox *buttons;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "mana_base_widget.h"
|
||||
|
||||
#include "../../../general/display/charts/bars/bar_widget.h"
|
||||
#include "../../../general/display/charts/bars/color_bar.h"
|
||||
#include "../../../general/display/charts/pies/color_pie.h"
|
||||
#include "../../analytics_panel_widget_registrar.h"
|
||||
#include "mana_base_config_dialog.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QListWidget>
|
||||
#include <libcockatrice/utility/color.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
AnalyticsPanelWidgetRegistrar registerManaBase{
|
||||
"manaBase", ManaBaseWidget::tr("Mana Base"),
|
||||
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg)
|
||||
: AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
|
||||
{
|
||||
barContainer = new QWidget(this);
|
||||
barLayout = new QHBoxLayout(barContainer);
|
||||
layout->addWidget(barContainer);
|
||||
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
void ManaBaseWidget::updateDisplay()
|
||||
{
|
||||
// Clear previous widgets
|
||||
while (QLayoutItem *item = barLayout->takeAt(0)) {
|
||||
if (item->widget()) {
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
auto &pipCount = analyzer->getProductionPipCount();
|
||||
auto &cardCount = analyzer->getProductionCardCount();
|
||||
|
||||
QHash<QString, int> manaMap;
|
||||
for (auto key : pipCount.keys()) {
|
||||
manaMap[key] = pipCount[key];
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (!config.filters.isEmpty()) {
|
||||
QHash<QString, int> filtered;
|
||||
for (auto f : config.filters) {
|
||||
if (manaMap.contains(f)) {
|
||||
filtered[f] = manaMap[f];
|
||||
}
|
||||
}
|
||||
manaMap = filtered;
|
||||
}
|
||||
|
||||
// Determine maximum for bar charts
|
||||
int highest = 1;
|
||||
for (auto val : manaMap) {
|
||||
highest = std::max(highest, val);
|
||||
}
|
||||
|
||||
// Convert to QMap for ColorBar / ColorPie (sorted)
|
||||
QMap<QString, int> mapSorted;
|
||||
for (auto it = manaMap.begin(); it != manaMap.end(); ++it) {
|
||||
mapSorted.insert(it.key(), it.value());
|
||||
}
|
||||
|
||||
// Choose display mode
|
||||
if (config.displayType == "bar") {
|
||||
const QList<QPair<QString, int>> sortedColors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(mapSorted);
|
||||
static const QHash<QString, QColor> colorMap = {
|
||||
{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)}, {"B", QColor(21, 11, 0)},
|
||||
{"R", QColor(211, 32, 42)}, {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)},
|
||||
};
|
||||
|
||||
for (const auto &[color, count] : sortedColors) {
|
||||
QString label = QString("%1 %2 (%3)").arg(color).arg(count).arg(cardCount.value(color));
|
||||
|
||||
BarWidget *bar = new BarWidget(label, count, highest, colorMap.value(color, Qt::gray), this);
|
||||
|
||||
barLayout->addWidget(bar);
|
||||
}
|
||||
} else if (config.displayType == "combinedBar") {
|
||||
ColorBar *cb = new ColorBar(mapSorted, this);
|
||||
cb->setMinimumHeight(30);
|
||||
barLayout->addWidget(cb);
|
||||
} else if (config.displayType == "pie") {
|
||||
ColorPie *pie = new ColorPie(mapSorted, this);
|
||||
pie->setMinimumSize(200, 200);
|
||||
barLayout->addWidget(pie);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
QSize ManaBaseWidget::sizeHint() const
|
||||
{
|
||||
return QSize(800, 150);
|
||||
}
|
||||
|
||||
QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent)
|
||||
{
|
||||
ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent);
|
||||
return dlg;
|
||||
}
|
||||
|
||||
QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const
|
||||
{
|
||||
auto *mc = qobject_cast<ManaBaseConfigDialog *>(dlg);
|
||||
if (!mc) {
|
||||
return {};
|
||||
}
|
||||
return mc->result().toJson();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @file mana_base_widget.h
|
||||
* @ingroup DeckEditorAnalyticsWidgets
|
||||
* @brief TODO: Document this.
|
||||
*/
|
||||
|
||||
#ifndef MANA_BASE_WIDGET_H
|
||||
#define MANA_BASE_WIDGET_H
|
||||
|
||||
#include "../../../general/display/banner_widget.h"
|
||||
#include "../../abstract_analytics_panel_widget.h"
|
||||
#include "../../deck_list_statistics_analyzer.h"
|
||||
#include "mana_base_config.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
#include <utility>
|
||||
|
||||
class ManaBaseWidget : public AbstractAnalyticsPanelWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
QSize sizeHint() const override;
|
||||
void updateDisplay() override;
|
||||
QDialog *createConfigDialog(QWidget *parent) override;
|
||||
|
||||
public:
|
||||
ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {});
|
||||
|
||||
QJsonObject saveConfig() const override
|
||||
{
|
||||
return config.toJson();
|
||||
}
|
||||
void loadConfig(const QJsonObject &o) override
|
||||
{
|
||||
config = ManaBaseConfig::fromJson(o);
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
|
||||
|
||||
private:
|
||||
ManaBaseConfig config;
|
||||
QWidget *barContainer;
|
||||
QHBoxLayout *barLayout;
|
||||
};
|
||||
|
||||
#endif // MANA_BASE_WIDGET_H
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "mana_curve_category_widget.h"
|
||||
|
||||
#include "libcockatrice/utility/color.h"
|
||||
#include "libcockatrice/utility/qt_utils.h"
|
||||
#include "mana_curve_config.h"
|
||||
#include "mana_curve_total_widget.h"
|
||||
|
||||
constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row
|
||||
|
||||
ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(4);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
// Same as minimum for now
|
||||
QSize ManaCurveCategoryWidget::sizeHint() const
|
||||
{
|
||||
if (layout->isEmpty()) {
|
||||
return QSize(0, 0);
|
||||
}
|
||||
|
||||
// Calculate exact height needed for all rows
|
||||
int rowCount = layout->count();
|
||||
|
||||
int totalHeight = rowCount * MIN_ROW_HEIGHT;
|
||||
totalHeight += (rowCount - 1) * layout->spacing();
|
||||
|
||||
return QSize(0, totalHeight);
|
||||
}
|
||||
|
||||
QSize ManaCurveCategoryWidget::minimumSizeHint() const
|
||||
{
|
||||
if (layout->isEmpty()) {
|
||||
return QSize(0, 0);
|
||||
}
|
||||
|
||||
// Calculate actual minimum based on number of rows
|
||||
int rowCount = layout->count();
|
||||
|
||||
int totalHeight = rowCount * MIN_ROW_HEIGHT;
|
||||
totalHeight += (rowCount - 1) * layout->spacing();
|
||||
|
||||
return QSize(0, totalHeight);
|
||||
}
|
||||
|
||||
void ManaCurveCategoryWidget::updateDisplay(int minCmc,
|
||||
int maxCmc,
|
||||
int highest,
|
||||
QHash<QString, QHash<int, int>> qCategoryCounts,
|
||||
QHash<QString, QHash<int, QStringList>> qCategoryCards,
|
||||
const ManaCurveConfig &config)
|
||||
{
|
||||
// Clear previous content
|
||||
QtUtils::clearLayoutRec(layout);
|
||||
|
||||
if (!config.showCategoryRows) {
|
||||
return; // nothing to show
|
||||
}
|
||||
|
||||
// Collect categories
|
||||
QStringList categories = qCategoryCounts.keys();
|
||||
|
||||
// Apply filters
|
||||
if (!config.filters.isEmpty()) {
|
||||
QStringList filtered;
|
||||
for (const QString &cat : categories) {
|
||||
if (config.filters.contains(cat)) {
|
||||
filtered.append(cat);
|
||||
}
|
||||
}
|
||||
categories = filtered;
|
||||
}
|
||||
|
||||
std::sort(categories.begin(), categories.end());
|
||||
|
||||
for (const QString &cat : categories) {
|
||||
QWidget *row = new QWidget(this);
|
||||
row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
row->setFixedHeight(MIN_ROW_HEIGHT);
|
||||
|
||||
QHBoxLayout *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rowLayout->setSpacing(4);
|
||||
|
||||
QLabel *categoryLabel = new QLabel(cat, row);
|
||||
categoryLabel->setFixedWidth(80);
|
||||
rowLayout->addWidget(categoryLabel);
|
||||
|
||||
QVector<BarData> catBars;
|
||||
const auto cmcCounts = qCategoryCounts.value(cat);
|
||||
const auto cmcCards = qCategoryCards.value(cat);
|
||||
|
||||
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
|
||||
int val = cmcCounts.value(cmc, 0);
|
||||
QStringList cards = cmcCards.value(cmc);
|
||||
|
||||
QVector<BarSegment> segments;
|
||||
if (val > 0) {
|
||||
segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)});
|
||||
}
|
||||
|
||||
catBars.push_back({QString::number(cmc), segments});
|
||||
}
|
||||
|
||||
auto *catChart = new BarChartWidget(row);
|
||||
catChart->setHighest(highest);
|
||||
catChart->setBars(catBars);
|
||||
catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
rowLayout->addWidget(catChart);
|
||||
layout->addWidget(row);
|
||||
}
|
||||
|
||||
// Update geometry after adding all widgets
|
||||
updateGeometry();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
|
||||
#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
|
||||
|
||||
#include "../../../general/display/charts/bars/bar_chart_widget.h"
|
||||
#include "mana_curve_config.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QWidget>
|
||||
|
||||
class ManaCurveCategoryWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ManaCurveCategoryWidget(QWidget *parent);
|
||||
void updateDisplay(int minCmc,
|
||||
int maxCmc,
|
||||
int highest,
|
||||
QHash<QString, QHash<int, int>> qCategoryCounts,
|
||||
QHash<QString, QHash<int, QStringList>> qCategoryCards,
|
||||
const ManaCurveConfig &config);
|
||||
|
||||
public slots:
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
|
||||