Compare commits

..

5 Commits

Author SHA1 Message Date
Brübach, Lukas
2c0c8b416a [Oracle] Qt version guard for regexp 2025-12-03 10:57:56 +01:00
Brübach, Lukas
65a3423009 [Oracle] Lint. 2025-12-03 10:53:08 +01:00
Brübach, Lukas
4ddbc8d018 [Oracle] Include QRegularExpression. 2025-12-03 10:51:11 +01:00
Brübach, Lukas
d4bf40694a [Oracle] Correct size and only do this if the user has not set a non-default url. 2025-12-03 10:44:40 +01:00
Brübach, Lukas
ec98bcf95d [Oracle] Add low-memory check (<=4GiB) and use mtgxml url instead. 2025-12-03 10:43:40 +01:00
420 changed files with 47033 additions and 71120 deletions

View File

@@ -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")
flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0" "-DWITH_DBCONVERTER=0")
fi
if [[ $MAKE_TEST ]]; then
flags+=("-DTEST=1")
@@ -156,18 +156,6 @@ 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
@@ -258,7 +246,7 @@ fi
if [[ $RUNNER_OS == macOS ]]; then
echo "::group::Inspect Mach-O binaries"
for app in cockatrice oracle servatrice; do
for app in cockatrice oracle servatrice dbconverter; do
binary="$GITHUB_WORKSPACE/build/$app/$app.app/Contents/MacOS/$app"
echo "Inspecting $app..."
vtool -show-build "$binary"

View File

@@ -1,40 +0,0 @@
#!/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"

View File

@@ -1,25 +0,0 @@
#!/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::"

View File

@@ -142,6 +142,7 @@ jobs:
- distro: Ubuntu
version: 22.04
package: DEB
test: skip # Running tests on all distros is superfluous
- distro: Ubuntu
version: 24.04
@@ -165,7 +166,7 @@ jobs:
- name: Restore compiler cache (ccache)
id: ccache_restore
uses: actions/cache/restore@v5
uses: actions/cache/restore@v4
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
@@ -204,7 +205,7 @@ jobs:
- name: Save compiler cache (ccache)
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v5
uses: actions/cache/save@v4
with:
path: ${{env.CACHE}}
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
@@ -212,7 +213,7 @@ jobs:
- name: Upload artifact
id: upload_artifact
if: matrix.package != 'skip'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: ${{matrix.distro}}${{matrix.version}}-package
path: ${{steps.build.outputs.path}}
@@ -262,6 +263,7 @@ 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
@@ -277,6 +279,7 @@ jobs:
qt_version: 6.6.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja
use_ccache: 1
@@ -292,6 +295,7 @@ jobs:
qt_version: 6.6.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja
use_ccache: 1
@@ -304,6 +308,7 @@ jobs:
qt_version: 6.6.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja
use_ccache: 1
@@ -316,6 +321,7 @@ 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
@@ -329,17 +335,13 @@ 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
@@ -354,72 +356,24 @@ 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 && matrix.os == 'macOS'
run: brew install ccache
- 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 }}
uses: jianmingyong/ccache-action@v1
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}}-
install-type: "binary"
ccache-key-prefix: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}
max-size: 500M
gh-token: ${{ secrets.GITHUB_TOKEN }}
- 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'
- name: Install Qt ${{matrix.qt_version}}
uses: jurplel/install-qt-action@v4
with:
version: ${{matrix.qt_version}}
arch: ${{matrix.qt_arch}}
modules: ${{matrix.qt_modules}}
cache: true
cache: ${{matrix.cache_qt}}
- name: Setup vcpkg cache
id: vcpkg-cache
@@ -449,15 +403,6 @@ 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:
@@ -505,7 +450,7 @@ jobs:
- name: Upload artifact
id: upload_artifact
if: matrix.make_package
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: ${{matrix.artifact_name}}
path: ${{steps.build.outputs.path}}
@@ -513,7 +458,7 @@ jobs:
- name: Upload pdb database
if: matrix.os == 'Windows'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v5
with:
name: Windows${{matrix.target}}-debug-pdbs
path: |

View File

@@ -33,7 +33,7 @@ jobs:
- name: Create pull request
if: github.event_name != 'pull_request'
id: create_pr
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@v7
with:
add-paths: |
cockatrice/translations/*.ts

View File

@@ -57,7 +57,7 @@ jobs:
- name: Create pull request
if: github.event_name != 'pull_request'
id: create_pr
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@v7
with:
add-paths: |
cockatrice/cockatrice_en@source.ts

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "vcpkg"]
path = vcpkg
url = https://github.com/microsoft/vcpkg.git
[submodule "doxygen-awesome-css"]
path = doc/doxygen/theme
url = https://github.com/jothepro/doxygen-awesome-css.git

View File

@@ -20,6 +20,8 @@ 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
@@ -354,6 +356,11 @@ 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)

View File

@@ -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 && \
cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=0 && \
make -j$(nproc) && \
make install

View File

@@ -54,7 +54,7 @@ PROJECT_NUMBER = $(COCKATRICE_REF)
# for a project that appears at the top of each page and should give viewers a
# quick idea about the purpose of the project. Keep the description short.
PROJECT_BRIEF = "A virtual tabletop for multiplayer card games"
PROJECT_BRIEF = "A cross-platform virtual tabletop for multiplayer card games"
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
# in the documentation. The maximum height of the logo should not exceed 55
@@ -1068,8 +1068,7 @@ RECURSIVE = YES
EXCLUDE = build/ \
cmake/ \
doc/doxygen/theme/docs/ \
doc/doxygen/theme/include/ \
dbconverter/ \
vcpkg/ \
webclient/
@@ -1432,9 +1431,7 @@ HTML_STYLESHEET =
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET = doc/doxygen/theme/doxygen-awesome.css \
doc/doxygen/css/hide_nav_sync.css \
doc/doxygen/css/cockatrice_docs_style.css
HTML_EXTRA_STYLESHEET = doc/doxygen/css/doxygen_style.css
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
@@ -1457,7 +1454,7 @@ HTML_EXTRA_FILES = doc/doxygen/js/graph_toggle.js
# The default value is: AUTO_LIGHT.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE = LIGHT
HTML_COLORSTYLE = AUTO_DARK
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to
@@ -1768,7 +1765,7 @@ ECLIPSE_DOC_ID = org.doxygen.Project
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
DISABLE_INDEX = NO
DISABLE_INDEX = YES
# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
# structure should be generated to display hierarchical information. If the tag

View File

@@ -44,10 +44,9 @@ Latest <kbd>beta</kbd> version:
# Related Repositories
- [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)
- [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
# Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA)
@@ -58,28 +57,11 @@ 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
![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats")
<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
@@ -93,19 +75,18 @@ 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 [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](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.
### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/)
<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 [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](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>
@@ -143,8 +124,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>

View File

@@ -42,6 +42,7 @@ 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

View File

@@ -1,11 +1,12 @@
# Find a compatible Qt version
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, FORCE_USE_QT5
# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, WITH_DBCONVERTER, 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)
@@ -28,12 +29,15 @@ 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}
${_TEST_NEEDED}
${_DBCONVERTER_NEEDED} ${_TEST_NEEDED}
)
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
@@ -59,7 +63,7 @@ if(Qt6_FOUND)
endif()
else()
find_package(
Qt5 5.15.2
Qt5 5.8.0
COMPONENTS ${REQUIRED_QT_COMPONENTS}
QUIET HINTS ${Qt5_DIR}
)
@@ -108,6 +112,7 @@ 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)

View File

@@ -213,6 +213,7 @@ ${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"

View File

@@ -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")
set(APPLICATIONS "cockatrice" "servatrice" "oracle" "dbconverter")
foreach(app_name IN LISTS APPLICATIONS)
set(FULL_APP_PATH "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${app_name}.app")

View File

@@ -19,10 +19,7 @@ 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
@@ -47,7 +44,6 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
src/interface/widgets/dialogs/dlg_update.cpp
src/interface/widgets/dialogs/dlg_view_log.cpp
src/interface/widgets/dialogs/override_printing_warning.cpp
src/interface/widgets/dialogs/tip_of_the_day.cpp
src/filters/deck_filter_string.cpp
src/filters/filter_builder.cpp
@@ -145,55 +141,28 @@ 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/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_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_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/deck_editor/printing_disabled_info_widget.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
@@ -205,7 +174,6 @@ 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
@@ -229,12 +197,8 @@ set(cockatrice_SOURCES
src/interface/widgets/utility/custom_line_edit.cpp
src/interface/widgets/utility/get_text_with_max.cpp
src/interface/widgets/utility/sequence_edit.cpp
src/interface/widgets/utility/visibility_change_listener.cpp
src/interface/widgets/utility/visibility_change_listener.h
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
@@ -242,7 +206,6 @@ 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
@@ -263,7 +226,6 @@ 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
@@ -320,10 +282,6 @@ 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)

View File

@@ -15,23 +15,18 @@
<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>
@@ -39,13 +34,10 @@
<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>
@@ -60,8 +52,6 @@
<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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,29 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,10 +1,6 @@
[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

View File

@@ -1,13 +1,11 @@
@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>
@@ -17,23 +15,15 @@ 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 <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><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>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>
@@ -49,4 +39,4 @@ searches are case insensitive.
<dt>Grouping:</dt>
<dd><a href="#red -([[]]:100 or aggro)">red -([[]]:100 or aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
</dl>
</dl>

View File

@@ -1,12 +1,10 @@
@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>
@@ -67,4 +65,4 @@ In this list of examples below, each entry has an explanation and can be clicked
<dd>[o:/counter target .* spell/](#o:/counter target .* spell/) <small>(Any card text with "counter target *something* spell")</small></dd>
<dd>[o:/for each .* and\/or .*/](#o:/for each .* and\/or .*/) <small>(/'s can be escaped with a \)</small></dd>
</dl>
</dl>

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 410 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 693 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 899 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 704 B

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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="left" />
id="right" />
<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="right"
id="left"
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

View File

@@ -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="left" />
id="right" />
<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="right"
id="left"
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

View File

@@ -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="left" />
id="right" />
<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="right"
id="left"
inkscape:connector-curvature="0" />
<path
sodipodi:type="star"

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -8,7 +8,6 @@
#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)
@@ -43,32 +42,31 @@ void DeckStatsInterface::queryFinished(QNetworkReply *reply)
deleteLater();
}
void DeckStatsInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
void DeckStatsInterface::getAnalyzeRequestData(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(const DeckList &deck)
void DeckStatsInterface::analyzeDeck(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(const DeckList &source, DeckList &destination)
void DeckStatsInterface::copyDeckWithoutTokens(DeckList &source, DeckList &destination)
{
auto copyIfNotAToken = [this, &destination](const auto node, const auto card) {
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName());

View File

@@ -28,15 +28,15 @@ private:
* closest non-token card instead. So we construct a new deck which has no
* tokens.
*/
void copyDeckWithoutTokens(const DeckList &source, DeckList &destination);
void copyDeckWithoutTokens(DeckList &source, DeckList &destination);
private slots:
void queryFinished(QNetworkReply *reply);
void getAnalyzeRequestData(const DeckList &deck, QByteArray &data);
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
public:
explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
void analyzeDeck(const DeckList &deck);
void analyzeDeck(DeckList *deck);
};
#endif

View File

@@ -8,7 +8,6 @@
#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)
@@ -67,33 +66,32 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
deleteLater();
}
void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
void TappedOutInterface::getAnalyzeRequestData(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(const DeckList &deck)
void TappedOutInterface::analyzeDeck(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(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard)
{
auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName());

View File

@@ -30,14 +30,14 @@ private:
QNetworkAccessManager *manager;
CardDatabase &cardDatabase;
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard);
private slots:
void queryFinished(QNetworkReply *reply);
void getAnalyzeRequestData(const DeckList &deck, QByteArray &data);
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
public:
explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
void analyzeDeck(const DeckList &deck);
void analyzeDeck(DeckList *deck);
};
#endif

View File

@@ -6,8 +6,6 @@
#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>
@@ -18,21 +16,21 @@ class IJsonDeckParser
public:
virtual ~IJsonDeckParser() = default;
virtual DeckList parse(const QJsonObject &obj) = 0;
virtual DeckLoader *parse(const QJsonObject &obj) = 0;
};
class ArchidektJsonParser : public IJsonDeckParser
{
public:
DeckList parse(const QJsonObject &obj) override
DeckLoader *parse(const QJsonObject &obj) override
{
DeckList deckList;
DeckLoader *loader = new DeckLoader(nullptr);
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
deckList.setName(deckName);
deckList.setComments(deckDescription);
loader->getDeckList()->setName(deckName);
loader->getDeckList()->setComments(deckDescription);
QString outputText;
QTextStream outStream(&outputText);
@@ -49,25 +47,25 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
deckList.loadFromStream_Plain(outStream, false);
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
loader->getDeckList()->loadFromStream_Plain(outStream, false);
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
return deckList;
return loader;
}
};
class MoxfieldJsonParser : public IJsonDeckParser
{
public:
DeckList parse(const QJsonObject &obj) override
DeckLoader *parse(const QJsonObject &obj) override
{
DeckList deckList;
DeckLoader *loader = new DeckLoader(nullptr);
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
deckList.setName(deckName);
deckList.setComments(deckDescription);
loader->getDeckList()->setName(deckName);
loader->getDeckList()->setComments(deckDescription);
QString outputText;
QTextStream outStream(&outputText);
@@ -96,8 +94,8 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
deckList.loadFromStream_Plain(outStream, false);
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
loader->getDeckList()->loadFromStream_Plain(outStream, false);
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
QJsonObject commandersObj = obj.value("commanders").toObject();
if (!commandersObj.isEmpty()) {
@@ -108,12 +106,12 @@ public:
QString collectorNumber = cardData.value("cn").toString();
QString providerId = cardData.value("scryfall_id").toString();
deckList.setBannerCard({commanderName, providerId});
deckList.addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
loader->getDeckList()->setBannerCard({commanderName, providerId});
loader->getDeckList()->addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
}
}
return deckList;
return loader;
}
};

View File

@@ -14,7 +14,6 @@
#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"
@@ -40,9 +39,7 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
{
auto *nam = new QNetworkAccessManager(this);
auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = nam->get(request);
QNetworkReply *reply = nam->get(QNetworkRequest(url));
if (saveResults) {
// This will write out to the file (used for spoiler.xml)

View File

@@ -2,7 +2,6 @@
#include "../network/update/client/release_channel.h"
#include "card_counter_settings.h"
#include "version_string.h"
#include <QAbstractListModel>
#include <QApplication>
@@ -199,13 +198,7 @@ SettingsCache::SettingsCache()
mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).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
}
checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
startupCardUpdateCheckPromptForUpdate =
settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool();
startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool();
@@ -213,15 +206,7 @@ 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();
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
}
updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt();
lang = settings->value("personal/lang").toString();
keepalive = settings->value("personal/keepalive", 3).toInt();
@@ -239,7 +224,6 @@ 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();
@@ -289,7 +273,6 @@ 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 =
@@ -310,7 +293,6 @@ SettingsCache::SettingsCache()
visualDeckStorageDefaultTagsList =
settings->value("interface/visualdeckstoragedefaulttagslist", defaultTags).toStringList();
visualDeckStorageSearchFolderNames = settings->value("interface/visualdeckstoragesearchfoldernames", true).toBool();
visualDeckStorageShowColorIdentity = settings->value("interface/visualdeckstorageshowcoloridentity", true).toBool();
visualDeckStorageShowBannerCardComboBox =
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
visualDeckStorageShowTagsOnDeckPreviews =
@@ -596,13 +578,6 @@ 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;
@@ -725,13 +700,6 @@ 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);
@@ -830,13 +798,6 @@ void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T val
settings->setValue("interface/visualdeckstoragesearchfoldernames", visualDeckStorageSearchFolderNames);
}
void SettingsCache::setVisualDeckStorageShowColorIdentity(QT_STATE_CHANGED_T value)
{
visualDeckStorageShowColorIdentity = value;
settings->setValue("interface/visualdeckstorageshowcoloridentity", visualDeckStorageShowColorIdentity);
emit visualDeckStorageShowColorIdentityChanged(visualDeckStorageShowColorIdentity);
}
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox)
{
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;

View File

@@ -143,10 +143,8 @@ 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();
@@ -158,7 +156,6 @@ signals:
void deckEditorTagsWidgetVisibleChanged(bool _visible);
void visualDeckStorageShowTagFilterChanged(bool _visible);
void visualDeckStorageDefaultTagsListChanged();
void visualDeckStorageShowColorIdentityChanged(bool _visible);
void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible);
void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible);
void visualDeckStorageCardSizeChanged();
@@ -224,7 +221,6 @@ private:
bool showTipsOnStartup;
QList<int> seenTips;
int homeTabBackgroundShuffleFrequency;
bool homeTabDisplayCardName;
bool mbDownloadSpoilers;
int updateReleaseChannel;
int maxFontSize;
@@ -240,7 +236,6 @@ private:
bool annotateTokens;
QByteArray tabGameSplitterSizes;
bool showShortcuts;
bool showGameSelectorFilterToolbar;
bool displayCardNames;
bool overrideAllCardArtWithPersonalPreference;
bool bumpSetsWithCardsInDeckToTop;
@@ -252,7 +247,6 @@ private:
bool deckEditorTagsWidgetVisible;
int visualDeckStorageSortingOrder;
bool visualDeckStorageShowFolders;
bool visualDeckStorageShowColorIdentity;
bool visualDeckStorageShowBannerCardComboBox;
bool visualDeckStorageShowTagsOnDeckPreviews;
bool visualDeckStorageShowTagFilter;
@@ -417,10 +411,6 @@ public:
{
return homeTabBackgroundShuffleFrequency;
}
[[nodiscard]] bool getHomeTabDisplayCardName() const
{
return homeTabDisplayCardName;
}
[[nodiscard]] bool getTabVisualDeckStorageOpen() const
{
return tabVisualDeckStorageOpen;
@@ -563,10 +553,6 @@ public:
{
return showShortcuts;
}
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const
{
return showGameSelectorFilterToolbar;
}
[[nodiscard]] bool getDisplayCardNames() const
{
return displayCardNames;
@@ -623,10 +609,6 @@ public:
{
return visualDeckStorageSearchFolderNames;
}
[[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const
{
return visualDeckStorageShowColorIdentity;
}
[[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const
{
return visualDeckStorageShowBannerCardComboBox;
@@ -1013,7 +995,6 @@ 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);
@@ -1036,7 +1017,6 @@ 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);
@@ -1051,7 +1031,6 @@ public slots:
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
void setVisualDeckStorageDefaultTagsList(QStringList _defaultTagsList);
void setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowColorIdentity(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
@@ -1124,4 +1103,5 @@ public slots:
void setMaxFontSize(int _max);
void setRoundCardCorners(bool _roundCardCorners);
};
#endif

View File

@@ -150,7 +150,12 @@ void ShortcutTreeView::currentChanged(const QModelIndex &current, const QModelIn
*/
void ShortcutTreeView::updateSearchString(const QString &searchString)
{
QStringList searchWords = searchString.split(" ", Qt::SkipEmptyParts);
#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);
auto escapeRegex = [](const QString &s) { return QRegularExpression::escape(s); };
std::transform(searchWords.begin(), searchWords.end(), searchWords.begin(), escapeRegex);

View File

@@ -660,12 +660,6 @@ 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)},

View File

@@ -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 / FormatQuery / CommentQuery / GenericQuery
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
@@ -22,10 +22,8 @@ CardSearch <- '[[' CardFilterString ']]'
CardFilterString <- (!']]'.)*
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
PathQuery <- [Pp] 'ath'? [:] String
FormatQuery <- [Ff] 'ormat'? [:] String
CommentQuery <- [Cc] ('omment' 's'?)? [:] String
GenericQuery <- String
@@ -120,13 +118,12 @@ static void setupParserRules()
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
int count = 0;
auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes();
for (auto node : cardNodes) {
deck->deckLoader->getDeckList()->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
count += node->getNumber();
}
}
});
return numberMatcher(count);
};
};
@@ -140,7 +137,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->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive);
return deck->deckLoader->getDeckList()->getName().contains(name, Qt::CaseInsensitive);
};
};
@@ -159,22 +156,6 @@ 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 &) {

View File

@@ -343,7 +343,10 @@ void DeckViewScene::rebuildTree()
if (!deck)
return;
for (auto *currentZone : deck->getZoneNodes()) {
InnerDecklistNode *listRoot = deck->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0);
if (!container) {
container = new DeckViewCardContainer(currentZone->getName());

View File

@@ -259,21 +259,22 @@ void DeckViewContainer::loadLocalDeck()
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
{
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath);
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
DeckLoader deck(this);
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(filePath, fmt, true);
bool success = deck.loadFromFile(filePath, fmt, true);
if (!deckOpt) {
if (!success) {
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
return;
}
loadDeckFromDeckList(deckOpt.value().deckList);
loadDeckFromDeckLoader(&deck);
}
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
void DeckViewContainer::loadDeckFromDeckLoader(DeckLoader *deck)
{
QString deckString = deck.writeToString_Native();
QString deckString = deck->getDeckList()->writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size."));
@@ -307,8 +308,8 @@ void DeckViewContainer::loadFromClipboard()
return;
}
DeckList deck = dlg.getDeckList();
loadDeckFromDeckList(deck);
DeckLoader *deck = dlg.getDeckList();
loadDeckFromDeckLoader(deck);
}
void DeckViewContainer::loadFromWebsite()
@@ -319,15 +320,16 @@ void DeckViewContainer::loadFromWebsite()
return;
}
DeckList deck = dlg.getDeck();
loadDeckFromDeckList(deck);
DeckLoader *deck = dlg.getDeck();
loadDeckFromDeckLoader(deck);
}
void DeckViewContainer::deckSelectFinished(const Response &r)
{
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
DeckList newDeck = DeckList(QString::fromStdString(resp.deck()));
CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(newDeck.getCardRefList()));
DeckLoader newDeck(this, new DeckList(QString::fromStdString(resp.deck())));
CardPictureLoader::cacheCardPixmaps(
CardDatabaseManager::query()->getCards(newDeck.getDeckList()->getCardRefList()));
setDeck(newDeck);
switchToDeckLoadedView();
}
@@ -408,8 +410,8 @@ void DeckViewContainer::setSideboardLocked(bool locked)
deckView->resetSideboardPlan();
}
void DeckViewContainer::setDeck(const DeckList &deck)
void DeckViewContainer::setDeck(DeckLoader &deck)
{
deckView->setDeck(deck);
deckView->setDeck(*deck.getDeckList());
switchToDeckLoadedView();
}

View File

@@ -85,12 +85,12 @@ public:
void setReadyStart(bool ready);
void readyAndUpdate();
void setSideboardLocked(bool locked);
void setDeck(const DeckList &deck);
void setDeck(DeckLoader &deck);
void setVisualDeckStorageExists(bool exists);
public slots:
void loadDeckFromFile(const QString &filePath);
void loadDeckFromDeckList(const DeckList &deck);
void loadDeckFromDeckLoader(DeckLoader *deck);
};
#endif // DECK_VIEW_CONTAINER_H

View File

@@ -117,7 +117,11 @@ 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;
@@ -219,7 +223,11 @@ 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
}
}

View File

@@ -60,16 +60,6 @@ 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());
@@ -114,9 +104,7 @@ void HandMenu::retranslateUi()
aSortHandByType->setText(tr("Type"));
aSortHandByManaValue->setText(tr("Mana Value"));
aMulligan->setText(tr("Take &mulligan (Choose hand size)"));
aMulliganSame->setText(tr("Take mulligan (Same hand size)"));
aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)"));
aMulligan->setText(tr("Take &mulligan"));
mMoveHandMenu->setTitle(tr("&Move hand to..."));
aMoveHandToTopLibrary->setText(tr("&Top of library"));
@@ -140,8 +128,6 @@ 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"));
}

View File

@@ -46,8 +46,6 @@ private:
QAction *aViewHand = nullptr;
QAction *aMulligan = nullptr;
QAction *aMulliganSame = nullptr;
QAction *aMulliganMinusOne = nullptr;
QMenu *mSortHand = nullptr;
QAction *aSortHandByName = nullptr;

View File

@@ -5,7 +5,6 @@
#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)
@@ -60,19 +59,21 @@ void UtilityMenu::populatePredefinedTokensMenu()
clear();
setEnabled(false);
predefinedTokens.clear();
const DeckList &deckList = player->getDeck();
DeckLoader *_deck = player->getDeck();
if (deckList.isEmpty()) {
if (!_deck) {
return;
}
auto tokenCardNodes = deckList.getCardNodes({DECK_ZONE_TOKENS});
InnerDecklistNode *tokenZone =
dynamic_cast<InnerDecklistNode *>(_deck->getDeckList()->getRoot()->findChild(DECK_ZONE_TOKENS));
if (!tokenCardNodes.isEmpty()) {
setEnabled(true);
if (tokenZone) {
if (!tokenZone->empty())
setEnabled(true);
for (int i = 0; i < tokenCardNodes.size(); ++i) {
const QString tokenName = tokenCardNodes[i]->getName();
for (int i = 0; i < tokenZone->size(); ++i) {
const QString tokenName = tokenZone->at(i)->getName();
predefinedTokens.append(tokenName);
QAction *a = addAction(tokenName);
if (i < 10) {

View File

@@ -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), zoneId(0), dialogSemaphore(false)
conceded(false), deck(nullptr), zoneId(0), dialogSemaphore(false)
{
initializeZones();
@@ -263,9 +263,10 @@ void Player::deleteCard(CardItem *card)
}
}
void Player::setDeck(const DeckList &_deck)
// TODO: Does a player need a DeckLoader?
void Player::setDeck(DeckLoader &_deck)
{
deck = _deck;
deck = new DeckLoader(this, _deck.getDeckList());
emit deckChanged();
}

View File

@@ -9,7 +9,6 @@
#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"
@@ -45,6 +44,7 @@ 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(const LoadedDeck &deck);
void openDeckEditor(DeckLoader *deck);
void deckChanged();
void newCardAdded(AbstractCardItem *card);
void rearrangeCounters();
@@ -130,9 +130,9 @@ public:
return playerMenu;
}
void setDeck(const DeckList &_deck);
void setDeck(DeckLoader &_deck);
[[nodiscard]] const DeckList &getDeck() const
[[nodiscard]] DeckLoader *getDeck() const
{
return deck;
}
@@ -241,7 +241,7 @@ private:
bool active;
bool conceded;
DeckList deck;
DeckLoader *deck;
int zoneId;
QMap<QString, CardZoneLogic *> zones;

View File

@@ -218,7 +218,7 @@ void PlayerActions::actAlwaysLookAtTopCard()
void PlayerActions::actOpenDeckInDeckEditor()
{
emit player->openDeckEditor({.deckList = player->getDeck()});
emit player->openDeckEditor(player->getDeck());
}
void PlayerActions::actViewGraveyard()
@@ -310,48 +310,28 @@ void PlayerActions::actMulligan()
{
int startSize = SettingsCache::instance().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize;
int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck
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;
}
if (number < 1) {
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);
if (number < 1) {
if (handSize == 0) {
return;
}
cmd.set_number(handSize + number);
} else {
cmd.set_number(number);
}
sendGameCommand(cmd);
if (startSize != number) {
SettingsCache::instance().setStartingHandSize(number);
}
}
void PlayerActions::actDrawCards()

View File

@@ -85,9 +85,6 @@ public slots:
void actDrawCards();
void actUndoDraw();
void actMulligan();
void actMulliganSameSize();
void actMulliganMinusOne();
void doMulligan(int number);
void actPlay();
void actPlayFacedown();

View File

@@ -78,7 +78,14 @@ 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()) {

View File

@@ -13,20 +13,12 @@
#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.
@@ -249,191 +241,33 @@ void ZoneViewWidget::retranslateUi()
pileViewCheckBox.setText(tr("pile view"));
}
void ZoneViewWidget::stopWindowDrag()
void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */)
{
if (!draggingWindow)
if (!scene())
return;
draggingWindow = false;
ungrabMouse();
}
int titleBarHeight = 24;
void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event)
{
draggingWindow = true;
dragStartItemPos = pos();
dragStartScreenPos = event->screenPos();
dragView = findDragView(event->widget());
QPointF scenePos = pos();
// 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.x() < 0) {
scenePos.setX(0);
} else {
qreal maxw = scene()->sceneRect().width() - 100;
if (scenePos.x() > maxw)
scenePos.setX(maxw);
}
// 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.y() < titleBarHeight) {
scenePos.setY(titleBarHeight);
} else {
qreal maxh = scene()->sceneRect().height() - titleBarHeight;
if (scenePos.y() > maxh)
scenePos.setY(maxh);
}
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);
if (scenePos != pos())
setPos(scenePos);
}
void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event)
@@ -516,7 +350,6 @@ 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();

View File

@@ -3,6 +3,7 @@
* @ingroup GameGraphicsZones
* @brief TODO: Document this.
*/
#ifndef ZONEVIEWWIDGET_H
#define ZONEVIEWWIDGET_H
@@ -13,7 +14,6 @@
#include <QGraphicsProxyWidget>
#include <QGraphicsWidget>
#include <QLineEdit>
#include <QPointer>
#include <libcockatrice/utility/macros.h>
class QLabel;
@@ -28,8 +28,6 @@ class ServerInfo_Card;
class QGraphicsSceneMouseEvent;
class QGraphicsSceneWheelEvent;
class QStyleOption;
class QGraphicsView;
class QWidget;
class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget
{
@@ -54,6 +52,7 @@ private:
ZoneViewZone *zone;
QGraphicsWidget *zoneContainer;
QPushButton *closeButton;
QScrollBar *scrollBar;
ScrollableGraphicsProxyWidget *scrollBarProxy;
@@ -67,33 +66,6 @@ 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);
@@ -104,6 +76,7 @@ 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();
@@ -128,10 +101,6 @@ 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;
};

View File

@@ -17,7 +17,12 @@ void AbstractGraphicsItem::paintNumberEllipse(int number,
font.setWeight(QFont::Bold);
QFontMetrics fm(font);
double w = 1.3 * fm.horizontalAdvance(numStr);
double w = 1.3 *
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
fm.horizontalAdvance(numStr);
#else
fm.width(numStr);
#endif
double h = fm.height() * 1.3;
if (w < h)
w = h;

View File

@@ -10,7 +10,6 @@
#include <QNetworkReply>
#include <QThread>
#include <utility>
#include <version_string.h>
static constexpr int MAX_REQUESTS_PER_SEC = 10;
@@ -21,7 +20,9 @@ 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 *
@@ -85,7 +86,6 @@ 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);
}

View File

@@ -1,40 +0,0 @@
#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);
}

View File

@@ -1,39 +0,0 @@
#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

View File

@@ -1,9 +0,0 @@
#include "deck_file_format.h"
DeckFileFormat::Format DeckFileFormat::getFormatFromName(const QString &fileName)
{
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
return Cockatrice;
}
return PlainText;
}

View File

@@ -1,36 +0,0 @@
#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

View File

@@ -25,33 +25,34 @@ 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)
DeckLoader::DeckLoader(QObject *parent) : QObject(parent), deckList(new DeckList())
{
}
std::optional<LoadedDeck>
DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
DeckLoader::DeckLoader(QObject *parent, DeckList *_deckList) : QObject(parent), deckList(_deckList)
{
}
bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "File does not exist:" << fileName;
return std::nullopt;
return false;
}
bool result = false;
DeckList deckList;
switch (fmt) {
case DeckFileFormat::PlainText:
result = deckList.loadFromFile_Plain(&file);
case PlainTextFormat:
result = deckList->loadFromFile_Plain(&file);
break;
case DeckFileFormat::Cockatrice: {
result = deckList.loadFromFile_Native(&file);
case CockatriceFormat: {
result = deckList->loadFromFile_Native(&file);
qCInfo(DeckLoaderLog) << "Loaded from" << fileName << "-" << result;
if (!result) {
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
<< "as cockatrice format; retrying as plain format";
qCInfo(DeckLoaderLog) << "Retrying as plain format";
file.seek(0);
result = deckList.loadFromFile_Plain(&file);
fmt = DeckFileFormat::PlainText;
result = deckList->loadFromFile_Plain(&file);
fmt = PlainTextFormat;
}
break;
}
@@ -60,128 +61,119 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
break;
}
if (!result) {
qCWarning(DeckLoaderLog) << "Failed to load " << fileName << "as" << fmt;
return std::nullopt;
}
LoadedDeck::LoadInfo lastLoadInfo = {
.fileName = fileName,
.fileFormat = fmt,
};
LoadedDeck loadedDeck = {deckList, lastLoadInfo};
if (userRequest) {
updateLastLoadedTimestamp(loadedDeck);
}
qCDebug(DeckLoaderLog) << "Loaded deck" << fileName << "with userRequest:" << userRequest;
return loadedDeck;
}
void DeckLoader::loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
{
QFuture<void> future = QtConcurrent::run([=, this] {
std::optional<LoadedDeck> deckOpt = loadFromFile(fileName, fmt, userRequest);
if (deckOpt) {
loadedDeck = deckOpt.value();
if (result) {
lastLoadInfo = {
.fileName = fileName,
.fileFormat = fmt,
};
if (userRequest) {
updateLastLoadedTimestamp(fileName, fmt);
}
emit loadFinished(deckOpt.has_value());
emit deckLoaded();
}
qCInfo(DeckLoaderLog) << "Deck was loaded -" << result;
return result;
}
bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest)
{
auto *watcher = new QFutureWatcher<bool>(this);
connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, fileName, fmt, userRequest]() {
const bool result = watcher->result();
watcher->deleteLater();
if (result) {
lastLoadInfo = {
.fileName = fileName,
.fileFormat = fmt,
};
if (userRequest) {
updateLastLoadedTimestamp(fileName, fmt);
}
emit deckLoaded();
}
emit loadFinished(result);
});
QFuture<bool> future = QtConcurrent::run([=, this]() {
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
switch (fmt) {
case PlainTextFormat:
return deckList->loadFromFile_Plain(&file);
case CockatriceFormat: {
bool result = false;
result = deckList->loadFromFile_Native(&file);
if (!result) {
file.seek(0);
return deckList->loadFromFile_Plain(&file);
}
return result;
}
default:
return false;
break;
}
});
watcher->setFuture(future);
return true; // Return immediately to indicate the async task was started
}
bool DeckLoader::reload()
bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
{
QString lastFileName = loadedDeck.lastLoadInfo.fileName;
if (lastFileName.isEmpty()) {
return false;
}
std::optional<LoadedDeck> deck = loadFromFile(lastFileName, loadedDeck.lastLoadInfo.fileFormat, false);
bool result = deckList->loadFromString_Native(nativeString);
if (result) {
lastLoadInfo = {
.remoteDeckId = remoteDeckId,
};
if (!deck) {
return false;
emit deckLoaded();
}
loadedDeck = *deck;
return true;
return result;
}
std::optional<LoadedDeck> DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
{
DeckList deckList;
bool success = deckList.loadFromString_Native(nativeString);
if (!success) {
qCWarning(DeckLoaderLog) << "Failed to load remote deck with id" << remoteDeckId << ":" << nativeString;
return std::nullopt;
}
LoadedDeck::LoadInfo lastLoadInfo = {.remoteDeckId = remoteDeckId};
LoadedDeck loadedDeck = {deckList, lastLoadInfo};
qCDebug(DeckLoaderLog) << "Loaded remote deck with id" << remoteDeckId;
return loadedDeck;
}
std::optional<LoadedDeck::LoadInfo>
DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFormat::Format fmt)
bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Could not create or open file:" << fileName;
return std::nullopt;
return false;
}
bool success = false;
bool result = false;
switch (fmt) {
case DeckFileFormat::PlainText:
success = deck.saveToFile_Plain(&file);
case PlainTextFormat:
result = deckList->saveToFile_Plain(&file);
break;
case DeckFileFormat::Cockatrice:
success = deck.saveToFile_Native(&file);
case CockatriceFormat:
result = deckList->saveToFile_Native(&file);
qCInfo(DeckLoaderLog) << "Saving to " << fileName << "-" << result;
break;
}
if (result) {
lastLoadInfo = {
.fileName = fileName,
.fileFormat = fmt,
};
qCInfo(DeckLoaderLog) << "Deck was saved -" << result;
}
file.flush();
file.close();
qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt << "-" << success;
if (!success) {
return std::nullopt;
}
LoadedDeck::LoadInfo lastLoadInfo = {fileName, fmt};
return lastLoadInfo;
return result;
}
bool DeckLoader::saveToFile(const LoadedDeck &deck)
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt)
{
auto opt = saveToFile(deck.deckList, deck.lastLoadInfo.fileName, deck.lastLoadInfo.fileFormat);
return opt.has_value();
}
bool DeckLoader::saveToNewFile(LoadedDeck &deck, const QString &fileName, DeckFileFormat::Format fmt)
{
std::optional<LoadedDeck::LoadInfo> infoOpt = saveToFile(deck.deckList, fileName, fmt);
if (infoOpt) {
deck.lastLoadInfo = infoOpt.value();
}
return infoOpt.has_value();
}
/**
* @brief Updates the lastLoadedTimestamp field in the file corresponding to the deck, without changing the
* FileModificationTime of the file.
*/
bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck)
{
QString fileName = deck.lastLoadInfo.fileName;
QFileInfo fileInfo(fileName);
if (!fileInfo.exists()) {
qCWarning(DeckLoaderLog) << "File does not exist:" << fileName;
@@ -200,19 +192,24 @@ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck)
bool result = false;
// Perform file modifications
switch (deck.lastLoadInfo.fileFormat) {
case DeckFileFormat::PlainText:
result = deck.deckList.saveToFile_Plain(&file);
switch (fmt) {
case PlainTextFormat:
result = deckList->saveToFile_Plain(&file);
break;
case DeckFileFormat::Cockatrice:
deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
result = deck.deckList.saveToFile_Native(&file);
case CockatriceFormat:
deckList->setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
result = deckList->saveToFile_Native(&file);
break;
}
file.close(); // Close the file to ensure changes are flushed
if (result) {
lastLoadInfo = {
.fileName = fileName,
.fileFormat = fmt,
};
// Re-open the file and set the original timestamp
if (!file.open(QIODevice::ReadWrite)) {
qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName;
@@ -272,35 +269,39 @@ 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;
// export all cards in zone
QString mainBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_MAIN}));
QString sideBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_SIDE}));
// 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;
}
// 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);
@@ -315,7 +316,113 @@ QString DeckLoader::exportDeckToDecklist(const DeckList &deckList, DecklistWebsi
return deckString;
}
void DeckLoader::saveToClipboard(const DeckList &deckList, bool addComments, bool addSetNameAndNumber)
// 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)
{
QString buffer;
QTextStream stream(&buffer);
@@ -325,7 +432,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)
{
@@ -334,7 +441,9 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
}
// loop zones
for (auto zoneNode : deckList.getZoneNodes()) {
for (int i = 0; i < deckList->getRoot()->size(); i++) {
const auto *zoneNode = dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i));
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
// end of zone
@@ -344,14 +453,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";
}
@@ -439,13 +548,8 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
}
}
bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
bool DeckLoader::convertToCockatriceFormat(QString fileName)
{
QString fileName = deck.lastLoadInfo.fileName;
if (fileName.isEmpty()) {
return false;
}
// Change the file extension to .cod
QFileInfo fileInfo(fileName);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
@@ -460,12 +564,12 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
bool result = false;
// Perform file modifications based on the detected format
switch (DeckFileFormat::getFormatFromName(fileName)) {
case DeckFileFormat::PlainText:
switch (getFormatFromName(fileName)) {
case PlainTextFormat:
// Save in Cockatrice's native format
result = deck.deckList.saveToFile_Native(&file);
result = deckList->saveToFile_Native(&file);
break;
case DeckFileFormat::Cockatrice:
case CockatriceFormat:
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
result = true;
break;
@@ -484,16 +588,39 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
} else {
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
}
deck.lastLoadInfo = {
lastLoadInfo = {
.fileName = newFileName,
.fileFormat = DeckFileFormat::Cockatrice,
.fileFormat = CockatriceFormat,
};
}
return result;
}
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
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)
{
const int totalColumns = 2;
@@ -553,7 +680,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
cursor->movePosition(QTextCursor::End);
}
void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList)
void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList)
{
QTextDocument doc;
@@ -569,18 +696,19 @@ 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 (auto zoneNode : deckList.getZoneNodes()) {
for (int i = 0; i < deckList->getRoot()->size(); i++) {
cursor.insertHtml("<br><img src=theme:hr.jpg>");
// cursor.insertHtml("<hr>");
cursor.insertBlock(headerBlockFormat, headerCharFormat);
printDeckListNode(&cursor, zoneNode);
printDeckListNode(&cursor, dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i)));
}
doc.print(printer);

View File

@@ -7,22 +7,42 @@
#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:
void deckLoaded();
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
*/
@@ -40,85 +60,47 @@ public:
};
private:
LoadedDeck loadedDeck;
DeckList *deckList;
LoadInfo lastLoadInfo;
public:
DeckLoader(QObject *parent);
DeckLoader(QObject *parent, DeckList *_deckList);
DeckLoader(const DeckLoader &) = delete;
DeckLoader &operator=(const DeckLoader &) = delete;
[[nodiscard]] bool hasNotBeenLoaded() const
const LoadInfo &getLastLoadInfo() const
{
return loadedDeck.lastLoadInfo.isEmpty();
return lastLoadInfo;
}
/**
* @brief Asynchronously loads a deck from a local file into this DeckLoader.
* The `loadFinished` signal will be emitted when the load finishes.
* Once the loading finishes, the deck can be accessed with `getDeck`
* @param fileName The file to load
* @param fmt The format of the file to load
* @param userRequest Whether the load was manually requested by the user, instead of being done in the background.
*/
void loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest);
void setLastLoadInfo(const LoadInfo &info)
{
lastLoadInfo = info;
}
/**
* @brief Loads the file that the lastLoadInfo currently points to into this instance.
* No-ops if the lastLoadInfo is missing the required info or the load fails.
* @return Whether the loaded succeeded.
*/
bool reload();
[[nodiscard]] bool hasNotBeenLoaded() const
{
return lastLoadInfo.fileName.isEmpty() && lastLoadInfo.remoteDeckId == LoadInfo::NON_REMOTE_ID;
}
/**
* @brief Loads a deck from a local file.
* @param fileName The file to load
* @param fmt The format of the file to load
* @param userRequest Whether the load was manually requested by the user, instead of being done in the background.
* @return An optional containing the LoadedDeck, or empty if the load failed.
*/
static std::optional<LoadedDeck>
loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest = false);
static void clearSetNamesAndNumbers(const DeckList *deckList);
static FileFormat getFormatFromName(const QString &fileName);
/**
* @brief Loads a deck from the response of a remote deck request
* @param nativeString The deck string, in cod format
* @param remoteDeckId The remote deck id
* @return An optional containing the LoadedDeck, or empty if the load failed.
*/
static std::optional<LoadedDeck> loadFromRemote(const QString &nativeString, int remoteDeckId);
bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false);
bool loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest);
bool loadFromRemote(const QString &nativeString, int remoteDeckId);
bool saveToFile(const QString &fileName, FileFormat fmt);
bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt);
/**
* @brief Saves a DeckList to a local file.
* @param deck The DeckList
* @param fileName The file to write to
* @param fmt The deck file format to use
* @return An optional containing the LoadInfo for the new file, or empty if the save failed.
*/
static std::optional<LoadedDeck::LoadInfo>
saveToFile(const DeckList &deck, const QString &fileName, DeckFileFormat::Format fmt);
static QString exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website);
/**
* @brief Saves a LoadedDeck to a local file.
* Uses the lastLoadInfo in the LoadedDeck to determine where to save to.
* @param deck The LoadedDeck to save. Should have valid lastLoadInfo.
* @return Whether the save succeeded.
*/
static bool saveToFile(const LoadedDeck &deck);
static void setProviderIdToPreferredPrinting(const DeckList *deckList);
static void resolveSetNameAndNumberToProviderID(const DeckList *deckList);
/**
* @brief Saves a LoadedDeck to a new local file.
* @param deck The LoadedDeck to save. Will update the lastLoadInfo.
* @param fileName The file to write to
* @param fmt The deck file format to use
* @return Whether the save succeeded.
*/
static bool saveToNewFile(LoadedDeck &deck, const QString &fileName, DeckFileFormat::Format fmt);
static QString exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website);
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);
@@ -127,33 +109,18 @@ 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);
/**
* Converts the given deck's file to the cockatrice file format.
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
* @return Whether the conversion succeeded.
*/
static bool convertToCockatriceFormat(LoadedDeck &deck);
bool convertToCockatriceFormat(QString fileName);
LoadedDeck &getDeck()
DeckList *getDeckList()
{
return loadedDeck;
}
const LoadedDeck &getDeck() const
{
return loadedDeck;
}
void setDeck(const LoadedDeck &deck)
{
loadedDeck = deck;
return deckList;
}
private:
static bool updateLastLoadedTimestamp(LoadedDeck &deck);
static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node);
static void saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList);
static void printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node);
static void saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList);
static void saveToStream_DeckZone(QTextStream &out,
const InnerDecklistNode *zoneNode,
@@ -164,6 +131,9 @@ 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

View File

@@ -1,11 +0,0 @@
#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();
}

View File

@@ -1,39 +0,0 @@
#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

View File

@@ -57,10 +57,17 @@ 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;
}
@@ -70,7 +77,11 @@ 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();
}
@@ -92,7 +103,11 @@ 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
}
}

View File

@@ -8,10 +8,30 @@
#include <QRegularExpression>
#include <QResizeEvent>
#include <QSize>
#include <libcockatrice/utility/qt_utils.h>
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity)
: QWidget(parent), colorIdentity(_colorIdentity)
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)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
@@ -25,21 +45,12 @@ ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const QString &_colorI
&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(colorIdentity); // Parse mana cost string
QStringList symbols = parseColorIdentity(manaCost); // 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));
@@ -53,18 +64,15 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
}
}
void ColorIdentityWidget::setColorIdentity(const QString &_colorIdentity)
{
if (colorIdentity == _colorIdentity) {
return;
}
colorIdentity = _colorIdentity;
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();
}
@@ -89,12 +97,12 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
}
}
QStringList ColorIdentityWidget::parseColorIdentity(const QString &manaString)
QStringList ColorIdentityWidget::parseColorIdentity(const QString &cmc)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = manaString.split(" // ");
QStringList splitCosts = cmc.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);

View File

@@ -15,20 +15,19 @@ class ColorIdentityWidget : public QWidget
{
Q_OBJECT
public:
explicit ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity = "");
explicit ColorIdentityWidget(QWidget *parent, const CardInfoPtr &card);
explicit ColorIdentityWidget(QWidget *parent, CardInfoPtr card);
explicit ColorIdentityWidget(QWidget *parent, QString manaCost);
void populateManaSymbolWidgets();
static QStringList parseColorIdentity(const QString &manaString);
QStringList parseColorIdentity(const QString &manaString);
public slots:
void setColorIdentity(const QString &_colorIdentity);
void resizeEvent(QResizeEvent *event) override;
void toggleUnusedVisibility();
private:
QString colorIdentity;
CardInfoPtr card;
QString manaCost;
QHBoxLayout *layout;
};

View File

@@ -37,18 +37,147 @@ CardGroupDisplayWidget::CardGroupDisplayWidget(QWidget *parent,
&CardGroupDisplayWidget::onSelectionChanged);
}
connect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);
connect(deckListModel, &QAbstractItemModel::dataChanged, this, &CardGroupDisplayWidget::onDataChanged);
}
// Just here so it can get overwritten in subclasses.
void CardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
QWidget::resizeEvent(event);
auto proxyModel = qobject_cast<QAbstractProxyModel *>(selectionModel->model());
for (auto &range : selected) {
for (int row = range.top(); row <= range.bottom(); ++row) {
QModelIndex idx = range.model()->index(row, 0, range.parent());
if (proxyModel) {
idx = proxyModel->mapToSource(idx);
}
auto it = indexToWidgetMap.find(QPersistentModelIndex(idx));
if (it != indexToWidgetMap.end()) {
if (auto displayWidget = qobject_cast<CardInfoPictureWithTextOverlayWidget *>(it.value())) {
displayWidget->setHighlighted(true);
}
}
}
}
for (auto &range : deselected) {
for (int row = range.top(); row <= range.bottom(); ++row) {
QModelIndex idx = range.model()->index(row, 0, range.parent());
if (proxyModel)
idx = proxyModel->mapToSource(idx);
auto it = indexToWidgetMap.find(QPersistentModelIndex(idx));
if (it != indexToWidgetMap.end()) {
if (auto displayWidget = qobject_cast<CardInfoPictureWithTextOverlayWidget *>(it.value())) {
displayWidget->setHighlighted(false);
}
}
}
}
}
// =====================================================================================================================
// User Interaction
// =====================================================================================================================
void CardGroupDisplayWidget::clearAllDisplayWidgets()
{
for (auto idx : indexToWidgetMap.keys()) {
auto displayWidget = indexToWidgetMap.value(idx);
removeFromLayout(displayWidget);
indexToWidgetMap.remove(idx);
delete displayWidget;
}
}
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 widget = new CardInfoPictureWithTextOverlayWidget(getLayoutParent(), true);
widget->setScaleFactor(cardSizeWidget->getSlider()->value());
widget->setCard(CardDatabaseManager::query()->getCard({cardName, cardProviderId}));
connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &CardGroupDisplayWidget::onClick);
connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::onHover);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor);
indexToWidgetMap.insert(index, widget);
return widget;
}
void CardGroupDisplayWidget::updateCardDisplays()
{
DeckListSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel);
proxy.setSortCriteria(activeSortCriteria);
// 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);
// 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
// 2. iterate children under the proxy parent
for (int i = 0; i < proxy.rowCount(proxyParent); ++i) {
QModelIndex proxyIndex = proxy.index(i, 0, proxyParent);
// 3. map back to source
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
// 4. persist the source index
QPersistentModelIndex persistent(sourceIndex);
addToLayout(constructWidgetForIndex(persistent));
}
}
void CardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first, int last)
{
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
return;
}
if (parent == trackedIndex) {
for (int row = first; row <= last; ++row) {
QModelIndex child = deckListModel->index(row, 0, parent);
// Persist the index
QPersistentModelIndex persistent(child);
insertIntoLayout(constructWidgetForIndex(persistent), row);
}
}
}
void CardGroupDisplayWidget::onCardRemoval(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(first);
Q_UNUSED(last);
if (parent == trackedIndex) {
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
if (!idx.isValid()) {
removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
}
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
}
}
}
void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria)
{
activeSortCriteria = std::move(_activeSortCriteria);
clearAllDisplayWidgets();
updateCardDisplays();
}
void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event)
{
@@ -68,280 +197,7 @@ void CardGroupDisplayWidget::onHover(const ExactCard &card)
emit cardHovered(card);
}
void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
void CardGroupDisplayWidget::resizeEvent(QResizeEvent *event)
{
auto proxyModel = qobject_cast<QAbstractProxyModel *>(selectionModel->model());
for (auto &range : selected) {
for (int row = range.top(); row <= range.bottom(); ++row) {
QModelIndex idx = range.model()->index(row, 0, range.parent());
if (proxyModel) {
idx = proxyModel->mapToSource(idx);
}
auto it = indexToWidgetMap.find(QPersistentModelIndex(idx));
if (it != indexToWidgetMap.end()) {
// Highlight all copies of this card
for (auto widget : it.value()) {
if (auto displayWidget = qobject_cast<CardInfoPictureWithTextOverlayWidget *>(widget)) {
displayWidget->setHighlighted(true);
}
}
}
}
}
for (auto &range : deselected) {
for (int row = range.top(); row <= range.bottom(); ++row) {
QModelIndex idx = range.model()->index(row, 0, range.parent());
if (proxyModel)
idx = proxyModel->mapToSource(idx);
auto it = indexToWidgetMap.find(QPersistentModelIndex(idx));
if (it != indexToWidgetMap.end()) {
// Un-highlight all copies of this card
for (auto widget : it.value()) {
if (auto displayWidget = qobject_cast<CardInfoPictureWithTextOverlayWidget *>(widget)) {
displayWidget->setHighlighted(false);
}
}
}
}
}
}
void CardGroupDisplayWidget::refreshSelectionForIndex(const QPersistentModelIndex &persistent)
{
if (!selectionModel || !indexToWidgetMap.contains(persistent)) {
return;
}
// Convert persistent index to regular index for selection check
QModelIndex idx = QModelIndex(persistent);
// Check if this index is selected
// We need to check against the selection model's model (which might be a proxy)
bool isSelected = false;
if (auto proxyModel = qobject_cast<QAbstractProxyModel *>(selectionModel->model())) {
// Map source index to proxy
QModelIndex proxyIdx = proxyModel->mapFromSource(idx);
isSelected = selectionModel->isSelected(proxyIdx);
} else {
isSelected = selectionModel->isSelected(idx);
}
// Apply selection state to all widgets for this index
for (auto widget : indexToWidgetMap[persistent]) {
if (auto displayWidget = qobject_cast<CardInfoPictureWithTextOverlayWidget *>(widget)) {
displayWidget->setHighlighted(isSelected);
}
}
}
// =====================================================================================================================
// Display Widget Management
// =====================================================================================================================
QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex index)
{
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());
widget->setCard(CardDatabaseManager::query()->getCard({cardName, cardProviderId}));
connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &CardGroupDisplayWidget::onClick);
connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::onHover);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor);
indexToWidgetMap[index].append(widget);
return widget;
}
void CardGroupDisplayWidget::updateCardDisplays()
{
DeckListSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel);
proxy.setSortCriteria(activeSortCriteria);
// This doesn't really matter since overwrite the whole lessThan function to just compare dynamically anyway.
proxy.setSortRole(Qt::EditRole);
proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
// 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
// 2. iterate children under the proxy parent
for (int i = 0; i < proxy.rowCount(proxyParent); ++i) {
QModelIndex proxyIndex = proxy.index(i, 0, proxyParent);
// 3. map back to source
QModelIndex sourceIndex = proxy.mapToSource(proxyIndex);
// 4. persist the source index
QPersistentModelIndex persistent(sourceIndex);
// Get the card amount
int cardAmount =
sourceIndex.sibling(sourceIndex.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
// Create multiple widgets for the card count
for (int copy = 0; copy < cardAmount; ++copy) {
addToLayout(constructWidgetForIndex(persistent));
}
}
}
void CardGroupDisplayWidget::clearAllDisplayWidgets()
{
auto it = indexToWidgetMap.begin();
while (it != indexToWidgetMap.end()) {
for (auto displayWidget : it.value()) {
removeFromLayout(displayWidget);
delete displayWidget;
}
it = indexToWidgetMap.erase(it);
}
}
// =====================================================================================================================
// DeckListModel Signal Responses
// =====================================================================================================================
void CardGroupDisplayWidget::onCardAddition(const QModelIndex &parent, int first, int last)
{
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
return;
}
if (parent == trackedIndex) {
for (int row = first; row <= last; ++row) {
QModelIndex child = deckListModel->index(row, 0, parent);
// Persist the index
QPersistentModelIndex persistent(child);
// Get the card amount for the newly added card
int cardAmount = child.sibling(child.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
// Insert multiple copies
for (int copy = 0; copy < cardAmount; ++copy) {
insertIntoLayout(constructWidgetForIndex(persistent), row);
}
}
}
}
void CardGroupDisplayWidget::onCardRemoval(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(first);
Q_UNUSED(last);
if (parent != trackedIndex) {
return;
}
// Use iterator so we can remove while iterating
auto it = indexToWidgetMap.begin();
while (it != indexToWidgetMap.end()) {
const QPersistentModelIndex &idx = it.key();
bool shouldRemove = !idx.isValid() || it.value().isEmpty();
if (shouldRemove) {
// Clean up widgets
for (auto widget : it.value()) {
removeFromLayout(widget);
widget->deleteLater();
}
// Erase and advance iterator
it = indexToWidgetMap.erase(it);
} else {
++it;
}
}
if (!trackedIndex.isValid()) {
emit cleanupRequested(this);
}
}
void CardGroupDisplayWidget::onDataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QVector<int> &roles)
{
if (topLeft.parent() != trackedIndex && bottomRight.parent() != trackedIndex) {
return;
}
// Check if CARD_AMOUNT column changed
bool amountChanged = (topLeft.column() <= DeckListModelColumns::CARD_AMOUNT &&
bottomRight.column() >= DeckListModelColumns::CARD_AMOUNT) ||
roles.isEmpty() || roles.contains(Qt::EditRole);
if (!amountChanged) {
return;
}
// For each affected row, adjust widget count
for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
QModelIndex idx = deckListModel->index(row, 0, trackedIndex);
if (!idx.isValid()) {
continue;
}
QPersistentModelIndex persistent(idx);
int newAmount = idx.sibling(idx.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
// Get current widget count
int currentWidgetCount = indexToWidgetMap.contains(persistent) ? indexToWidgetMap.value(persistent).count() : 0;
if (newAmount == currentWidgetCount) {
// Still refresh selection even if count didn't change
refreshSelectionForIndex(persistent);
continue;
}
if (newAmount < currentWidgetCount) {
// Remove excess widgets
int toRemove = currentWidgetCount - newAmount;
for (int i = 0; i < toRemove; ++i) {
if (!indexToWidgetMap[persistent].isEmpty()) {
QWidget *widget = indexToWidgetMap[persistent].takeLast();
removeFromLayout(widget);
widget->deleteLater();
}
}
// If all widgets removed, clean up the map entry
if (indexToWidgetMap[persistent].isEmpty()) {
indexToWidgetMap.remove(persistent);
}
} else {
// Add new widgets
int toAdd = newAmount - currentWidgetCount;
for (int i = 0; i < toAdd; ++i) {
addToLayout(constructWidgetForIndex(persistent));
}
}
// Always refresh selection state after modifying widgets
refreshSelectionForIndex(persistent);
}
}
void CardGroupDisplayWidget::onActiveSortCriteriaChanged(QStringList _activeSortCriteria)
{
activeSortCriteria = std::move(_activeSortCriteria);
clearAllDisplayWidgets();
updateCardDisplays();
QWidget::resizeEvent(event);
}

View File

@@ -33,13 +33,12 @@ public:
int bannerOpacity,
CardSizeWidget *cardSizeWidget);
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
void refreshSelectionForIndex(const QPersistentModelIndex &persistent);
void clearAllDisplayWidgets();
DeckListModel *deckListModel;
QItemSelectionModel *selectionModel;
QPersistentModelIndex trackedIndex;
QMap<QPersistentModelIndex, QList<QWidget *>> indexToWidgetMap;
QHash<QPersistentModelIndex, QWidget *> indexToWidgetMap;
QString zoneName;
QString cardGroupCategory;
QString activeGroupCriteria;
@@ -54,7 +53,6 @@ public slots:
virtual void updateCardDisplays();
virtual void onCardAddition(const QModelIndex &parent, int first, int last);
virtual void onCardRemoval(const QModelIndex &parent, int first, int last);
void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles);
void onActiveSortCriteriaChanged(QStringList activeSortCriteria);
void resizeEvent(QResizeEvent *event) override;

View File

@@ -31,17 +31,13 @@ FlatCardGroupDisplayWidget::FlatCardGroupDisplayWidget(QWidget *parent,
layout->addWidget(flowWidget);
// Clear all existing widgets
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
for (auto widget : indexToWidgetMap.value(idx)) {
FlatCardGroupDisplayWidget::removeFromLayout(widget);
widget->deleteLater();
}
FlatCardGroupDisplayWidget::removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}
FlatCardGroupDisplayWidget::updateCardDisplays();
disconnect(deckListModel, &QAbstractItemModel::rowsInserted, this, &CardGroupDisplayWidget::onCardAddition);
disconnect(deckListModel, &QAbstractItemModel::rowsRemoved, this, &CardGroupDisplayWidget::onCardRemoval);

View File

@@ -30,12 +30,9 @@ OverlappedCardGroupDisplayWidget::OverlappedCardGroupDisplayWidget(QWidget *pare
layout->addWidget(overlapWidget);
// Clear all existing widgets
for (const QPersistentModelIndex &idx : indexToWidgetMap.keys()) {
for (auto widget : indexToWidgetMap.value(idx)) {
OverlappedCardGroupDisplayWidget::removeFromLayout(widget);
widget->deleteLater();
}
OverlappedCardGroupDisplayWidget::removeFromLayout(indexToWidgetMap.value(idx));
indexToWidgetMap.value(idx)->deleteLater();
indexToWidgetMap.remove(idx);
}

View File

@@ -27,7 +27,7 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa
layout->addWidget(text, 0, Qt::AlignCenter);
setLayout(layout);
setFrameStyle(static_cast<int>(QFrame::Panel) | QFrame::Raised);
setFrameStyle(QFrame::Panel | QFrame::Raised);
int pixmapHeight = QGuiApplication::primaryScreen()->geometry().height() / 3;
int pixmapWidth = static_cast<int>(pixmapHeight / aspectRatio);

View File

@@ -39,6 +39,10 @@ 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);
@@ -47,7 +51,7 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
originalPos = this->pos();
// Create the animation
animation = new QPropertyAnimation(this, "pos", this);
animation = new QPropertyAnimation(this, "pos");
animation->setDuration(200); // 200ms animation duration
animation->setEasingCurve(QEasingCurve::OutQuad);
@@ -273,7 +277,7 @@ void CardInfoPictureWidget::leaveEvent(QEvent *event)
if (hoverToZoomEnabled) {
hoverTimer->stop();
destroyEnlargedPixmapWidget();
enlargedPixmapWidget->hide();
}
if (raiseOnEnter) {
@@ -290,7 +294,7 @@ void CardInfoPictureWidget::moveEvent(QMoveEvent *event)
QWidget::moveEvent(event);
hoverTimer->stop();
destroyEnlargedPixmapWidget();
enlargedPixmapWidget->hide();
if (animation->state() == QAbstractAnimation::Running) {
return;
@@ -306,7 +310,7 @@ void CardInfoPictureWidget::mouseMoveEvent(QMouseEvent *event)
{
QWidget::mouseMoveEvent(event);
if (hoverToZoomEnabled && enlargedPixmapWidget && enlargedPixmapWidget->isVisible()) {
if (hoverToZoomEnabled && enlargedPixmapWidget->isVisible()) {
const QPoint cursorPos = QCursor::pos();
const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry();
const QSize widgetSize = enlargedPixmapWidget->size();
@@ -340,7 +344,7 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event)
void CardInfoPictureWidget::hideEvent(QHideEvent *event)
{
destroyEnlargedPixmapWidget();
enlargedPixmapWidget->hide();
QWidget::hideEvent(event);
}
@@ -440,19 +444,12 @@ QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu()
* If card information is available, the enlarged pixmap is loaded, positioned near the cursor,
* and displayed.
*/
void CardInfoPictureWidget::showEnlargedPixmap()
void CardInfoPictureWidget::showEnlargedPixmap() const
{
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);
@@ -463,6 +460,7 @@ void CardInfoPictureWidget::showEnlargedPixmap()
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;
}
@@ -474,11 +472,3 @@ void CardInfoPictureWidget::showEnlargedPixmap()
enlargedPixmapWidget->show();
}
void CardInfoPictureWidget::destroyEnlargedPixmapWidget()
{
if (enlargedPixmapWidget) {
enlargedPixmapWidget->deleteLater();
enlargedPixmapWidget = nullptr;
}
}

View File

@@ -63,8 +63,7 @@ protected:
{
return resizedPixmap;
}
void showEnlargedPixmap();
void destroyEnlargedPixmapWidget();
void showEnlargedPixmap() const;
private:
ExactCard exactCard;

View File

@@ -2,7 +2,6 @@
#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>
@@ -23,7 +22,6 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
displayType(_displayType), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity),
cardSizeWidget(_cardSizeWidget)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout = new QVBoxLayout(this);
setLayout(layout);
@@ -47,20 +45,6 @@ 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) {
@@ -84,17 +68,24 @@ void DeckCardZoneDisplayWidget::onSelectionChanged(const QItemSelection &selecte
}
}
// =====================================================================================================================
// Display Widget Management
// =====================================================================================================================
void DeckCardZoneDisplayWidget::cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget)
{
cardGroupLayout->removeWidget(displayWidget);
displayWidget->setParent(nullptr);
for (auto idx : indexToWidgetMap.keys()) {
if (!idx.isValid()) {
indexToWidgetMap.remove(idx);
}
}
delete displayWidget;
}
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,
@@ -129,7 +120,7 @@ void DeckCardZoneDisplayWidget::displayCards()
QSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel);
proxy.setSortRole(Qt::EditRole);
proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
proxy.sort(1, Qt::AscendingOrder);
// 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
@@ -148,45 +139,6 @@ 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()) {
@@ -219,6 +171,48 @@ 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;
@@ -235,13 +229,10 @@ QList<QString> DeckCardZoneDisplayWidget::getGroupCriteriaValueList()
{
QList<QString> groupCriteriaValues;
QList<const DecklistCardNode *> nodes = deckListModel->getCardNodesForZone(zoneName);
QList<ExactCard> cardsInZone = deckListModel->getCardsForZone(zoneName);
for (auto node : nodes) {
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());
if (info) {
groupCriteriaValues.append(info->getProperty(activeGroupCriteria));
}
for (const ExactCard &cardInZone : cardsInZone) {
groupCriteriaValues.append(cardInZone.getInfo().getProperty(activeGroupCriteria));
}
groupCriteriaValues.removeDuplicates();

View File

@@ -40,6 +40,7 @@ public:
QPersistentModelIndex trackedIndex;
QString zoneName;
void addCardsToOverlapWidget();
void resizeEvent(QResizeEvent *event) override;
public slots:
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);

View File

@@ -17,6 +17,7 @@
* @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.
*/

View File

@@ -1,49 +0,0 @@
#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;
}

View File

@@ -1,61 +0,0 @@
#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

View File

@@ -1,32 +0,0 @@
#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);
}

View File

@@ -1,29 +0,0 @@
#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

View File

@@ -1,33 +0,0 @@
#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();
}

View File

@@ -1,44 +0,0 @@
#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

View File

@@ -1 +0,0 @@
#include "analytics_panel_widget_registrar.h"

View File

@@ -1,17 +0,0 @@
#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

View File

@@ -1,28 +0,0 @@
#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;
}

View File

@@ -1,19 +0,0 @@
#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

View File

@@ -1,92 +0,0 @@
#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();
}

View File

@@ -1,44 +0,0 @@
#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;
};

View File

@@ -1,238 +0,0 @@
#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);
controlLayout->setContentsMargins(11, 0, 11, 0);
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);
resultTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
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);
}

View File

@@ -1,54 +0,0 @@
#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

View File

@@ -1,32 +0,0 @@
#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;
}

View File

@@ -1,19 +0,0 @@
#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

View File

@@ -1,67 +0,0 @@
#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();
}

View File

@@ -1,42 +0,0 @@
#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

Some files were not shown because too many files have changed in this diff Show More