Merge branch 'master' into tooomm-doxy_awesome_theme

This commit is contained in:
tooomm 2026-01-12 22:32:24 +01:00
commit 7613db0348
211 changed files with 10701 additions and 4480 deletions

View file

@ -156,6 +156,18 @@ function ccachestatsverbose() {
# Compile # Compile
if [[ $RUNNER_OS == macOS ]]; then 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 if [[ $TARGET_MACOS_VERSION ]]; then
# CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version

View file

@ -0,0 +1,40 @@
#!/bin/bash
# This script is used to resolve the latest patch version of Qt using aqtinstall.
# It interprets wildcards to get the latest patch version. E.g. "6.6.*" -> "6.6.3".
# This script is meant to be used by the ci enironment.
# It uses the runner's GITHUB_OUTPUT env variable.
# Usage example: .ci/resolve_latest_aqt_qt_version.sh "6.6.*"
qt_spec=$1
if [[ ! $qt_spec ]]; then
echo "usage: $0 [version]"
exit 2
fi
# If version is already specific (no wildcard), use it as-is
if [[ $qt_spec != *"*" ]]; then
echo "version $qt_spec is already resolved"
echo "version=$qt_spec" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! hash aqt; then
echo "aqt could not be found, has aqtinstall been installed?"
exit 2
fi
# Resolve latest patch
if ! qt_resolved=$(aqt list-qt mac desktop --spec "$qt_spec" --latest-version); then
exit 1
fi
echo "resolved $qt_spec to $qt_resolved"
if [[ ! $qt_resolved ]]; then
echo "Error: Could not resolve Qt version for $qt_spec"
exit 1
fi
echo "version=$qt_resolved" >> "$GITHUB_OUTPUT"

25
.ci/thin_macos_qtlib.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# The macos binaries from aqt are fat (universal), so we thin them to the target architecture to reduce the size of
# the packages and caches using lipo.
# This script is meant to be used by the ci enironment on macos runners only.
# It uses the runner's GITHUB_WORKSPACE env variable.
arch=$(uname -m)
nproc=$(sysctl -n hw.ncpu)
function thin() {
local libfile=$1
if [[ $(file -b --mime-type "$libfile") == application/x-mach-binary* ]]; then
echo "Processing $libfile"
lipo "$libfile" -thin "$arch" -output "$libfile"
fi
return 0
}
export -f thin # export to allow use in xargs
export arch
set -eo pipefail
echo "::group::Thinning Qt libraries to $arch using $nproc cores"
find "$GITHUB_WORKSPACE/Qt" -type f -print0 | xargs -0 -n1 -P"$nproc" -I{} bash -c "thin '{}'"
echo "::endgroup::"

View file

@ -142,7 +142,6 @@ jobs:
- distro: Ubuntu - distro: Ubuntu
version: 22.04 version: 22.04
package: DEB package: DEB
test: skip # Running tests on all distros is superfluous
- distro: Ubuntu - distro: Ubuntu
version: 24.04 version: 24.04
@ -213,7 +212,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
id: upload_artifact id: upload_artifact
if: matrix.package != 'skip' if: matrix.package != 'skip'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: ${{matrix.distro}}${{matrix.version}}-package name: ${{matrix.distro}}${{matrix.version}}-package
path: ${{steps.build.outputs.path}} path: ${{steps.build.outputs.path}}
@ -263,7 +262,6 @@ jobs:
qt_version: 6.6.* qt_version: 6.6.*
qt_arch: clang_64 qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false # qt caches take too much space for macOS (1.1Gi)
cmake_generator: Ninja cmake_generator: Ninja
use_ccache: 1 use_ccache: 1
@ -279,7 +277,6 @@ jobs:
qt_version: 6.6.* qt_version: 6.6.*
qt_arch: clang_64 qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja cmake_generator: Ninja
use_ccache: 1 use_ccache: 1
@ -295,7 +292,6 @@ jobs:
qt_version: 6.6.* qt_version: 6.6.*
qt_arch: clang_64 qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja cmake_generator: Ninja
use_ccache: 1 use_ccache: 1
@ -308,7 +304,6 @@ jobs:
qt_version: 6.6.* qt_version: 6.6.*
qt_arch: clang_64 qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja cmake_generator: Ninja
use_ccache: 1 use_ccache: 1
@ -321,7 +316,6 @@ jobs:
artifact_name: Windows7-installer artifact_name: Windows7-installer
qt_version: 5.15.* qt_version: 5.15.*
qt_arch: win64_msvc2019_64 qt_arch: win64_msvc2019_64
cache_qt: true
cmake_generator: "Visual Studio 17 2022" cmake_generator: "Visual Studio 17 2022"
cmake_generator_platform: x64 cmake_generator_platform: x64
@ -335,13 +329,17 @@ jobs:
qt_version: 6.6.* qt_version: 6.6.*
qt_arch: win64_msvc2019_64 qt_arch: win64_msvc2019_64
qt_modules: qtimageformats qtmultimedia qtwebsockets qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: true
cmake_generator: "Visual Studio 17 2022" cmake_generator: "Visual Studio 17 2022"
cmake_generator_platform: x64 cmake_generator_platform: x64
name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
needs: configure needs: configure
runs-on: ${{matrix.runner}} 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: steps:
- name: Checkout - name: Checkout
@ -356,24 +354,72 @@ jobs:
with: with:
msbuild-architecture: x64 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 - name: Setup ccache
if: matrix.use_ccache == 1 if: matrix.use_ccache == 1 && matrix.os == 'macOS'
uses: jianmingyong/ccache-action@v1 run: brew install ccache
with:
install-type: "binary"
ccache-key-prefix: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}
max-size: 500M
gh-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Qt ${{matrix.qt_version}} - name: Restore compiler cache (ccache)
if: matrix.use_ccache == 1
id: ccache_restore
uses: actions/cache/restore@v5
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
path: ${{env.CCACHE_DIR}}
key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}}
restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-
- name: Install aqtinstall
if: matrix.os == 'macOS'
run: pipx install aqtinstall
# Checking if there's a newer, uncached version of Qt available to install via aqtinstall
- name: Resolve latest Qt patch version
if: matrix.os == 'macOS'
id: resolve_qt_version
shell: bash
# Ouputs the version of Qt to install via aqtinstall
run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}"
- name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS'
id: restore_qt
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/Qt
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
# Using jurplel/install-qt-action to install Qt without using brew
# qt build using vcpkg either just fails or takes too long to build
- name: Install fat Qt ${{ steps.resolve_qt_version.outputs.version }} (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v4
with:
cache: false
version: ${{ steps.resolve_qt_version.outputs.version }}
arch: ${{matrix.qt_arch}}
modules: ${{matrix.qt_modules}}
dir: ${{github.workspace}}
- name: Thin Qt libraries (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
run: .ci/thin_macos_qtlib.sh
- name: Cache thin Qt libraries (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ github.workspace }}/Qt
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
- name: Install Qt ${{matrix.qt_version}} (Windows)
if: matrix.os == 'Windows'
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
with: with:
version: ${{matrix.qt_version}} version: ${{matrix.qt_version}}
arch: ${{matrix.qt_arch}} arch: ${{matrix.qt_arch}}
modules: ${{matrix.qt_modules}} modules: ${{matrix.qt_modules}}
cache: ${{matrix.cache_qt}} cache: true
- name: Setup vcpkg cache - name: Setup vcpkg cache
id: vcpkg-cache id: vcpkg-cache
@ -403,6 +449,15 @@ jobs:
TARGET_MACOS_VERSION: ${{ matrix.override_target }} TARGET_MACOS_VERSION: ${{ matrix.override_target }}
run: .ci/compile.sh --server --test --vcpkg 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 - name: Sign app bundle
if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null) if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
env: env:
@ -450,7 +505,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
id: upload_artifact id: upload_artifact
if: matrix.make_package if: matrix.make_package
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: ${{matrix.artifact_name}} name: ${{matrix.artifact_name}}
path: ${{steps.build.outputs.path}} path: ${{steps.build.outputs.path}}
@ -458,7 +513,7 @@ jobs:
- name: Upload pdb database - name: Upload pdb database
if: matrix.os == 'Windows' if: matrix.os == 'Windows'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: Windows${{matrix.target}}-debug-pdbs name: Windows${{matrix.target}}-debug-pdbs
path: | path: |

View file

@ -44,10 +44,10 @@ Latest <kbd>beta</kbd> version:
# Related Repositories # Related Repositories
- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice - [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): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to 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 - [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage
- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package - [io.github.Cockatrice.cockatrice](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration of our Linux `flatpak` package hosted at [Flathub](https://flathub.org/en/apps/io.github.Cockatrice.cockatrice)
# Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA) # Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA)
@ -55,7 +55,6 @@ Latest <kbd>beta</kbd> version:
Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out. Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out.
- [Official Website](https://cockatrice.github.io) - [Official Website](https://cockatrice.github.io)
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) - [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki)
- [Official Code Documentation](https://cockatrice.github.io/docs)
- [Official Discord](https://discord.gg/3Z9yzmA) - [Official Discord](https://discord.gg/3Z9yzmA)
- [reddit r/Cockatrice](https://reddit.com/r/cockatrice) - [reddit r/Cockatrice](https://reddit.com/r/cockatrice)
@ -64,6 +63,23 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
# Contribute # 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 ### Code
@ -79,21 +95,17 @@ We'll happily advice on how best to implement a feature, or we can show you wher
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). 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. 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.
#### Repository Activity ### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/)
![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>
### 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> 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>

View file

@ -59,7 +59,7 @@ if(Qt6_FOUND)
endif() endif()
else() else()
find_package( find_package(
Qt5 5.8.0 Qt5 5.15.2
COMPONENTS ${REQUIRED_QT_COMPONENTS} COMPONENTS ${REQUIRED_QT_COMPONENTS}
QUIET HINTS ${Qt5_DIR} QUIET HINTS ${Qt5_DIR}
) )

View file

@ -144,28 +144,54 @@ set(cockatrice_SOURCES
src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/card_size_widget.cpp
src/interface/widgets/cards/deck_card_zone_display_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/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_analytics_widget.cpp
src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp
src/interface/widgets/deck_analytics/mana_base_widget.cpp src/interface/widgets/deck_analytics/resizable_panel.cpp
src/interface/widgets/deck_analytics/mana_curve_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
src/interface/widgets/deck_analytics/mana_devotion_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp
src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_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_card_info_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_database_display_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_deck_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_filter_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_editor_printing_selector_dock_widget.cpp
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
src/interface/widgets/deck_editor/deck_state_manager.cpp
src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/background_sources.cpp
src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/background_plate_widget.cpp
src/interface/widgets/general/display/banner_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_label.cpp
src/interface/widgets/general/display/dynamic_font_size_push_button.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/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/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_styled_button.cpp
src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/home_widget.cpp
src/interface/widgets/general/layout_containers/flow_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp
@ -287,6 +313,10 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/interface/key_signals.cpp src/interface/key_signals.cpp
src/interface/logger.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) add_subdirectory(sounds)

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,11 @@ searches are case insensitive.
<dt><u>F</u>ormat:</dt> <dt><u>F</u>ormat:</dt>
<dd>[f:standard](#f:standard) <small>(Any deck with format set to standard)</small></dd> <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> <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="#[[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> <dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>

View file

@ -43,23 +43,23 @@ void DeckStatsInterface::queryFinished(QNetworkReply *reply)
deleteLater(); deleteLater();
} }
void DeckStatsInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data) void DeckStatsInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
{ {
DeckList deckWithoutTokens; DeckList deckWithoutTokens;
copyDeckWithoutTokens(*deck, deckWithoutTokens); copyDeckWithoutTokens(deck, deckWithoutTokens);
QUrl params; QUrl params;
QUrlQuery urlQuery; QUrlQuery urlQuery;
urlQuery.addQueryItem("deck", deckWithoutTokens.writeToString_Plain()); urlQuery.addQueryItem("deck", deckWithoutTokens.writeToString_Plain());
urlQuery.addQueryItem("decktitle", deck->getName()); urlQuery.addQueryItem("decktitle", deck.getName());
params.setQuery(urlQuery); params.setQuery(urlQuery);
data->append(params.query(QUrl::EncodeReserved).toUtf8()); data.append(params.query(QUrl::EncodeReserved).toUtf8());
} }
void DeckStatsInterface::analyzeDeck(DeckList *deck) void DeckStatsInterface::analyzeDeck(const DeckList &deck)
{ {
QByteArray data; QByteArray data;
getAnalyzeRequestData(deck, &data); getAnalyzeRequestData(deck, data);
QNetworkRequest request(QUrl("https://deckstats.net/index.php")); QNetworkRequest request(QUrl("https://deckstats.net/index.php"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
@ -68,7 +68,7 @@ void DeckStatsInterface::analyzeDeck(DeckList *deck)
manager->post(request, data); manager->post(request, data);
} }
void DeckStatsInterface::copyDeckWithoutTokens(DeckList &source, DeckList &destination) void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList &destination)
{ {
auto copyIfNotAToken = [this, &destination](const auto node, const auto card) { auto copyIfNotAToken = [this, &destination](const auto node, const auto card) {
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName()); 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 * closest non-token card instead. So we construct a new deck which has no
* tokens. * tokens.
*/ */
void copyDeckWithoutTokens(DeckList &source, DeckList &destination); void copyDeckWithoutTokens(const DeckList &source, DeckList &destination);
private slots: private slots:
void queryFinished(QNetworkReply *reply); void queryFinished(QNetworkReply *reply);
void getAnalyzeRequestData(DeckList *deck, QByteArray *data); void getAnalyzeRequestData(const DeckList &deck, QByteArray &data);
public: public:
explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
void analyzeDeck(DeckList *deck); void analyzeDeck(const DeckList &deck);
}; };
#endif #endif

View file

@ -67,24 +67,24 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
deleteLater(); deleteLater();
} }
void TappedOutInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data) void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
{ {
DeckList mainboard, sideboard; DeckList mainboard, sideboard;
copyDeckSplitMainAndSide(*deck, mainboard, sideboard); copyDeckSplitMainAndSide(deck, mainboard, sideboard);
QUrl params; QUrl params;
QUrlQuery urlQuery; QUrlQuery urlQuery;
urlQuery.addQueryItem("name", deck->getName()); urlQuery.addQueryItem("name", deck.getName());
urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true)); urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true));
urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true)); urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true));
params.setQuery(urlQuery); params.setQuery(urlQuery);
data->append(params.query(QUrl::EncodeReserved).toUtf8()); data.append(params.query(QUrl::EncodeReserved).toUtf8());
} }
void TappedOutInterface::analyzeDeck(DeckList *deck) void TappedOutInterface::analyzeDeck(const DeckList &deck)
{ {
QByteArray data; QByteArray data;
getAnalyzeRequestData(deck, &data); getAnalyzeRequestData(deck, data);
QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/")); QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
@ -93,7 +93,7 @@ void TappedOutInterface::analyzeDeck(DeckList *deck)
manager->post(request, data); manager->post(request, data);
} }
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard) void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
{ {
auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) { auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName()); CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName());

View file

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

View file

@ -2,6 +2,7 @@
#include "../network/update/client/release_channel.h" #include "../network/update/client/release_channel.h"
#include "card_counter_settings.h" #include "card_counter_settings.h"
#include "version_string.h"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QApplication> #include <QApplication>
@ -198,7 +199,13 @@ SettingsCache::SettingsCache()
mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool(); mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool();
checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool(); if (settings->contains("personal/startupUpdateCheck")) {
checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
} else if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
checkUpdatesOnStartup = false; // do not run auto updater on custom version
} else {
checkUpdatesOnStartup = true; // default to run auto updater
}
startupCardUpdateCheckPromptForUpdate = startupCardUpdateCheckPromptForUpdate =
settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool(); settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool();
startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool(); startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool();
@ -206,7 +213,15 @@ SettingsCache::SettingsCache()
lastCardUpdateCheck = settings->value("personal/lastCardUpdateCheck", QDateTime::currentDateTime().date()).toDate(); lastCardUpdateCheck = settings->value("personal/lastCardUpdateCheck", QDateTime::currentDateTime().date()).toDate();
notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool(); notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool();
notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool(); notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool();
updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt();
if (settings->contains("personal/updatereleasechannel")) {
updateReleaseChannel = settings->value("personal/updatereleasechannel").toInt();
} else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) {
// default to beta if this is a beta release
updateReleaseChannel = 1;
} else {
updateReleaseChannel = 0; // stable
}
lang = settings->value("personal/lang").toString(); lang = settings->value("personal/lang").toString();
keepalive = settings->value("personal/keepalive", 3).toInt(); keepalive = settings->value("personal/keepalive", 3).toInt();
@ -273,6 +288,7 @@ SettingsCache::SettingsCache()
focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool();
showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool();
showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool();
displayCardNames = settings->value("cards/displaycardnames", true).toBool(); displayCardNames = settings->value("cards/displaycardnames", true).toBool();
roundCardCorners = settings->value("cards/roundcardcorners", true).toBool(); roundCardCorners = settings->value("cards/roundcardcorners", true).toBool();
overrideAllCardArtWithPersonalPreference = overrideAllCardArtWithPersonalPreference =
@ -700,6 +716,13 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts)
settings->setValue("menu/showshortcuts", 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) void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames)
{ {
displayCardNames = static_cast<bool>(_displayCardNames); displayCardNames = static_cast<bool>(_displayCardNames);

View file

@ -145,6 +145,7 @@ signals:
void homeTabBackgroundShuffleFrequencyChanged(); void homeTabBackgroundShuffleFrequencyChanged();
void picDownloadChanged(); void picDownloadChanged();
void showStatusBarChanged(bool state); void showStatusBarChanged(bool state);
void showGameSelectorFilterToolbarChanged(bool state);
void displayCardNamesChanged(); void displayCardNamesChanged();
void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference); void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference);
void bumpSetsWithCardsInDeckToTopChanged(); void bumpSetsWithCardsInDeckToTopChanged();
@ -236,6 +237,7 @@ private:
bool annotateTokens; bool annotateTokens;
QByteArray tabGameSplitterSizes; QByteArray tabGameSplitterSizes;
bool showShortcuts; bool showShortcuts;
bool showGameSelectorFilterToolbar;
bool displayCardNames; bool displayCardNames;
bool overrideAllCardArtWithPersonalPreference; bool overrideAllCardArtWithPersonalPreference;
bool bumpSetsWithCardsInDeckToTop; bool bumpSetsWithCardsInDeckToTop;
@ -553,6 +555,10 @@ public:
{ {
return showShortcuts; return showShortcuts;
} }
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const
{
return showGameSelectorFilterToolbar;
}
[[nodiscard]] bool getDisplayCardNames() const [[nodiscard]] bool getDisplayCardNames() const
{ {
return displayCardNames; return displayCardNames;
@ -1017,6 +1023,7 @@ public slots:
void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens); void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens);
void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes); void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes);
void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts); void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts);
void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar);
void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames); void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames);
void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt); void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt);
void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop); void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop);

View file

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

View file

@ -660,6 +660,12 @@ private:
{"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"), {"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"),
parseSequenceString("Ctrl+M"), parseSequenceString("Ctrl+M"),
ShortcutGroup::Drawing)}, 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"), {"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"),
parseSequenceString("Ctrl+D"), parseSequenceString("Ctrl+D"),
ShortcutGroup::Drawing)}, ShortcutGroup::Drawing)},

View file

@ -13,7 +13,7 @@ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / GenericQuery QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / CommentQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
@ -25,6 +25,7 @@ DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String
PathQuery <- [Pp] 'ath'? [:] String PathQuery <- [Pp] 'ath'? [:] String
FormatQuery <- [Ff] 'ormat'? [:] String FormatQuery <- [Ff] 'ormat'? [:] String
CommentQuery <- [Cc] ('omment' 's'?)? [:] String
GenericQuery <- String GenericQuery <- String
@ -166,6 +167,14 @@ static void setupParserRules()
}; };
}; };
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 { search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]); auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {

View file

@ -117,11 +117,7 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
chooseTokenFromDeckRadioButton->setDisabled(true); // No tokens in deck = no need for option chooseTokenFromDeckRadioButton->setDisabled(true); // No tokens in deck = no need for option
} else { } else {
chooseTokenFromDeckRadioButton->setChecked(true); chooseTokenFromDeckRadioButton->setChecked(true);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>(predefinedTokens.begin(), predefinedTokens.end())); cardDatabaseDisplayModel->setCardNameSet(QSet<QString>(predefinedTokens.begin(), predefinedTokens.end()));
#else
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>::fromList(predefinedTokens));
#endif
} }
auto *tokenChooseLayout = new QVBoxLayout; auto *tokenChooseLayout = new QVBoxLayout;
@ -223,11 +219,7 @@ void DlgCreateToken::actChooseTokenFromAll(bool checked)
void DlgCreateToken::actChooseTokenFromDeck(bool checked) void DlgCreateToken::actChooseTokenFromDeck(bool checked)
{ {
if (checked) { if (checked) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>(predefinedTokens.begin(), predefinedTokens.end())); cardDatabaseDisplayModel->setCardNameSet(QSet<QString>(predefinedTokens.begin(), predefinedTokens.end()));
#else
cardDatabaseDisplayModel->setCardNameSet(QSet<QString>::fromList(predefinedTokens));
#endif
} }
} }

View file

@ -60,6 +60,16 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T
connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan); connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan);
addAction(aMulligan); 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(); addSeparator();
mMoveHandMenu = addTearOffMenu(QString()); mMoveHandMenu = addTearOffMenu(QString());
@ -104,7 +114,9 @@ void HandMenu::retranslateUi()
aSortHandByType->setText(tr("Type")); aSortHandByType->setText(tr("Type"));
aSortHandByManaValue->setText(tr("Mana Value")); aSortHandByManaValue->setText(tr("Mana Value"));
aMulligan->setText(tr("Take &mulligan")); aMulligan->setText(tr("Take &mulligan (Choose hand size)"));
aMulliganSame->setText(tr("Take mulligan (Same hand size)"));
aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)"));
mMoveHandMenu->setTitle(tr("&Move hand to...")); mMoveHandMenu->setTitle(tr("&Move hand to..."));
aMoveHandToTopLibrary->setText(tr("&Top of library")); aMoveHandToTopLibrary->setText(tr("&Top of library"));
@ -128,6 +140,8 @@ void HandMenu::setShortcutsActive()
aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType")); aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType"));
aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue")); aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue"));
aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan")); aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan"));
aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame"));
aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne"));
aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll")); aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll"));
aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll")); aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll"));
} }

View file

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

View file

@ -310,28 +310,48 @@ void PlayerActions::actMulligan()
{ {
int startSize = SettingsCache::instance().getStartingHandSize(); int startSize = SettingsCache::instance().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size(); int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck int deckSize = player->getDeckZone()->getCards().size() + handSize;
bool ok; bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"), int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"),
tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + tr("Number of cards: (max. %1)").arg(deckSize) + '\n' +
tr("0 and lower are in comparison to current hand size"), tr("0 and lower are in comparison to current hand size"),
startSize, -handSize, deckSize, 1, &ok); startSize, -handSize, deckSize, 1, &ok);
if (!ok) { if (!ok) {
return; return;
} }
Command_Mulligan cmd;
if (number < 1) { if (number < 1) {
if (handSize == 0) { number = handSize + number;
return;
}
cmd.set_number(handSize + number);
} else {
cmd.set_number(number);
} }
doMulligan(number);
SettingsCache::instance().setStartingHandSize(number);
}
void PlayerActions::actMulliganSameSize()
{
int handSize = player->getHandZone()->getCards().size();
doMulligan(handSize);
}
void PlayerActions::actMulliganMinusOne()
{
int handSize = player->getHandZone()->getCards().size();
int targetSize = qMax(1, handSize - 1);
doMulligan(targetSize);
}
void PlayerActions::doMulligan(int number)
{
if (number < 1) {
return;
}
Command_Mulligan cmd;
cmd.set_number(number);
sendGameCommand(cmd); sendGameCommand(cmd);
if (startSize != number) {
SettingsCache::instance().setStartingHandSize(number);
}
} }
void PlayerActions::actDrawCards() void PlayerActions::actDrawCards()

View file

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

View file

@ -78,14 +78,7 @@ void PlayerEventHandler::eventShuffle(const Event_Shuffle &event)
void PlayerEventHandler::eventRollDie(const Event_RollDie &event) void PlayerEventHandler::eventRollDie(const Event_RollDie &event)
{ {
if (!event.values().empty()) { if (!event.values().empty()) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QList<uint> rolls(event.values().begin(), event.values().end()); 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()); std::sort(rolls.begin(), rolls.end());
emit logRollDie(player, static_cast<int>(event.sides()), rolls); emit logRollDie(player, static_cast<int>(event.sides()), rolls);
} else if (event.value()) { } else if (event.value()) {

View file

@ -13,12 +13,20 @@
#include <QGraphicsLinearLayout> #include <QGraphicsLinearLayout>
#include <QGraphicsProxyWidget> #include <QGraphicsProxyWidget>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QLabel> #include <QLabel>
#include <QPainter> #include <QPainter>
#include <QScrollBar> #include <QScrollBar>
#include <QStyle>
#include <QStyleOption> #include <QStyleOption>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #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 _player the player the cards were revealed to.
* @param _origZone the zone the cards were revealed from. * @param _origZone the zone the cards were revealed from.
@ -241,33 +249,191 @@ void ZoneViewWidget::retranslateUi()
pileViewCheckBox.setText(tr("pile view")); pileViewCheckBox.setText(tr("pile view"));
} }
void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */) void ZoneViewWidget::stopWindowDrag()
{ {
if (!scene()) if (!draggingWindow)
return; return;
int titleBarHeight = 24; draggingWindow = false;
ungrabMouse();
}
QPointF scenePos = pos(); void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event)
{
draggingWindow = true;
dragStartItemPos = pos();
dragStartScreenPos = event->screenPos();
dragView = findDragView(event->widget());
if (scenePos.x() < 0) { // need to grab mouse to receive events and not miss initial movement
scenePos.setX(0); grabMouse();
} else { }
qreal maxw = scene()->sceneRect().width() - 100;
if (scenePos.x() > maxw) QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
scenePos.setX(maxw); {
const QRectF frameRectF = windowFrameRect();
// query the style for the close button position (handles macOS top-left placement)
// Title bar rect MUST be local (0,0-based) for QStyle
const QRect titleBarRect(0, 0, static_cast<int>(frameRectF.width()), static_cast<int>(kTitleBarHeight));
if (styleWidget) {
QStyleOptionTitleBar opt;
opt.initFrom(styleWidget);
opt.rect = titleBarRect;
opt.text = windowTitle();
opt.icon = styleWidget->windowIcon();
opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;
opt.subControls = QStyle::SC_TitleBarCloseButton;
opt.activeSubControls = QStyle::SC_TitleBarCloseButton;
opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState;
if (styleWidget->isActiveWindow()) {
opt.state |= QStyle::State_Active;
}
QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton,
styleWidget);
if (r.isValid() && !r.isEmpty()) {
// Translate from local-titlebar coords → frame coords
r.translate(frameRectF.topLeft().toPoint());
return QRectF(r);
}
} }
if (scenePos.y() < titleBarHeight) { // Fallback: frame-relative top-right
scenePos.setY(titleBarHeight); return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight);
} else { }
qreal maxh = scene()->sceneRect().height() - titleBarHeight;
if (scenePos.y() > maxh) QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const
scenePos.setY(maxh); {
QWidget *current = eventWidget;
while (current) {
if (auto *view = qobject_cast<QGraphicsView *>(current))
return view;
current = current->parentWidget();
} }
if (scenePos != pos()) if (scene() && !scene()->views().isEmpty())
setPos(scenePos); return scene()->views().constFirst();
return nullptr;
}
QPointF ZoneViewWidget::calcDraggedWindowPos(const QPoint &screenPos,
const QPointF &scenePos,
const QPointF &buttonDownScenePos) const
{
if (dragView && dragView->viewport()) {
const QPoint vpStart = dragView->viewport()->mapFromGlobal(dragStartScreenPos);
const QPoint vpNow = dragView->viewport()->mapFromGlobal(screenPos);
const QPointF sceneStart = dragView->mapToScene(vpStart);
const QPointF sceneNow = dragView->mapToScene(vpNow);
return dragStartItemPos + (sceneNow - sceneStart);
}
return dragStartItemPos + (scenePos - buttonDownScenePos);
}
bool ZoneViewWidget::windowFrameEvent(QEvent *event)
{
if (event->type() == QEvent::UngrabMouse) {
stopWindowDrag();
return QGraphicsWidget::windowFrameEvent(event);
}
auto *me = dynamic_cast<QGraphicsSceneMouseEvent *>(event);
if (!me)
return QGraphicsWidget::windowFrameEvent(event);
switch (event->type()) {
case QEvent::GraphicsSceneMousePress:
if (me->button() == Qt::LeftButton && windowFrameSectionAt(me->pos()) == Qt::TitleBarArea) {
// avoid drag on close button
if (closeButtonRect(me->widget()).contains(me->pos())) {
me->accept();
close();
return true;
}
startWindowDrag(me);
me->accept();
return true;
}
break;
case QEvent::GraphicsSceneMouseMove:
if (draggingWindow) {
if (!(me->buttons() & Qt::LeftButton)) {
stopWindowDrag();
} else {
setPos(
calcDraggedWindowPos(me->screenPos(), me->scenePos(), me->buttonDownScenePos(Qt::LeftButton)));
}
me->accept();
return true;
}
break;
case QEvent::GraphicsSceneMouseRelease:
if (draggingWindow && me->button() == Qt::LeftButton) {
stopWindowDrag();
me->accept();
return true;
}
break;
default:
break;
}
return QGraphicsWidget::windowFrameEvent(event);
}
void ZoneViewWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
// move if the scene routes moves while dragging
if (draggingWindow && (event->buttons() & Qt::LeftButton)) {
setPos(calcDraggedWindowPos(event->screenPos(), event->scenePos(), event->buttonDownScenePos(Qt::LeftButton)));
event->accept();
return;
}
QGraphicsWidget::mouseMoveEvent(event);
}
void ZoneViewWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (draggingWindow && event->button() == Qt::LeftButton) {
stopWindowDrag();
event->accept();
return;
}
QGraphicsWidget::mouseReleaseEvent(event);
}
QVariant ZoneViewWidget::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemPositionChange && scene()) {
// Keep grab area in main view
const QRectF sceneRect = scene()->sceneRect();
const QPointF requestedPos = value.toPointF();
QPointF desiredPos = requestedPos;
const qreal minX = sceneRect.left();
const qreal maxX = qMax(minX, sceneRect.right() - kMinVisibleWidth);
const qreal minY = sceneRect.top() + kTitleBarHeight;
const qreal maxY = qMax(minY, sceneRect.bottom() - kTitleBarHeight);
desiredPos.setX(qBound(minX, desiredPos.x(), maxX));
desiredPos.setY(qBound(minY, desiredPos.y(), maxY));
return desiredPos;
}
return QGraphicsWidget::itemChange(change, value);
} }
void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event) void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event)
@ -350,6 +516,7 @@ void ZoneViewWidget::handleScrollBarChange(int value)
void ZoneViewWidget::closeEvent(QCloseEvent *event) void ZoneViewWidget::closeEvent(QCloseEvent *event)
{ {
stopWindowDrag();
disconnect(zone, &ZoneViewZone::closed, this, 0); disconnect(zone, &ZoneViewZone::closed, this, 0);
// manually call zone->close in order to remove it from the origZones views // manually call zone->close in order to remove it from the origZones views
zone->close(); zone->close();

View file

@ -3,7 +3,6 @@
* @ingroup GameGraphicsZones * @ingroup GameGraphicsZones
* @brief TODO: Document this. * @brief TODO: Document this.
*/ */
#ifndef ZONEVIEWWIDGET_H #ifndef ZONEVIEWWIDGET_H
#define ZONEVIEWWIDGET_H #define ZONEVIEWWIDGET_H
@ -14,6 +13,7 @@
#include <QGraphicsProxyWidget> #include <QGraphicsProxyWidget>
#include <QGraphicsWidget> #include <QGraphicsWidget>
#include <QLineEdit> #include <QLineEdit>
#include <QPointer>
#include <libcockatrice/utility/macros.h> #include <libcockatrice/utility/macros.h>
class QLabel; class QLabel;
@ -28,6 +28,8 @@ class ServerInfo_Card;
class QGraphicsSceneMouseEvent; class QGraphicsSceneMouseEvent;
class QGraphicsSceneWheelEvent; class QGraphicsSceneWheelEvent;
class QStyleOption; class QStyleOption;
class QGraphicsView;
class QWidget;
class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget
{ {
@ -52,7 +54,6 @@ private:
ZoneViewZone *zone; ZoneViewZone *zone;
QGraphicsWidget *zoneContainer; QGraphicsWidget *zoneContainer;
QPushButton *closeButton;
QScrollBar *scrollBar; QScrollBar *scrollBar;
ScrollableGraphicsProxyWidget *scrollBarProxy; ScrollableGraphicsProxyWidget *scrollBarProxy;
@ -66,6 +67,33 @@ private:
int extraHeight; int extraHeight;
Player *player; 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); void resizeScrollbar(qreal newZoneHeight);
signals: signals:
void closePressed(ZoneViewWidget *zv); void closePressed(ZoneViewWidget *zv);
@ -76,7 +104,6 @@ private slots:
void resizeToZoneContents(bool forceInitialHeight = false); void resizeToZoneContents(bool forceInitialHeight = false);
void handleScrollBarChange(int value); void handleScrollBarChange(int value);
void zoneDeleted(); void zoneDeleted();
void moveEvent(QGraphicsSceneMoveEvent * /* event */) override;
void resizeEvent(QGraphicsSceneResizeEvent * /* event */) override; void resizeEvent(QGraphicsSceneResizeEvent * /* event */) override;
void expandWindow(); void expandWindow();
@ -101,6 +128,10 @@ public:
protected: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
void initStyleOption(QStyleOption *option) const 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; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
}; };

View file

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

View file

@ -21,9 +21,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
// We need a timeout to ensure requests don't hang indefinitely in case of // 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 // cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22) // Use Qt's default timeout (30s, as of 2023-02-22)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout(); networkManager->setTransferTimeout();
#endif
cache = new QNetworkDiskCache(this); cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath()); cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L * cache->setMaximumCacheSize(1024L * 1024L *

View file

@ -287,14 +287,14 @@ static QString toDecklistExportString(const QList<const DecklistCardNode *> &car
* @param deckList The decklist to export * @param deckList The decklist to export
* @param website The website we're sending the deck to * @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 // Add the base url
QString deckString = "https://" + getDomainForWebsite(website) + "/?"; QString deckString = "https://" + getDomainForWebsite(website) + "/?";
// export all cards in zone // export all cards in zone
QString mainBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_MAIN})); QString mainBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_MAIN}));
QString sideBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_SIDE})); QString sideBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_SIDE}));
// Remove the extra return at the end of the last cards // Remove the extra return at the end of the last cards
mainBoardCards.chop(3); mainBoardCards.chop(3);
@ -310,7 +310,7 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi
return deckString; return deckString;
} }
void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, bool addSetNameAndNumber) void DeckLoader::saveToClipboard(const DeckList &deckList, bool addComments, bool addSetNameAndNumber)
{ {
QString buffer; QString buffer;
QTextStream stream(&buffer); QTextStream stream(&buffer);
@ -320,7 +320,7 @@ void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, boo
} }
bool DeckLoader::saveToStream_Plain(QTextStream &out, bool DeckLoader::saveToStream_Plain(QTextStream &out,
const DeckList *deckList, const DeckList &deckList,
bool addComments, bool addComments,
bool addSetNameAndNumber) bool addSetNameAndNumber)
{ {
@ -329,7 +329,7 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
} }
// loop zones // loop zones
for (auto zoneNode : deckList->getZoneNodes()) { for (auto zoneNode : deckList.getZoneNodes()) {
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber); saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
// end of zone // end of zone
@ -339,14 +339,14 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
return true; return true;
} }
void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList) void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList)
{ {
if (!deckList->getName().isEmpty()) { if (!deckList.getName().isEmpty()) {
out << "// " << deckList->getName() << "\n\n"; out << "// " << deckList.getName() << "\n\n";
} }
if (!deckList->getComments().isEmpty()) { if (!deckList.getComments().isEmpty()) {
QStringList commentRows = deckList->getComments().split(QRegularExpression("\n|\r\n|\r")); QStringList commentRows = deckList.getComments().split(QRegularExpression("\n|\r\n|\r"));
for (const QString &row : commentRows) { for (const QString &row : commentRows) {
out << "// " << row << "\n"; out << "// " << row << "\n";
} }
@ -434,7 +434,7 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
} }
} }
bool DeckLoader::convertToCockatriceFormat(QString fileName) bool DeckLoader::convertToCockatriceFormat(const QString &fileName)
{ {
// Change the file extension to .cod // Change the file extension to .cod
QFileInfo fileInfo(fileName); QFileInfo fileInfo(fileName);
@ -543,7 +543,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
cursor->movePosition(QTextCursor::End); cursor->movePosition(QTextCursor::End);
} }
void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList) void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList)
{ {
QTextDocument doc; QTextDocument doc;
@ -559,14 +559,14 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList)
headerCharFormat.setFontWeight(QFont::Bold); headerCharFormat.setFontWeight(QFont::Bold);
cursor.insertBlock(headerBlockFormat, headerCharFormat); cursor.insertBlock(headerBlockFormat, headerCharFormat);
cursor.insertText(deckList->getName()); cursor.insertText(deckList.getName());
headerCharFormat.setFontPointSize(12); headerCharFormat.setFontPointSize(12);
cursor.insertBlock(headerBlockFormat, headerCharFormat); cursor.insertBlock(headerBlockFormat, headerCharFormat);
cursor.insertText(deckList->getComments()); cursor.insertText(deckList.getComments());
cursor.insertBlock(headerBlockFormat, headerCharFormat); cursor.insertBlock(headerBlockFormat, headerCharFormat);
for (auto zoneNode : deckList->getZoneNodes()) { for (auto zoneNode : deckList.getZoneNodes()) {
cursor.insertHtml("<br><img src=theme:hr.jpg>"); cursor.insertHtml("<br><img src=theme:hr.jpg>");
cursor.insertBlock(headerBlockFormat, headerCharFormat); cursor.insertBlock(headerBlockFormat, headerCharFormat);

View file

@ -59,11 +59,11 @@ public:
bool saveToFile(const QString &fileName, DeckFileFormat::Format fmt); bool saveToFile(const QString &fileName, DeckFileFormat::Format fmt);
bool updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt); bool updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt);
static QString exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website); static QString exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website);
static void 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, static bool saveToStream_Plain(QTextStream &out,
const DeckList *deckList, const DeckList &deckList,
bool addComments = true, bool addComments = true,
bool addSetNameAndNumber = true); bool addSetNameAndNumber = true);
@ -72,9 +72,9 @@ public:
* @param printer The printer to render the decklist to. * @param printer The printer to render the decklist to.
* @param deckList * @param deckList
*/ */
static void printDeckList(QPrinter *printer, const DeckList *deckList); static void printDeckList(QPrinter *printer, const DeckList &deckList);
bool convertToCockatriceFormat(QString fileName); bool convertToCockatriceFormat(const QString &fileName);
LoadedDeck &getDeck() LoadedDeck &getDeck()
{ {
@ -91,7 +91,7 @@ public:
private: private:
static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node); static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node);
static void saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList); static void saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList);
static void saveToStream_DeckZone(QTextStream &out, static void saveToStream_DeckZone(QTextStream &out,
const InnerDecklistNode *zoneNode, const InnerDecklistNode *zoneNode,

View file

@ -57,17 +57,10 @@ void Logger::openLogfileSession()
return; return;
} }
fileStream.setDevice(&fileHandle); fileStream.setDevice(&fileHandle);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
fileStream << "Log session started at " << QDateTime::currentDateTime().toString() << Qt::endl; fileStream << "Log session started at " << QDateTime::currentDateTime().toString() << Qt::endl;
fileStream << getClientVersion() << Qt::endl; fileStream << getClientVersion() << Qt::endl;
fileStream << getSystemArchitecture() << Qt::endl; fileStream << getSystemArchitecture() << Qt::endl;
fileStream << getClientInstallInfo() << 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; logToFileEnabled = true;
} }
@ -77,11 +70,7 @@ void Logger::closeLogfileSession()
return; return;
logToFileEnabled = false; logToFileEnabled = false;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << Qt::endl; fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << Qt::endl;
#else
fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << endl;
#endif
fileHandle.close(); fileHandle.close();
} }
@ -103,11 +92,7 @@ void Logger::internalLog(const QString &message)
std::cerr << message.toStdString() << std::endl; // Print to stdout std::cerr << message.toStdString() << std::endl; // Print to stdout
if (logToFileEnabled) { if (logToFileEnabled) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
fileStream << message << Qt::endl; // Print to fileStream fileStream << message << Qt::endl; // Print to fileStream
#else
fileStream << message << endl; // Print to fileStream
#endif
} }
} }

View file

@ -91,8 +91,9 @@ QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex i
if (indexToWidgetMap.contains(index)) { if (indexToWidgetMap.contains(index)) {
return indexToWidgetMap[index]; return indexToWidgetMap[index];
} }
auto cardName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString(); auto cardName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
auto cardProviderId = deckListModel->data(index.sibling(index.row(), 4), Qt::EditRole).toString(); auto cardProviderId =
index.sibling(index.row(), DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::EditRole).toString();
auto widget = new CardInfoPictureWithTextOverlayWidget(getLayoutParent(), true); auto widget = new CardInfoPictureWithTextOverlayWidget(getLayoutParent(), true);
widget->setScaleFactor(cardSizeWidget->getSlider()->value()); widget->setScaleFactor(cardSizeWidget->getSlider()->value());
@ -114,7 +115,7 @@ void CardGroupDisplayWidget::updateCardDisplays()
// This doesn't really matter since overwrite the whole lessThan function to just compare dynamically anyway. // This doesn't really matter since overwrite the whole lessThan function to just compare dynamically anyway.
proxy.setSortRole(Qt::EditRole); proxy.setSortRole(Qt::EditRole);
proxy.sort(1, Qt::AscendingOrder); proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
// 1. trackedIndex is a source index → map it to proxy space // 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex); QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);

View file

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

View file

@ -47,7 +47,7 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
originalPos = this->pos(); originalPos = this->pos();
// Create the animation // Create the animation
animation = new QPropertyAnimation(this, "pos"); animation = new QPropertyAnimation(this, "pos", this);
animation->setDuration(200); // 200ms animation duration animation->setDuration(200); // 200ms animation duration
animation->setEasingCurve(QEasingCurve::OutQuad); animation->setEasingCurve(QEasingCurve::OutQuad);

View file

@ -2,6 +2,7 @@
#include "card_group_display_widgets/flat_card_group_display_widget.h" #include "card_group_display_widgets/flat_card_group_display_widget.h"
#include "card_group_display_widgets/overlapped_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 <QResizeEvent>
#include <libcockatrice/models/deck_list/deck_list_model.h> #include <libcockatrice/models/deck_list/deck_list_model.h>
@ -82,10 +83,11 @@ void DeckCardZoneDisplayWidget::cleanupInvalidCardGroup(CardGroupDisplayWidget *
void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex index) void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex index)
{ {
auto categoryName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString();
if (indexToWidgetMap.contains(index)) { if (indexToWidgetMap.contains(index)) {
return; return;
} }
auto categoryName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
if (displayType == DisplayType::Overlap) { if (displayType == DisplayType::Overlap) {
auto *displayWidget = new OverlappedCardGroupDisplayWidget( auto *displayWidget = new OverlappedCardGroupDisplayWidget(
cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria, cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria,
@ -120,7 +122,7 @@ void DeckCardZoneDisplayWidget::displayCards()
QSortFilterProxyModel proxy; QSortFilterProxyModel proxy;
proxy.setSourceModel(deckListModel); proxy.setSourceModel(deckListModel);
proxy.setSortRole(Qt::EditRole); proxy.setSortRole(Qt::EditRole);
proxy.sort(1, Qt::AscendingOrder); proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder);
// 1. trackedIndex is a source index → map it to proxy space // 1. trackedIndex is a source index → map it to proxy space
QModelIndex proxyParent = proxy.mapFromSource(trackedIndex); QModelIndex proxyParent = proxy.mapFromSource(trackedIndex);
@ -229,10 +231,13 @@ QList<QString> DeckCardZoneDisplayWidget::getGroupCriteriaValueList()
{ {
QList<QString> groupCriteriaValues; QList<QString> groupCriteriaValues;
QList<ExactCard> cardsInZone = deckListModel->getCardsForZone(zoneName); QList<const DecklistCardNode *> nodes = deckListModel->getCardNodesForZone(zoneName);
for (const ExactCard &cardInZone : cardsInZone) { for (auto node : nodes) {
groupCriteriaValues.append(cardInZone.getInfo().getProperty(activeGroupCriteria)); CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());
if (info) {
groupCriteriaValues.append(info->getProperty(activeGroupCriteria));
}
} }
groupCriteriaValues.removeDuplicates(); groupCriteriaValues.removeDuplicates();

View file

@ -0,0 +1,49 @@
#include "abstract_analytics_panel_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QPushButton>
AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: QWidget(parent), analyzer(analyzer)
{
layout = new QVBoxLayout(this);
bannerAndSettingsContainer = new QWidget(this);
bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer);
bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout);
bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
bannerAndSettingsLayout->addWidget(bannerWidget, 1);
// config button
configureButton = new QPushButton(this);
configureButton->setIcon(QPixmap("theme:icons/cogwheel"));
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
bannerAndSettingsLayout->addWidget(configureButton, 0);
layout->addWidget(bannerAndSettingsContainer);
connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay);
}
bool AbstractAnalyticsPanelWidget::applyConfigFromDialog()
{
QDialog *dlg = createConfigDialog(this);
if (!dlg) {
return false;
}
bool ok = dlg->exec() == QDialog::Accepted;
if (ok) {
// dialog must expose its final config as JSON
auto newCfg = extractConfigFromDialog(dlg);
loadConfig(newCfg);
updateDisplay();
}
dlg->deleteLater();
return ok;
}

View file

@ -0,0 +1,61 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
#include "../general/display/banner_widget.h"
#include <QDialog>
#include <QJsonObject>
#include <QVBoxLayout>
#include <QWidget>
class DeckListStatisticsAnalyzer;
class AbstractAnalyticsPanelWidget : public QWidget
{
Q_OBJECT
public slots:
virtual void updateDisplay() = 0;
// Widgets must return a config dialog
virtual QDialog *createConfigDialog(QWidget *parent) = 0;
public:
explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void setDisplayTitle(const QString &title)
{
displayTitle = title;
if (bannerWidget) {
bannerWidget->setText(displayTitle);
}
}
QString displayTitleText() const
{
return displayTitle;
}
virtual QJsonObject saveConfig() const
{
return {};
}
virtual void loadConfig(const QJsonObject &)
{
}
// Unified helper to run config dialog and update widget
bool applyConfigFromDialog();
// Dialog → JSON must be supplied by each subclass
virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0;
protected:
DeckListStatisticsAnalyzer *analyzer;
QVBoxLayout *layout;
QWidget *bannerAndSettingsContainer;
QHBoxLayout *bannerAndSettingsLayout;
QString displayTitle;
BannerWidget *bannerWidget;
QPushButton *configureButton;
};
#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H

View file

@ -0,0 +1,32 @@
#include "add_analytics_panel_dialog.h"
#include "analytics_panel_widget_factory.h"
#include <QDialogButtonBox>
#include <QVBoxLayout>
AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Add Analytics Panel"));
layout = new QVBoxLayout(this);
typeCombo = new QComboBox(this);
// Populate using descriptors
const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets();
for (const auto &desc : widgets) {
// Show translated title to user
typeCombo->addItem(desc.title, desc.type);
}
layout->addWidget(typeCombo);
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
}

View file

@ -0,0 +1,29 @@
#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
#include "analytics_panel_widget_factory.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QVBoxLayout>
class AddAnalyticsPanelDialog : public QDialog
{
Q_OBJECT
public:
explicit AddAnalyticsPanelDialog(QWidget *parent);
QString selectedType() const
{
return typeCombo->currentData().toString();
}
private:
QVBoxLayout *layout;
QComboBox *typeCombo;
QDialogButtonBox *buttons;
};
#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H

View file

@ -0,0 +1,33 @@
#include "analytics_panel_widget_factory.h"
#include "abstract_analytics_panel_widget.h"
AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance()
{
static AnalyticsPanelWidgetFactory f;
return f;
}
void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc)
{
widgets.insert(desc.type, desc);
}
AbstractAnalyticsPanelWidget *
AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const
{
auto it = widgets.find(type);
if (it == widgets.end())
return nullptr;
auto w = it->creator(parent, analyzer);
w->setDisplayTitle(it->title);
return w;
}
QList<AnalyticsPanelWidgetFactory::Descriptor> AnalyticsPanelWidgetFactory::availableWidgets() const
{
return widgets.values();
}

View file

@ -0,0 +1,44 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
#include <QMap>
#include <QString>
#include <QStringList>
#include <QWidget>
#include <functional>
class AbstractAnalyticsPanelWidget;
class DeckListStatisticsAnalyzer;
class AnalyticsPanelWidgetFactory
{
public:
using Creator = std::function<AbstractAnalyticsPanelWidget *(QWidget *, DeckListStatisticsAnalyzer *)>;
struct Descriptor
{
QString type; // stable ID ("manaProdDevotion")
QString title; // translated, user-facing
Creator creator;
};
static AnalyticsPanelWidgetFactory &instance();
// NEW: richer registration
void registerWidget(const Descriptor &desc);
AbstractAnalyticsPanelWidget *
create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const;
// NEW: expose widgets to UI
QList<Descriptor> availableWidgets() const;
private:
AnalyticsPanelWidgetFactory() = default; // Ensure private constructor
AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete;
AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete;
QMap<QString, Descriptor> widgets;
};
#endif

View file

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

View file

@ -0,0 +1,17 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
#include "analytics_panel_widget_factory.h"
class AnalyticsPanelWidgetRegistrar
{
public:
AnalyticsPanelWidgetRegistrar(const QString &type,
const QString &title,
AnalyticsPanelWidgetFactory::Creator creator)
{
AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator});
}
};
#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H

View file

@ -0,0 +1,28 @@
#include "draw_probability_config.h"
QJsonObject DrawProbabilityConfig::toJson() const
{
QJsonObject o;
o["criteria"] = criteria;
o["atLeast"] = atLeast;
o["quantity"] = quantity;
o["drawn"] = drawn;
return o;
}
DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o)
{
DrawProbabilityConfig cfg;
if (o.contains("criteria")) {
cfg.criteria = o["criteria"].toString();
}
if (o.contains("atLeast")) {
cfg.atLeast = o["atLeast"].toBool(true);
}
if (o.contains("quantity")) {
cfg.quantity = o["quantity"].toInt(1);
}
if (o.contains("drawn")) {
cfg.drawn = o["drawn"].toInt(7);
}
return cfg;
}

View file

@ -0,0 +1,19 @@
#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H
#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H
#include <QJsonObject>
#include <QString>
struct DrawProbabilityConfig
{
QString criteria = "name"; // name, type, subtype, cmc
bool atLeast = true; // true = at least, false = exactly
int quantity = 1; // N
int drawn = 7; // M
QJsonObject toJson() const;
static DrawProbabilityConfig fromJson(const QJsonObject &o);
};
#endif

View file

@ -0,0 +1,92 @@
#include "draw_probability_config_dialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QSpinBox>
DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent)
{
form = new QFormLayout(this);
// Criteria
labelCriteria = new QLabel(this);
criteria = new QComboBox(this);
criteria->addItem(QString(), "name");
criteria->addItem(QString(), "type");
criteria->addItem(QString(), "subtype");
criteria->addItem(QString(), "cmc");
form->addRow(labelCriteria, criteria);
// Exactness
labelExactness = new QLabel(this);
exactness = new QComboBox(this);
exactness->addItem(QString(), true);
exactness->addItem(QString(), false);
form->addRow(labelExactness, exactness);
// Quantity
labelQuantity = new QLabel(this);
quantity = new QSpinBox(this);
quantity->setRange(1, 60);
form->addRow(labelQuantity, quantity);
// Drawn
labelDrawn = new QLabel(this);
drawn = new QSpinBox(this);
drawn->setRange(1, 60);
drawn->setValue(7);
form->addRow(labelDrawn, drawn);
// Button box
auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
form->addWidget(bb);
connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept);
connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject);
retranslateUi();
}
void DrawProbabilityConfigDialog::retranslateUi()
{
setWindowTitle(tr("Draw Probability Settings"));
labelCriteria->setText(tr("Criteria:"));
criteria->setItemText(0, tr("Card Name"));
criteria->setItemText(1, tr("Type"));
criteria->setItemText(2, tr("Subtype"));
criteria->setItemText(3, tr("Mana Value"));
labelExactness->setText(tr("Exactness:"));
exactness->setItemText(0, tr("At least"));
exactness->setItemText(1, tr("Exactly"));
labelQuantity->setText(tr("Quantity (N):"));
labelDrawn->setText(tr("Cards drawn (M):"));
// i18n-friendly suffixes
quantity->setSuffix(tr(" cards"));
drawn->setSuffix(tr(" cards"));
}
void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config)
{
cfg = _config;
criteria->setCurrentIndex(criteria->findData(_config.criteria));
exactness->setCurrentIndex(exactness->findData(_config.atLeast));
quantity->setValue(_config.quantity);
drawn->setValue(_config.drawn);
}
void DrawProbabilityConfigDialog::accept()
{
cfg.criteria = criteria->currentData().toString();
cfg.atLeast = exactness->currentData().toBool();
cfg.quantity = quantity->value();
cfg.drawn = drawn->value();
QDialog::accept();
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "draw_probability_config.h"
#include <QDialog>
#include <QFormLayout>
class QComboBox;
class QSpinBox;
class QLabel;
class DrawProbabilityConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr);
void retranslateUi();
void setFromConfig(const DrawProbabilityConfig &_config);
DrawProbabilityConfig result() const
{
return cfg;
}
protected:
void accept() override;
private:
DrawProbabilityConfig cfg;
QFormLayout *form;
// Widgets
QComboBox *criteria;
QComboBox *exactness;
QSpinBox *quantity;
QSpinBox *drawn;
QLabel *labelCriteria;
QLabel *labelExactness;
QLabel *labelQuantity;
QLabel *labelDrawn;
};

View file

@ -0,0 +1,236 @@
#include "draw_probability_widget.h"
#include "draw_probability_config_dialog.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMap>
#include <QSpinBox>
#include <QTableWidgetItem>
#include <QWidget>
#include <QtMath>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_manager.h>
DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer)
{
controls = new QWidget(this);
controlLayout = new QHBoxLayout(controls);
labelPrefix = new QLabel(this);
controlLayout->addWidget(labelPrefix);
criteriaCombo = new QComboBox(this);
// Give these things item-data so we can translate the actual user-facing strings
criteriaCombo->addItem(QString(), "name");
criteriaCombo->addItem(QString(), "type");
criteriaCombo->addItem(QString(), "subtype");
criteriaCombo->addItem(QString(), "cmc");
controlLayout->addWidget(criteriaCombo);
exactnessCombo = new QComboBox(this);
exactnessCombo->addItem(QString(), true); // At least
exactnessCombo->addItem(QString(), false); // Exactly
controlLayout->addWidget(exactnessCombo);
quantitySpin = new QSpinBox(this);
quantitySpin->setRange(1, 60);
controlLayout->addWidget(quantitySpin);
labelMiddle = new QLabel(this);
controlLayout->addWidget(labelMiddle);
drawnSpin = new QSpinBox(this);
drawnSpin->setRange(1, 60);
drawnSpin->setValue(7);
controlLayout->addWidget(drawnSpin);
labelSuffix = new QLabel(this);
controlLayout->addWidget(labelSuffix);
labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
controlLayout->addStretch(1);
layout->addWidget(controls);
// Table
resultTable = new QTableWidget(this);
resultTable->setColumnCount(3);
resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
layout->addWidget(resultTable);
// Connections
connect(criteriaCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
config.criteria = criteriaCombo->currentData().toString();
updateDisplay();
});
connect(exactnessCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
config.atLeast = exactnessCombo->currentData().toBool();
updateDisplay();
});
connect(quantitySpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int v) {
config.quantity = v;
updateDisplay();
});
connect(drawnSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int v) {
config.drawn = v;
updateDisplay();
});
retranslateUi();
applyConfigToToolbar();
updateFilterOptions();
}
void DrawProbabilityWidget::retranslateUi()
{
bannerWidget->setText(tr("Draw Probability"));
labelPrefix->setText(tr("Probability of drawing"));
criteriaCombo->setItemText(0, tr("Card Name"));
criteriaCombo->setItemText(1, tr("Type"));
criteriaCombo->setItemText(2, tr("Subtype"));
criteriaCombo->setItemText(3, tr("Mana Value"));
exactnessCombo->setItemText(0, tr("At least"));
exactnessCombo->setItemText(1, tr("Exactly"));
labelMiddle->setText(tr("card(s) having drawn at least"));
labelSuffix->setText(tr("cards"));
resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")});
}
QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new DrawProbabilityConfigDialog(parent);
dlg->setFromConfig(config);
return dlg;
}
QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *dp = qobject_cast<DrawProbabilityConfigDialog *>(dlg);
return dp ? dp->result().toJson() : QJsonObject{};
}
void DrawProbabilityWidget::applyConfigToToolbar()
{
auto setComboByData = [](QComboBox *combo, const QVariant &value) {
int idx = combo->findData(value);
if (idx >= 0) {
combo->setCurrentIndex(idx);
}
};
setComboByData(criteriaCombo, config.criteria);
setComboByData(exactnessCombo, config.atLeast);
quantitySpin->setValue(config.quantity);
drawnSpin->setValue(config.drawn);
}
void DrawProbabilityWidget::updateDisplay()
{
updateFilterOptions();
}
void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg)
{
config = DrawProbabilityConfig::fromJson(cfg);
applyConfigToToolbar();
updateFilterOptions();
}
void DrawProbabilityWidget::updateFilterOptions()
{
if (!analyzer->getModel()->getDeckList()) {
return;
}
const QString criteria = config.criteria;
const bool atLeast = config.atLeast;
const int quantity = config.quantity;
const int drawn = config.drawn;
QMap<QString, int> categoryCounts;
int totalDeckCards = 0;
const auto nodes = analyzer->getModel()->getCardNodes();
for (auto *node : nodes) {
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());
if (!info) {
continue;
}
totalDeckCards += node->getNumber();
QStringList categories;
if (criteria == "name") {
categories << info->getName();
} else if (criteria == "type") {
categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts);
} else if (criteria == "subtype") {
categories = info->getCardType().split(' ', Qt::SkipEmptyParts);
} else if (criteria == "cmc") {
categories << QString::number(info->getCmc().toInt());
}
for (const QString &cat : categories) {
categoryCounts[cat] += node->getNumber();
}
}
resultTable->setRowCount(categoryCounts.size());
int row = 0;
for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) {
const QString &cat = it.key();
const int copies = it.value();
double probability = 0.0;
if (atLeast) {
for (int k = quantity; k <= drawn && k <= copies; ++k) {
probability += hypergeometricProbability(totalDeckCards, copies, drawn, k);
}
} else {
probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity);
}
resultTable->setItem(row, 0, new QTableWidgetItem(cat));
resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies)));
resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2)));
}
}
double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k)
{
if (k < 0 || k > n || K > N || n > N) {
return 0.0;
}
double logP = 0.0;
for (int i = 1; i <= k; ++i) {
logP += qLn(double(K - k + i) / i);
}
for (int i = 1; i <= n - k; ++i) {
logP += qLn(double(N - K - (n - k) + i) / i);
}
for (int i = 1; i <= n; ++i) {
logP -= qLn(double(N - n + i) / i);
}
return qExp(logP);
}

View file

@ -0,0 +1,54 @@
#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H
#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "draw_probability_config.h"
#include <QComboBox>
#include <QLineEdit>
#include <QSpinBox>
#include <QTableWidget>
class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public:
DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
QDialog *createConfigDialog(QWidget *parent) override;
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
void applyConfigToToolbar();
public slots:
void updateDisplay() override;
void loadConfig(const QJsonObject &cfg) override;
void retranslateUi();
private slots:
void updateFilterOptions();
private:
DrawProbabilityConfig config;
QWidget *controls;
QHBoxLayout *controlLayout;
QLabel *labelPrefix;
QLabel *labelMiddle;
QLabel *labelSuffix;
QLineEdit *cardNameEdit;
QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value
QComboBox *filterCombo; // The actual value
QComboBox *exactnessCombo; // At least / Exactly
QSpinBox *quantitySpin; // N
QSpinBox *drawnSpin; // M
QSpinBox *manaValueSpin;
QTableWidget *resultTable;
double hypergeometricProbability(int N, int K, int n, int k);
double calculateProbability(int totalCards, int copies, int drawn, bool atLeast);
};
#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H

View file

@ -0,0 +1,32 @@
#include "mana_base_config.h"
QJsonObject ManaBaseConfig::toJson() const
{
QJsonObject jsonObject;
QJsonArray jsonArray;
jsonObject["displayType"] = displayType;
for (auto &filter : filters) {
jsonArray.append(filter);
}
jsonObject["filters"] = jsonArray;
return jsonObject;
}
ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o)
{
ManaBaseConfig config;
if (o.contains("displayType")) {
config.displayType = o["displayType"].toString();
}
if (o.contains("filters")) {
config.filters.clear();
for (auto v : o["filters"].toArray()) {
config.filters << v.toString();
}
}
return config;
}

View file

@ -0,0 +1,19 @@
#ifndef COCKATRICE_MANA_BASE_CONFIG_H
#define COCKATRICE_MANA_BASE_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QStringList>
struct ManaBaseConfig
{
QString displayType; // "pie" or "bar" or "combinedBar"
QStringList filters; // which colors to show, empty = all
QJsonObject toJson() const;
static ManaBaseConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_BASE_CONFIG_H

View file

@ -0,0 +1,67 @@
#include "mana_base_config_dialog.h"
#include <QPushButton>
ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
ManaBaseConfig initial,
QWidget *parent)
: QDialog(parent), config(initial)
{
layout = new QVBoxLayout(this);
displayTypeLabel = new QLabel(this);
layout->addWidget(displayTypeLabel);
displayType = new QComboBox(this);
layout->addWidget(displayType);
filterLabel = new QLabel(this);
layout->addWidget(filterLabel);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
layout->addWidget(filterList);
QStringList colors = analyzer->getManaBase().keys();
colors.sort();
filterList->addItems(colors);
// select initial filters
for (int i = 0; i < filterList->count(); ++i) {
if (config.filters.contains(filterList->item(i)->text()))
filterList->item(i)->setSelected(true);
}
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject);
retranslateUi();
}
void ManaBaseConfigDialog::retranslateUi()
{
setWindowTitle(tr("Mana Base Configuration"));
displayTypeLabel->setText(tr("Display type:"));
displayType->clear();
displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")});
filterLabel->setText(tr("Filter Colors (optional):"));
buttons->button(QDialogButtonBox::Ok)->setText(tr("OK"));
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
}
void ManaBaseConfigDialog::accept()
{
config.displayType = displayType->currentText();
config.filters.clear();
for (auto *item : filterList->selectedItems()) {
config.filters << item->text();
}
QDialog::accept();
}

View file

@ -0,0 +1,42 @@
#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H
#define COCKATRICE_MANA_BASE_ADD_DIALOG_H
#include "../../deck_list_statistics_analyzer.h"
#include "mana_base_config.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
class ManaBaseConfigDialog : public QDialog
{
Q_OBJECT
public:
ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr);
void retranslateUi();
void accept() override;
ManaBaseConfig result() const
{
return config;
}
private:
ManaBaseConfig config;
QVBoxLayout *layout;
QLabel *displayTypeLabel;
QComboBox *displayType;
QLabel *filterLabel;
QListWidget *filterList;
QDialogButtonBox *buttons;
};
#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H

View file

@ -0,0 +1,115 @@
#include "mana_base_widget.h"
#include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
#include "../../analytics_panel_widget_registrar.h"
#include "mana_base_config_dialog.h"
#include <QDialog>
#include <QListWidget>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaBase{
"manaBase", ManaBaseWidget::tr("Mana Base"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
} // anonymous namespace
ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg)
: AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
{
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
layout->addWidget(barContainer);
updateDisplay();
}
void ManaBaseWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
auto &pipCount = analyzer->getProductionPipCount();
auto &cardCount = analyzer->getProductionCardCount();
QHash<QString, int> manaMap;
for (auto key : pipCount.keys()) {
manaMap[key] = pipCount[key];
}
// Apply filters
if (!config.filters.isEmpty()) {
QHash<QString, int> filtered;
for (auto f : config.filters) {
if (manaMap.contains(f)) {
filtered[f] = manaMap[f];
}
}
manaMap = filtered;
}
// Determine maximum for bar charts
int highest = 1;
for (auto val : manaMap) {
highest = std::max(highest, val);
}
// Convert to QMap for ColorBar / ColorPie (sorted)
QMap<QString, int> mapSorted;
for (auto it = manaMap.begin(); it != manaMap.end(); ++it) {
mapSorted.insert(it.key(), it.value());
}
// Choose display mode
if (config.displayType == "bar") {
QHash<QString, QColor> colors = {{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)},
{"B", QColor(21, 11, 0)}, {"R", QColor(211, 32, 42)},
{"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)}};
for (auto color : manaMap.keys()) {
QString label = QString("%1 %2 (%3)").arg(color).arg(manaMap[color]).arg(cardCount.value(color));
BarWidget *bar = new BarWidget(label, manaMap[color], highest, colors.value(color, Qt::gray), this);
barLayout->addWidget(bar);
}
} else if (config.displayType == "combinedBar") {
ColorBar *cb = new ColorBar(mapSorted, this);
cb->setMinimumHeight(30);
barLayout->addWidget(cb);
} else if (config.displayType == "pie") {
ColorPie *pie = new ColorPie(mapSorted, this);
pie->setMinimumSize(200, 200);
barLayout->addWidget(pie);
}
update();
}
QSize ManaBaseWidget::sizeHint() const
{
return QSize(800, 150);
}
QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent)
{
ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent);
return dlg;
}
QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *mc = qobject_cast<ManaBaseConfigDialog *>(dlg);
if (!mc) {
return {};
}
return mc->result().toJson();
}

View file

@ -0,0 +1,51 @@
/**
* @file mana_base_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_BASE_WIDGET_H
#define MANA_BASE_WIDGET_H
#include "../../../general/display/banner_widget.h"
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_base_config.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaBaseWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaBaseConfig::fromJson(o);
updateDisplay();
}
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaBaseConfig config;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_BASE_WIDGET_H

View file

@ -0,0 +1,121 @@
#include "mana_curve_category_widget.h"
#include "libcockatrice/utility/color.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config.h"
#include "mana_curve_total_widget.h"
constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row
ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent)
{
layout = new QVBoxLayout(this);
layout->setSpacing(4);
layout->setContentsMargins(0, 0, 0, 0);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
// Same as minimum for now
QSize ManaCurveCategoryWidget::sizeHint() const
{
if (layout->isEmpty()) {
return QSize(0, 0);
}
// Calculate exact height needed for all rows
int rowCount = layout->count();
int totalHeight = rowCount * MIN_ROW_HEIGHT;
totalHeight += (rowCount - 1) * layout->spacing();
return QSize(0, totalHeight);
}
QSize ManaCurveCategoryWidget::minimumSizeHint() const
{
if (layout->isEmpty()) {
return QSize(0, 0);
}
// Calculate actual minimum based on number of rows
int rowCount = layout->count();
int totalHeight = rowCount * MIN_ROW_HEIGHT;
totalHeight += (rowCount - 1) * layout->spacing();
return QSize(0, totalHeight);
}
void ManaCurveCategoryWidget::updateDisplay(int minCmc,
int maxCmc,
int highest,
QHash<QString, QHash<int, int>> qCategoryCounts,
QHash<QString, QHash<int, QStringList>> qCategoryCards,
const ManaCurveConfig &config)
{
// Clear previous content
QtUtils::clearLayoutRec(layout);
if (!config.showCategoryRows) {
return; // nothing to show
}
// Collect categories
QStringList categories = qCategoryCounts.keys();
// Apply filters
if (!config.filters.isEmpty()) {
QStringList filtered;
for (const QString &cat : categories) {
if (config.filters.contains(cat)) {
filtered.append(cat);
}
}
categories = filtered;
}
std::sort(categories.begin(), categories.end());
for (const QString &cat : categories) {
QWidget *row = new QWidget(this);
row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
row->setFixedHeight(MIN_ROW_HEIGHT);
QHBoxLayout *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
rowLayout->setSpacing(4);
QLabel *categoryLabel = new QLabel(cat, row);
categoryLabel->setFixedWidth(80);
rowLayout->addWidget(categoryLabel);
QVector<BarData> catBars;
const auto cmcCounts = qCategoryCounts.value(cat);
const auto cmcCards = qCategoryCards.value(cat);
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
int val = cmcCounts.value(cmc, 0);
QStringList cards = cmcCards.value(cmc);
QVector<BarSegment> segments;
if (val > 0) {
segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)});
}
catBars.push_back({QString::number(cmc), segments});
}
auto *catChart = new BarChartWidget(row);
catChart->setHighest(highest);
catChart->setBars(catBars);
catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
rowLayout->addWidget(catChart);
layout->addWidget(row);
}
// Update geometry after adding all widgets
updateGeometry();
}

View file

@ -0,0 +1,32 @@
#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class ManaCurveCategoryWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCurveCategoryWidget(QWidget *parent);
void updateDisplay(int minCmc,
int maxCmc,
int highest,
QHash<QString, QHash<int, int>> qCategoryCounts,
QHash<QString, QHash<int, QStringList>> qCategoryCards,
const ManaCurveConfig &config);
public slots:
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
private:
QVBoxLayout *layout;
};
#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H

View file

@ -0,0 +1,41 @@
#include "mana_curve_config.h"
QJsonObject ManaCurveConfig::toJson() const
{
QJsonObject jsonObject;
jsonObject["groupBy"] = groupBy;
QJsonArray jsonArray;
for (auto &filter : filters) {
jsonArray.append(filter);
}
jsonObject["filters"] = jsonArray;
jsonObject["showMain"] = showMain;
jsonObject["showCategoryRows"] = showCategoryRows;
return jsonObject;
}
ManaCurveConfig ManaCurveConfig::fromJson(const QJsonObject &o)
{
ManaCurveConfig config;
if (o.contains("groupBy")) {
config.groupBy = o["groupBy"].toString();
}
if (o.contains("filters")) {
config.filters.clear();
for (auto v : o["filters"].toArray()) {
config.filters << v.toString();
}
}
if (o.contains("showMain")) {
config.showMain = o["showMain"].toBool(true);
}
if (o.contains("showCategoryRows")) {
config.showCategoryRows = o["showCategoryRows"].toBool(true);
}
return config;
}

View file

@ -0,0 +1,21 @@
#ifndef COCKATRICE_MANA_CURVE_CONFIG_H
#define COCKATRICE_MANA_CURVE_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QStringList>
struct ManaCurveConfig
{
QString groupBy = "type"; // "type", "color", "subtype", etc.
QStringList filters; // empty = all
bool showMain = true;
bool showCategoryRows = true;
QJsonObject toJson() const;
static ManaCurveConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_CURVE_CONFIG_H

View file

@ -0,0 +1,91 @@
#include "mana_curve_config_dialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
ManaCurveConfigDialog::ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
: QDialog(parent), analyzer(analyzer)
{
auto *lay = new QVBoxLayout(this);
labelGroupBy = new QLabel(this);
lay->addWidget(labelGroupBy);
groupBy = new QComboBox(this);
lay->addWidget(groupBy);
labelFilters = new QLabel(this);
lay->addWidget(labelFilters);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
lay->addWidget(filterList);
showMain = new QCheckBox(this);
showMain->setChecked(true);
lay->addWidget(showMain);
showCatRows = new QCheckBox(this);
showCatRows->setChecked(true);
lay->addWidget(showCatRows);
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
lay->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaCurveConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaCurveConfigDialog::reject);
// populate dynamic data
QStringList cats = analyzer->getManaCurveByType().keys();
cats.append(analyzer->getManaCurveByColor().keys());
cats.removeDuplicates();
cats.sort();
filterList->addItems(cats);
groupBy->addItems({"type", "color", "subtype", "power", "toughness"});
retranslateUi();
}
void ManaCurveConfigDialog::retranslateUi()
{
labelGroupBy->setText(tr("Group By:"));
groupBy->setItemText(0, tr("type"));
groupBy->setItemText(1, tr("color"));
groupBy->setItemText(2, tr("subtype"));
groupBy->setItemText(3, tr("power"));
groupBy->setItemText(4, tr("toughness"));
labelFilters->setText(tr("Filters (optional):"));
showMain->setText(tr("Show main bar row"));
showCatRows->setText(tr("Show per-category rows"));
}
void ManaCurveConfigDialog::setFromConfig(const ManaCurveConfig &cfg)
{
groupBy->setCurrentText(cfg.groupBy);
// restore filters
for (int i = 0; i < filterList->count(); ++i)
filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text()));
showMain->setChecked(cfg.showMain);
showCatRows->setChecked(cfg.showCategoryRows);
}
void ManaCurveConfigDialog::accept()
{
cfg.groupBy = groupBy->currentText();
cfg.filters.clear();
for (auto *item : filterList->selectedItems())
cfg.filters << item->text();
cfg.showMain = showMain->isChecked();
cfg.showCategoryRows = showCatRows->isChecked();
QDialog::accept();
}

View file

@ -0,0 +1,44 @@
#ifndef COCKATRICE_MANA_CURVE_ADD_DIALOG_H
#define COCKATRICE_MANA_CURVE_ADD_DIALOG_H
#include "../../deck_list_statistics_analyzer.h"
#include "mana_curve_config.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
class QListWidget;
class QCheckBox;
class QComboBox;
class ManaCurveConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr);
void retranslateUi();
void setFromConfig(const ManaCurveConfig &cfg);
ManaCurveConfig result() const
{
return cfg;
}
private:
ManaCurveConfig cfg;
DeckListStatisticsAnalyzer *analyzer;
QLabel *labelGroupBy;
QComboBox *groupBy;
QLabel *labelFilters;
QListWidget *filterList;
QDialogButtonBox *buttons;
QCheckBox *showMain;
QCheckBox *showCatRows;
private slots:
void accept() override;
};
#endif // COCKATRICE_MANA_CURVE_ADD_DIALOG_H

View file

@ -0,0 +1,78 @@
#include "mana_curve_total_widget.h"
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "libcockatrice/utility/color.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
ManaCurveTotalWidget::ManaCurveTotalWidget(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
label = new QLabel(this);
label->setFixedWidth(80);
layout->addWidget(label);
barChart = new BarChartWidget(this);
layout->addWidget(barChart, 1);
setMinimumHeight(200);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
QSize ManaCurveTotalWidget::sizeHint() const
{
return {0, 280};
}
QSize ManaCurveTotalWidget::minimumSizeHint() const
{
return {0, 200};
}
void ManaCurveTotalWidget::updateDisplay(const QString &categoryName,
int minCmc,
int maxCmc,
int highest,
const QMap<int, QMap<QString, int>> &cmcMap,
const QMap<QString, QMap<int, QStringList>> &cardsMap,
const ManaCurveConfig &config)
{
QVector<BarData> mainBars;
mainBars.reserve(maxCmc - minCmc + 1);
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
QVector<BarSegment> segments;
const auto cmcIt = cmcMap.constFind(cmc);
if (cmcIt != cmcMap.cend()) {
for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) {
const QString &category = it.key();
if (!config.filters.isEmpty() && !config.filters.contains(category))
continue;
const int value = it.value();
QStringList cards;
const auto catIt = cardsMap.constFind(category);
if (catIt != cardsMap.cend())
cards = catIt->value(cmc);
segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)});
}
}
std::sort(segments.begin(), segments.end(),
[](const BarSegment &a, const BarSegment &b) { return a.category < b.category; });
mainBars.push_back({QString::number(cmc), segments});
}
label->setText(categoryName);
barChart->setHighest(highest);
barChart->setBars(mainBars);
}

View file

@ -0,0 +1,32 @@
#ifndef COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
#define COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class ManaCurveTotalWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCurveTotalWidget(QWidget *parent);
QSize sizeHint() const;
QSize minimumSizeHint() const;
void updateDisplay(const QString &categoryName,
int minCmc,
int maxCmc,
int highest,
const QMap<int, QMap<QString, int>> &cmcMap,
const QMap<QString, QMap<int, QStringList>> &cardsMap,
const ManaCurveConfig &config);
private:
QHBoxLayout *layout;
QLabel *label;
BarChartWidget *barChart;
};
#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H

View file

@ -0,0 +1,148 @@
#include "mana_curve_widget.h"
#include "../../../general/display/charts/bars/bar_chart_background_widget.h"
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "../../../general/display/charts/bars/segmented_bar_widget.h"
#include "../../analytics_panel_widget_registrar.h"
#include "../../deck_list_statistics_analyzer.h"
#include "libcockatrice/utility/color.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config_dialog.h"
#include <QInputDialog>
#include <QJsonArray>
#include <QLabel>
#include <QPushButton>
#include <QSettings>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaCurve{
"manaCurve", ManaCurveWidget::tr("Mana Curve"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }};
} // anonymous namespace
ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg)
: AbstractAnalyticsPanelWidget(parent, analyzer), config(cfg)
{
setLayout(layout);
totalWidget = new ManaCurveTotalWidget(this);
totalWidget->setHidden(true);
layout->addWidget(totalWidget);
categoryWidget = new ManaCurveCategoryWidget(this);
categoryWidget->setHidden(true);
layout->addWidget(categoryWidget);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay);
updateDisplay();
}
QDialog *ManaCurveWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new ManaCurveConfigDialog(analyzer, parent);
dlg->setFromConfig(config);
return dlg;
}
QJsonObject ManaCurveWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *mc = qobject_cast<ManaCurveConfigDialog *>(dlg);
return mc ? mc->result().toJson() : QJsonObject{};
}
static void buildMapsByCategory(const QHash<QString, QHash<int, int>> &categoryCounts,
const QHash<QString, QHash<int, QStringList>> &categoryCards,
QMap<int, QMap<QString, int>> &outCmcMap,
QMap<QString, QMap<int, QStringList>> &outCardsMap)
{
outCmcMap.clear();
outCardsMap.clear();
for (auto catIt = categoryCounts.cbegin(); catIt != categoryCounts.cend(); ++catIt) {
const QString &category = catIt.key();
const auto &countsByCmc = catIt.value();
for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it)
outCmcMap[it.key()][category] = it.value();
}
for (auto catIt = categoryCards.cbegin(); catIt != categoryCards.cend(); ++catIt) {
const QString &category = catIt.key();
const auto &cardsByCmc = catIt.value();
for (auto it = cardsByCmc.cbegin(); it != cardsByCmc.cend(); ++it)
outCardsMap[category][it.key()] = it.value();
}
}
static void findGlobalCmcRange(const QHash<QString, QHash<int, int>> &categoryCounts, int &minCmc, int &maxCmc)
{
minCmc = 0;
maxCmc = 0;
for (const auto &countsByCmc : categoryCounts) {
for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it)
maxCmc = qMax(maxCmc, it.key());
}
}
void ManaCurveWidget::updateDisplay()
{
QHash<QString, QHash<int, int>> categoryCounts;
QHash<QString, QHash<int, QStringList>> categoryCards;
if (config.groupBy == "color") {
categoryCounts = analyzer->getManaCurveByColor();
categoryCards = analyzer->getManaCurveCardsByColor();
} else if (config.groupBy == "subtype") {
categoryCounts = analyzer->getManaCurveBySubtype();
categoryCards = analyzer->getManaCurveCardsBySubtype();
} else if (config.groupBy == "power") {
categoryCounts = analyzer->getManaCurveByPower();
categoryCards = analyzer->getManaCurveCardsByPower();
} else {
categoryCounts = analyzer->getManaCurveByType();
categoryCards = analyzer->getManaCurveCardsByType();
}
QMap<int, QMap<QString, int>> cmcMap;
QMap<QString, QMap<int, QStringList>> cardsMap;
buildMapsByCategory(categoryCounts, categoryCards, cmcMap, cardsMap);
int minCmc = 0;
int maxCmc = 0;
findGlobalCmcRange(categoryCounts, minCmc, maxCmc);
int highest = 1;
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
int sum = 0;
const auto cmcIt = cmcMap.constFind(cmc);
if (cmcIt != cmcMap.cend()) {
for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) {
if (!config.filters.isEmpty() && !config.filters.contains(it.key())) {
continue;
}
sum += it.value();
}
}
highest = qMax(highest, sum);
}
totalWidget->updateDisplay(config.groupBy, minCmc, maxCmc, highest, cmcMap, cardsMap, config);
totalWidget->setVisible(config.showMain);
categoryWidget->updateDisplay(minCmc, maxCmc, highest, categoryCounts, categoryCards, config);
categoryWidget->setVisible(config.showCategoryRows);
}

View file

@ -0,0 +1,50 @@
/**
* @file mana_curve_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_CURVE_WIDGET_H
#define MANA_CURVE_WIDGET_H
#include "../../abstract_analytics_panel_widget.h"
#include "mana_curve_category_widget.h"
#include "mana_curve_config.h"
#include "mana_curve_total_widget.h"
#include <QVBoxLayout>
class SegmentedBarWidget;
class DeckListStatisticsAnalyzer;
class ManaCurveWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
// QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaCurveConfig::fromJson(o);
updateDisplay();
};
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaCurveConfig config;
ManaCurveTotalWidget *totalWidget;
ManaCurveCategoryWidget *categoryWidget;
};
#endif // MANA_CURVE_WIDGET_H

View file

@ -0,0 +1,31 @@
#include "mana_devotion_config.h"
QJsonObject ManaDevotionConfig::toJson() const
{
QJsonObject jsonObject;
QJsonArray jsonArray;
jsonObject["displayType"] = displayType;
for (auto &filter : filters) {
jsonArray.append(filter);
}
jsonObject["filters"] = jsonArray;
return jsonObject;
}
ManaDevotionConfig ManaDevotionConfig::fromJson(const QJsonObject &o)
{
ManaDevotionConfig 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

@ -0,0 +1,18 @@
#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H
#define COCKATRICE_MANA_DEVOTION_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QStringList>
struct ManaDevotionConfig
{
QString displayType; // "pie" or "bar" or "combinedBar"
QStringList filters; // which colors to show, empty = all
QJsonObject toJson() const;
static ManaDevotionConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_DEVOTION_CONFIG_H

View file

@ -0,0 +1,62 @@
#include "mana_devotion_config_dialog.h"
ManaDevotionConfigDialog::ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer,
ManaDevotionConfig initial,
QWidget *parent)
: QDialog(parent), config(initial)
{
layout = new QVBoxLayout(this);
labelDisplayType = new QLabel(this);
layout->addWidget(labelDisplayType);
displayType = new QComboBox(this);
layout->addWidget(displayType);
labelFilters = new QLabel(this);
layout->addWidget(labelFilters);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
QStringList colors = analyzer->getDevotionPipCount().keys();
colors.sort();
filterList->addItems(colors);
layout->addWidget(filterList);
// 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, &ManaDevotionConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaDevotionConfigDialog::reject);
// populate combo box items
displayType->addItems({"pie", "bar", "combinedBar"});
retranslateUi();
}
void ManaDevotionConfigDialog::retranslateUi()
{
labelDisplayType->setText(tr("Display type:"));
displayType->setItemText(0, tr("pie"));
displayType->setItemText(1, tr("bar"));
displayType->setItemText(2, tr("combinedBar"));
labelFilters->setText(tr("Filter Colors (optional):"));
}
void ManaDevotionConfigDialog::accept()
{
config.displayType = displayType->currentText();
config.filters.clear();
for (auto *item : filterList->selectedItems()) {
config.filters << item->text();
}
QDialog::accept();
}

View file

@ -0,0 +1,42 @@
#ifndef COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
#define COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
#include "../../deck_list_statistics_analyzer.h"
#include "mana_devotion_config.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
class ManaDevotionConfigDialog : public QDialog
{
Q_OBJECT
public:
ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer,
ManaDevotionConfig initial = {},
QWidget *parent = nullptr);
void retranslateUi();
void accept() override;
ManaDevotionConfig result() const
{
return config;
}
private:
ManaDevotionConfig config;
QVBoxLayout *layout;
QLabel *labelDisplayType;
QComboBox *displayType;
QLabel *labelFilters;
QListWidget *filterList;
QDialogButtonBox *buttons;
};
#endif // COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H

View file

@ -0,0 +1,123 @@
#include "mana_devotion_widget.h"
#include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
#include "../../analytics_panel_widget_registrar.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_devotion_config_dialog.h"
#include <QHash>
#include <QInputDialog>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaDevotion{
"manaDevotion", ManaDevotionWidget::tr("Mana Devotion"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }};
} // anonymous namespace
ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg)
: AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
{
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
barContainer->setLayout(barLayout);
layout->addWidget(barContainer);
updateDisplay();
}
void ManaDevotionWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
auto &pipCount = analyzer->getDevotionPipCount();
auto &cardCount = analyzer->getDevotionCardCount();
// Convert keys to single QChar form
QHash<QChar, int> devoMap;
for (auto key : pipCount.keys()) {
devoMap[key[0]] = pipCount[key];
}
// Apply filters
if (!config.filters.isEmpty()) {
QHash<QChar, int> filtered;
for (auto f : config.filters) {
if (devoMap.contains(f[0])) {
filtered[f[0]] = devoMap[f[0]];
}
}
devoMap = filtered;
}
// Determine maximum for bar charts
int highest = 1;
for (auto val : devoMap) {
highest = std::max(highest, val);
}
// Convert to QMap<QString,int> for ColorBar / ColorPie
QMap<QString, int> mapSorted;
for (auto it = devoMap.begin(); it != devoMap.end(); ++it) {
mapSorted.insert(QString(it.key()), it.value());
}
// Color map
QHash<QChar, QColor> colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
{'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
{'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
// Choose display mode
if (config.displayType == "bar") {
// One BarWidget per devotion color
for (auto c : devoMap.keys()) {
QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c)));
BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this);
barLayout->addWidget(bar);
}
} else if (config.displayType == "combinedBar") {
// Stacked devotion bar
ColorBar *cb = new ColorBar(mapSorted, this);
cb->setMinimumHeight(30);
barLayout->addWidget(cb);
} else if (config.displayType == "pie") {
// Devotion pie chart
ColorPie *pie = new ColorPie(mapSorted, this);
pie->setMinimumSize(200, 200);
barLayout->addWidget(pie);
}
update();
}
QDialog *ManaDevotionWidget::createConfigDialog(QWidget *parent)
{
ManaDevotionConfigDialog *dlg = new ManaDevotionConfigDialog(analyzer, config, parent);
return dlg;
}
QJsonObject ManaDevotionWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *mc = qobject_cast<ManaDevotionConfigDialog *>(dlg);
if (!mc) {
return {};
}
return mc->result().toJson();
}
QSize ManaDevotionWidget::sizeHint() const
{
return QSize(800, 150);
}

View file

@ -0,0 +1,45 @@
/**
* @file mana_devotion_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_DEVOTION_WIDGET_H
#define MANA_DEVOTION_WIDGET_H
#include "../../../general/display/banner_widget.h"
#include "../../abstract_analytics_panel_widget.h"
#include "mana_devotion_config.h"
#include <QHBoxLayout>
class ManaDevotionWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaDevotionConfig::fromJson(o);
updateDisplay();
}
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaDevotionConfig config;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_DEVOTION_WIDGET_H

View file

@ -0,0 +1,36 @@
#include "mana_distribution_config.h"
QJsonObject ManaDistributionConfig::toJson() const
{
QJsonObject o;
o["displayType"] = displayType;
QJsonArray jsonArray;
for (auto &s : filters) {
jsonArray.append(s);
}
o["filters"] = jsonArray;
o["showColorRows"] = showColorRows;
return o;
}
ManaDistributionConfig ManaDistributionConfig::fromJson(const QJsonObject &o)
{
ManaDistributionConfig 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();
}
if (o.contains("showColorRows")) {
config.showColorRows = o["showColorRows"].toBool(true);
}
return config;
}

View file

@ -0,0 +1,20 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QStringList>
struct ManaDistributionConfig
{
QString displayType = "pie"; // "pie" or "bar"
QStringList filters; // empty = all colors
bool showColorRows = true;
QJsonObject toJson() const;
static ManaDistributionConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_DISTRIBUTION_CONFIG_H

View file

@ -0,0 +1,83 @@
#include "mana_distribution_config_dialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
: QDialog(parent), analyzer(analyzer)
{
auto *lay = new QVBoxLayout(this);
// Labels
labelDisplayType = new QLabel(this);
lay->addWidget(labelDisplayType);
displayType = new QComboBox(this);
lay->addWidget(displayType);
labelFilters = new QLabel(this);
lay->addWidget(labelFilters);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
filterList->addItems(kColors); // dynamic/fixed, no translation needed
lay->addWidget(filterList);
showColorRows = new QCheckBox(this);
showColorRows->setChecked(true);
lay->addWidget(showColorRows);
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
lay->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaDistributionConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaDistributionConfigDialog::reject);
displayType->addItems({"pie", "bar"}); // combo items
retranslateUi();
}
void ManaDistributionConfigDialog::retranslateUi()
{
labelDisplayType->setText(tr("Top display type:"));
displayType->setItemText(0, tr("pie"));
displayType->setItemText(1, tr("bar"));
labelFilters->setText(tr("Colors:"));
showColorRows->setText(tr("Show per-color rows"));
// QDialogButtonBox buttons are automatically translated
}
void ManaDistributionConfigDialog::setFromConfig(const ManaDistributionConfig &cfgIn)
{
cfg = cfgIn;
displayType->setCurrentText(cfg.displayType);
for (int i = 0; i < filterList->count(); ++i)
filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text()));
showColorRows->setChecked(cfg.showColorRows);
}
void ManaDistributionConfigDialog::accept()
{
cfg.displayType = displayType->currentText();
// Filters
cfg.filters.clear();
for (auto *item : filterList->selectedItems())
cfg.filters << item->text();
cfg.showColorRows = showColorRows->isChecked();
QDialog::accept();
}

View file

@ -0,0 +1,45 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
#include "mana_distribution_config.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QStringList>
class QComboBox;
class QListWidget;
class QCheckBox;
class DeckListStatisticsAnalyzer;
class ManaDistributionConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr);
void retranslateUi();
void setFromConfig(const ManaDistributionConfig &cfg);
const ManaDistributionConfig &config() const
{
return cfg;
}
public slots:
void accept() override;
private:
DeckListStatisticsAnalyzer *analyzer;
QLabel *labelDisplayType;
QComboBox *displayType;
QLabel *labelFilters;
QListWidget *filterList;
QCheckBox *showColorRows;
QDialogButtonBox *buttons;
ManaDistributionConfig cfg;
};
#endif // COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H

View file

@ -0,0 +1,49 @@
#include "mana_distribution_single_display_widget.h"
#include "../../../cards/additional_info/mana_symbol_widget.h"
#include <QVBoxLayout>
ManaDistributionSingleDisplayWidget::ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent)
: QWidget(parent)
{
auto layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignHCenter);
symbolLabel = new ManaSymbolWidget(this, colorSymbol, true, false);
symbolLabel->setFixedSize(40, 40);
devotionBar = new QProgressBar(this);
devotionBar->setRange(0, 100);
devotionBar->setTextVisible(false);
devotionLabel = new QLabel(this);
devotionLabel->setAlignment(Qt::AlignCenter);
productionBar = new QProgressBar(this);
productionBar->setRange(0, 100);
productionBar->setTextVisible(false);
productionLabel = new QLabel(this);
productionLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(symbolLabel);
layout->addWidget(devotionBar);
layout->addWidget(devotionLabel);
layout->addWidget(productionBar);
layout->addWidget(productionLabel);
setLayout(layout);
}
void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int percent)
{
devotionBar->setValue(percent);
devotionLabel->setText(QString(tr("%1 pips (%2 cards)")).arg(pips).arg(cards));
}
void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent)
{
productionBar->setValue(percent);
productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards));
}

View file

@ -0,0 +1,28 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
#include <QHBoxLayout>
#include <QLabel>
#include <QProgressBar>
#include <QWidget>
class ManaDistributionSingleDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent = nullptr);
void setDevotion(int pips, int cards, int percent);
void setProduction(int pips, int cards, int percent);
private:
QLabel *symbolLabel;
QProgressBar *devotionBar;
QLabel *devotionLabel;
QProgressBar *productionBar;
QLabel *productionLabel;
};
#endif // COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H

View file

@ -0,0 +1,129 @@
#include "mana_distribution_widget.h"
#include "../../analytics_panel_widget_registrar.h"
#include "mana_distribution_config_dialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QVBoxLayout>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaDistribution{
"manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }};
} // anonymous namespace
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer)
{
container = new QWidget(this);
containerLayout = new QVBoxLayout(container);
devotionBarTop = new ColorBar({}, this);
devotionPieTop = new ColorPie({}, this);
productionBarTop = new ColorBar({}, this);
productionPieTop = new ColorPie({}, this);
containerLayout->addWidget(devotionBarTop);
containerLayout->addWidget(devotionPieTop);
containerLayout->addWidget(productionBarTop);
containerLayout->addWidget(productionPieTop);
devotionPieTop->hide();
productionPieTop->hide();
row = new QHBoxLayout();
containerLayout->addLayout(row);
for (const QString &c : kColors) {
auto *w = new ManaDistributionSingleDisplayWidget(c, this);
row->addWidget(w);
rows[c] = w;
}
layout->addWidget(container);
}
void ManaDistributionWidget::updateDisplay()
{
const auto &devPips = analyzer->getDevotionPipCount();
const auto &devCards = analyzer->getDevotionCardCount();
const auto &prodPips = analyzer->getProductionPipCount();
const auto &prodCards = analyzer->getProductionCardCount();
QStringList filtered = config.filters.isEmpty() ? kColors : config.filters;
QMap<QString, int> devMap, prodMap;
for (const QString &c : filtered) {
devMap[c] = devPips.value(c, 0);
prodMap[c] = prodPips.value(c, 0);
}
bool showPie = (config.displayType == "pie");
devotionBarTop->setVisible(!showPie);
productionBarTop->setVisible(!showPie);
devotionPieTop->setVisible(showPie);
productionPieTop->setVisible(showPie);
if (showPie) {
devotionPieTop->setColors(devMap);
productionPieTop->setColors(prodMap);
} else {
devotionBarTop->setColors(devMap);
productionBarTop->setColors(prodMap);
}
for (const QString &c : kColors) {
auto *w = rows.value(c);
if (!w) {
continue;
}
bool visible = config.showColorRows && filtered.contains(c);
w->setVisible(visible);
if (!visible) {
continue;
}
int dp = devPips.value(c, 0);
int dc = devCards.value(c, 0);
int pp = prodPips.value(c, 0);
int pc = prodCards.value(c, 0);
// Compute percentages
int totalDev = 0;
int totalProd = 0;
for (const QString &cc : filtered) {
totalDev += devPips.value(cc, 0);
totalProd += prodPips.value(cc, 0);
}
int devPct = (totalDev > 0) ? int(100.0 * dp / totalDev) : 0;
int prodPct = (totalProd > 0) ? int(100.0 * pp / totalProd) : 0;
w->setDevotion(dp, dc, devPct);
w->setProduction(pp, pc, prodPct);
}
}
QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new ManaDistributionConfigDialog(analyzer, parent);
dlg->setWindowTitle(tr("Mana Distribution Settings"));
dlg->setFromConfig(config);
connect(dlg, &QDialog::accepted, [this, dlg]() {
config = dlg->config();
updateDisplay();
});
return dlg;
}

View file

@ -0,0 +1,45 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
#define COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_distribution_config.h"
#include "mana_distribution_single_display_widget.h"
#include <QHBoxLayout>
#include <QMap>
#include <QVBoxLayout>
#include <QWidget>
class ManaDistributionWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public:
explicit ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
QJsonObject extractConfigFromDialog(QDialog *) const override
{
return {};
}
private:
ManaDistributionConfig config;
QWidget *container;
QVBoxLayout *containerLayout;
QVBoxLayout *topLayout;
ColorBar *devotionBarTop;
ColorPie *devotionPieTop;
ColorBar *productionBarTop;
ColorPie *productionPieTop;
QHBoxLayout *row;
QMap<QString, ManaDistributionSingleDisplayWidget *> rows;
};
#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H

View file

@ -1,35 +1,298 @@
#include "deck_analytics_widget.h" #include "deck_analytics_widget.h"
DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel) #include "abstract_analytics_panel_widget.h"
: QWidget(parent), deckListModel(_deckListModel) #include "add_analytics_panel_dialog.h"
{ #include "analytics_panel_widget_factory.h"
mainLayout = new QVBoxLayout(); #include "analyzer_modules/mana_base/mana_base_config.h"
setLayout(mainLayout); #include "analyzer_modules/mana_curve/mana_curve_config.h"
#include "analyzer_modules/mana_devotion/mana_devotion_config.h"
#include "deck_list_statistics_analyzer.h"
#include "resizable_panel.h"
#include <QEvent>
#include <QJsonArray>
#include <QJsonDocument>
#include <QPushButton>
#include <QScrollArea>
#include <QSettings>
#include <QVBoxLayout>
DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer)
: QWidget(parent), statsAnalyzer(_statsAnalyzer)
{
layout = new QVBoxLayout(this);
// Controls
controlContainer = new QWidget(this);
controlLayout = new QHBoxLayout(controlContainer);
addButton = new QPushButton(this);
removeButton = new QPushButton(this);
saveButton = new QPushButton(this);
loadButton = new QPushButton(this);
controlLayout->addWidget(addButton);
controlLayout->addWidget(removeButton);
controlLayout->addWidget(saveButton);
controlLayout->addWidget(loadButton);
layout->addWidget(controlContainer);
connect(addButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onAddPanel);
connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected);
connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout);
connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout);
// Scroll area and container
scrollArea = new QScrollArea(this); scrollArea = new QScrollArea(this);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setWidgetResizable(true); scrollArea->setWidgetResizable(true);
mainLayout->addWidget(scrollArea); scrollArea->setFrameShape(QFrame::NoFrame);
container = new QWidget(scrollArea); panelContainer = new QWidget(scrollArea);
containerLayout = new QVBoxLayout(container); panelLayout = new QVBoxLayout(panelContainer);
container->setLayout(containerLayout); panelLayout->setSpacing(8);
scrollArea->setWidget(container); panelLayout->setContentsMargins(4, 4, 4, 4);
panelLayout->addStretch(1); // push panels up
deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel); scrollArea->setWidget(panelContainer);
layout->addWidget(scrollArea);
manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer); loadLayout();
containerLayout->addWidget(manaCurveWidget);
manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer); retranslateUi();
containerLayout->addWidget(manaDevotionWidget);
manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer);
containerLayout->addWidget(manaBaseWidget);
} }
void DeckAnalyticsWidget::refreshDisplays() void DeckAnalyticsWidget::retranslateUi()
{ {
deckListStatisticsAnalyzer->update(); addButton->setText(tr("Add Panel"));
removeButton->setText(tr("Remove Panel"));
saveButton->setText(tr("Save Layout"));
loadButton->setText(tr("Load Layout"));
}
void DeckAnalyticsWidget::updateDisplays()
{
statsAnalyzer->analyze();
}
void DeckAnalyticsWidget::onAddPanel()
{
AddAnalyticsPanelDialog dlg(this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
QString selection = dlg.selectedType();
if (selection.isEmpty()) {
return;
}
AbstractAnalyticsPanelWidget *analyticsWidget =
AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer);
if (!analyticsWidget) {
return;
}
if (!analyticsWidget->applyConfigFromDialog()) {
analyticsWidget->deleteLater();
return;
}
addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig());
}
void DeckAnalyticsWidget::addPanelInstance(const QString &typeId,
AbstractAnalyticsPanelWidget *panel,
const QJsonObject &cfg)
{
panel->loadConfig(cfg);
panel->updateDisplay();
auto *resPanel = new ResizablePanel(typeId, panel, panelContainer);
panelWrappers.push_back(resPanel);
panelLayout->insertWidget(panelLayout->count() - 1, resPanel);
// Event filter for selection
resPanel->installEventFilter(this);
panel->installEventFilter(this);
// Connect drag-drop signals
connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped);
}
void DeckAnalyticsWidget::onRemoveSelected()
{
int idx = indexOfSelectedWrapper();
if (idx < 0) {
return;
}
ResizablePanel *panel = panelWrappers.takeAt(idx);
selectWrapper(nullptr);
panel->deleteLater();
}
void DeckAnalyticsWidget::saveLayout()
{
QJsonArray arr;
for (auto *wrapper : panelWrappers) {
QJsonObject entry;
entry["type"] = wrapper->getTypeId();
entry["config"] = wrapper->panel->saveConfig();
entry["height"] = wrapper->getCurrentHeight();
arr.append(entry);
}
QSettings s;
s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)));
}
void DeckAnalyticsWidget::loadLayout()
{
if (!loadLayoutInternal()) {
addDefaultPanels();
}
}
void DeckAnalyticsWidget::addDefaultPanels()
{
struct DefaultPanel
{
QString type;
QJsonObject cfg;
};
// Prepare configs
QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson();
QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson();
QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson();
QVector<DefaultPanel> defaults = {
{"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}};
for (auto &d : defaults) {
AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer);
if (!w) {
continue;
}
w->loadConfig(d.cfg);
addPanelInstance(d.type, w, d.cfg);
}
}
bool DeckAnalyticsWidget::loadLayoutInternal()
{
QSettings s;
QString layoutData = s.value("deckAnalytics/layout").toString();
if (layoutData.isEmpty()) {
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8());
if (!doc.isArray()) {
return false;
}
clearPanels();
for (auto v : doc.array()) {
if (!v.isObject()) {
continue;
}
QJsonObject o = v.toObject();
QString type = o["type"].toString();
QJsonObject cfg = o["config"].toObject();
AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer);
if (!w) {
continue;
}
addPanelInstance(type, w, cfg);
// Restore height AFTER adding the panel
if (o.contains("height")) {
panelWrappers.last()->setHeightFromSaved(o["height"].toInt());
}
}
return true;
}
void DeckAnalyticsWidget::clearPanels()
{
selectWrapper(nullptr);
while (!panelWrappers.isEmpty()) {
ResizablePanel *p = panelWrappers.takeLast();
p->deleteLater();
}
}
bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseButtonPress) {
for (auto *p : panelWrappers) {
if (obj == p || obj == p->panel) {
selectWrapper(p);
break;
}
}
}
return QWidget::eventFilter(obj, event);
}
void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w)
{
// Same wrapper
if (selectedWrapper == w) {
return;
}
// Deselect the old one
if (selectedWrapper) {
selectedWrapper->setSelected(false);
}
// Set current
selectedWrapper = w;
// Finally, select new
if (selectedWrapper) {
selectedWrapper->setSelected(true);
}
}
int DeckAnalyticsWidget::indexOfSelectedWrapper() const
{
if (!selectedWrapper) {
return -1;
}
return panelWrappers.indexOf(selectedWrapper);
}
void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore)
{
int draggedIdx = panelWrappers.indexOf(dragged);
int targetIdx = panelWrappers.indexOf(target);
if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) {
return;
}
// Remove dragged panel from list and layout
panelWrappers.removeAt(draggedIdx);
panelLayout->removeWidget(dragged);
// Adjust target index if needed
if (draggedIdx < targetIdx) {
targetIdx--;
}
// Calculate insertion position
int insertIdx = insertBefore ? targetIdx : targetIdx + 1;
// Insert back into list and layout
panelWrappers.insert(insertIdx, dragged);
panelLayout->insertWidget(insertIdx, dragged);
// Clear selection
selectWrapper(nullptr);
} }

View file

@ -1,44 +1,71 @@
/** /**
* @file deck_analytics_widget.h * @file deck_analytics_widget.h
* @ingroup DeckEditorAnalyticsWidgets * @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this. * @brief Main analytics widget container with resizable panels for deck statistics.
*/ */
#ifndef DECK_ANALYTICS_WIDGET_H #ifndef DECK_ANALYTICS_WIDGET_H
#define DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H
#include "mana_base_widget.h" #include "abstract_analytics_panel_widget.h"
#include "mana_curve_widget.h" #include "deck_list_statistics_analyzer.h"
#include "mana_devotion_widget.h" #include "resizable_panel.h"
#include <QHBoxLayout> #include <QJsonObject>
#include <QScrollArea> #include <QScrollArea>
#include <QVBoxLayout>
#include <QVector>
#include <QWidget> #include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h>
class LayoutInspector;
class DeckAnalyticsWidget : public QWidget class DeckAnalyticsWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
public slots:
void updateDisplays();
public: public:
explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel); explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void setDeckList(const DeckList &_deckListModel); void retranslateUi();
std::map<int, int> analyzeManaCurve();
void refreshDisplays(); private slots:
void onAddPanel();
void onRemoveSelected();
void onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore);
void saveLayout();
void loadLayout();
void addDefaultPanels();
bool loadLayoutInternal();
void clearPanels();
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
void selectWrapper(ResizablePanel *panel);
int indexOfSelectedWrapper() const;
private: private:
DeckListModel *deckListModel; void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {});
DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer;
QVBoxLayout *mainLayout;
QWidget *container; QVBoxLayout *layout;
QVBoxLayout *containerLayout; QWidget *controlContainer;
QHBoxLayout *controlLayout;
QPushButton *addButton;
QPushButton *removeButton;
QPushButton *saveButton;
QPushButton *loadButton;
QScrollArea *scrollArea; QScrollArea *scrollArea;
QWidget *panelContainer;
QVBoxLayout *panelLayout;
ManaCurveWidget *manaCurveWidget; QVector<ResizablePanel *> panelWrappers;
ManaDevotionWidget *manaDevotionWidget; ResizablePanel *selectedWrapper = nullptr;
ManaBaseWidget *manaBaseWidget;
DeckListStatisticsAnalyzer *statsAnalyzer;
LayoutInspector *insp = nullptr;
}; };
#endif // DECK_ANALYTICS_WIDGET_H #endif // DECK_ANALYTICS_WIDGET_H

View file

@ -9,38 +9,102 @@
DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent,
DeckListModel *_model, DeckListModel *_model,
DeckListStatisticsAnalyzerConfig cfg) DeckListStatisticsAnalyzerConfig _config)
: QObject(parent), model(_model), config(cfg) : QObject(parent), model(_model), config(_config)
{ {
connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::update); connect(model, &DeckListModel::cardsChanged, this, &DeckListStatisticsAnalyzer::analyze);
} }
void DeckListStatisticsAnalyzer::update() void DeckListStatisticsAnalyzer::analyze()
{ {
manaBaseMap.clear(); clearData();
manaCurveMap.clear();
manaDevotionMap.clear();
QList<ExactCard> cards = model->getCards(); QList<const DecklistCardNode *> nodes = model->getCardNodes();
for (const ExactCard &card : cards) { for (auto node : nodes) {
// ---- Mana curve ---- CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName());
if (!info) {
continue;
}
const int amount = node->getNumber();
QStringList copiesOfName;
for (int i = 0; i < amount; i++) {
copiesOfName.append(node->getName());
}
// Convert once
const int cmc = info->getCmc().toInt();
QStringList types = info->getMainCardType().split(' ');
QStringList subtypes = info->getCardType().split('-').last().split(" ");
QString colors = info->getColors();
int power = info->getPowTough().split("/").first().toInt();
int toughness = info->getPowTough().split("/").last().toInt();
// For each copy of card
// ---------------- Mana Curve ----------------
if (config.computeManaCurve) { if (config.computeManaCurve) {
manaCurveMap[card.getInfo().getCmc().toInt()]++; manaCurveMap[cmc] += amount;
} }
// ---- Mana base ---- // per-type curve
for (auto &t : types) {
manaCurveByType[t][cmc] += amount;
manaCurveCardsByType[t][cmc] << copiesOfName;
}
// Per-subtype curve
for (auto &st : subtypes) {
manaCurveBySubtype[st][cmc] += amount;
manaCurveCardsBySubtype[st][cmc] << copiesOfName;
}
// per-color curve
for (auto &c : colors) {
manaCurveByColor[c][cmc] += amount;
manaCurveCardsByColor[c][cmc] << copiesOfName;
}
// Power/toughness
manaCurveByPower[QString::number(power)][cmc] += amount;
manaCurveCardsByPower[QString::number(power)][cmc] << copiesOfName;
manaCurveByToughness[QString::number(toughness)][cmc] += amount;
manaCurveCardsByToughness[QString::number(toughness)][cmc] << copiesOfName;
// ========== Category Counts ===========
for (auto &t : types) {
typeCount[t] += amount;
}
for (auto &st : subtypes) {
subtypeCount[st] += amount;
}
for (auto &c : colors) {
colorCount[c] += amount;
}
manaValueCount[cmc] += amount;
// ---------------- Mana Base ----------------
if (config.computeManaBase) { if (config.computeManaBase) {
auto mana = determineManaProduction(card.getInfo().getText()); auto prod = determineManaProduction(info->getText());
for (auto it = mana.begin(); it != mana.end(); ++it) for (auto it = prod.begin(); it != prod.end(); ++it) {
manaBaseMap[it.key()] += it.value(); if (it.value() > 0) {
productionPipCount[it.key()] += it.value() * amount;
productionCardCount[it.key()] += amount;
}
manaBaseMap[it.key()] += it.value() * amount;
}
} }
// ---- Devotion ---- // ---------------- Devotion ----------------
if (config.computeDevotion) { if (config.computeDevotion) {
auto devo = countManaSymbols(card.getInfo().getManaCost()); auto devo = countManaSymbols(info->getManaCost());
for (auto &d : devo) for (auto &d : devo) {
manaDevotionMap[d.first] += d.second; if (d.second > 0) {
devotionPipCount[QString(d.first)] += d.second * amount;
devotionCardCount[QString(d.first)] += amount;
}
manaDevotionMap[d.first] += d.second * amount;
}
} }
} }
@ -112,3 +176,57 @@ std::unordered_map<char, int> DeckListStatisticsAnalyzer::countManaSymbols(const
return manaCounts; return manaCounts;
} }
// Hypergeometric probability: P(X=k)
double DeckListStatisticsAnalyzer::hypergeometric(int N, int K, int n, int k)
{
if (k < 0 || k > n || K > N) {
return 0.0;
}
auto choose = [](int n, int r) -> double {
if (r > n)
return 0.0;
if (r == 0 || r == n)
return 1.0;
double res = 1.0;
for (int i = 1; i <= r; ++i) {
res *= (n - r + i);
res /= i;
}
return res;
};
return choose(K, k) * choose(N - K, n - k) / choose(N, n);
}
void DeckListStatisticsAnalyzer::clearData()
{
manaBaseMap.clear();
manaCurveMap.clear();
manaDevotionMap.clear();
devotionPipCount.clear();
devotionCardCount.clear();
productionPipCount.clear();
productionCardCount.clear();
manaCurveByType.clear();
manaCurveBySubtype.clear();
manaCurveByColor.clear();
manaCurveByPower.clear();
manaCurveByToughness.clear();
manaCurveCardsByType.clear();
manaCurveCardsBySubtype.clear();
manaCurveCardsByColor.clear();
manaCurveCardsByPower.clear();
manaCurveCardsByToughness.clear();
typeCount.clear();
subtypeCount.clear();
colorCount.clear();
rarityCount.clear();
manaValueCount.clear();
}

View file

@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig
bool computeManaBase = true; bool computeManaBase = true;
bool computeManaCurve = true; bool computeManaCurve = true;
bool computeDevotion = true; bool computeDevotion = true;
bool computeCategories = true;
bool computeCurveBreakdowns = true;
bool computeProbabilities = true;
}; };
class DeckListStatisticsAnalyzer : public QObject class DeckListStatisticsAnalyzer : public QObject
@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject
public: public:
explicit DeckListStatisticsAnalyzer(QObject *parent, explicit DeckListStatisticsAnalyzer(QObject *parent,
DeckListModel *model, DeckListModel *model,
DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig()); DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig());
void update(); void analyze();
[[nodiscard]] const QHash<QString, int> &getManaBase() const [[nodiscard]] const QHash<QString, int> &getManaBase() const
{ {
@ -40,6 +43,96 @@ public:
return manaDevotionMap; return manaDevotionMap;
} }
const QHash<QString, int> &getDevotionPipCount() const
{
return devotionPipCount;
}
const QHash<QString, int> &getDevotionCardCount() const
{
return devotionCardCount;
}
const QHash<QString, int> &getProductionPipCount() const
{
return productionPipCount;
}
const QHash<QString, int> &getProductionCardCount() const
{
return productionCardCount;
}
const QHash<QString, int> &getTypeCount() const
{
return typeCount;
}
const QHash<QString, int> &getSubtypeCount() const
{
return subtypeCount;
}
const QHash<QString, int> &getColorCount() const
{
return colorCount;
}
const QHash<QString, int> &getRarityCount() const
{
return rarityCount;
}
const QHash<int, int> &getManaValueCount() const
{
return manaValueCount;
}
const QHash<QString, QHash<int, int>> &getManaCurveByType() const
{
return manaCurveByType;
}
const QHash<QString, QHash<int, int>> &getManaCurveBySubtype() const
{
return manaCurveBySubtype;
}
const QHash<QString, QHash<int, int>> &getManaCurveByColor() const
{
return manaCurveByColor;
}
const QHash<QString, QHash<int, int>> &getManaCurveByPower() const
{
return manaCurveByPower;
}
const QHash<QString, QHash<int, int>> &getManaCurveByToughness() const
{
return manaCurveByToughness;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByType() const
{
return manaCurveCardsByType;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsBySubtype() const
{
return manaCurveCardsBySubtype;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByColor() const
{
return manaCurveCardsByColor;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByPower() const
{
return manaCurveCardsByPower;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByToughness() const
{
return manaCurveCardsByToughness;
}
DeckListModel *getModel() const
{
return model;
}
signals: signals:
void statsUpdated(); void statsUpdated();
@ -47,14 +140,42 @@ private:
DeckListModel *model; DeckListModel *model;
DeckListStatisticsAnalyzerConfig config; DeckListStatisticsAnalyzerConfig config;
// Internal result containers
QHash<QString, int> manaBaseMap; QHash<QString, int> manaBaseMap;
std::unordered_map<int, int> manaCurveMap; std::unordered_map<int, int> manaCurveMap;
std::unordered_map<char, int> manaDevotionMap; std::unordered_map<char, int> manaDevotionMap;
// Internal helper functions QHash<QString, int> devotionPipCount; // W/U/B/R/G total symbols
QHash<QString, int> devotionCardCount; // how many cards provide devotion
QHash<QString, int> productionPipCount; // mana produced by cards
QHash<QString, int> productionCardCount; // number of producers
QHash<QString, int> typeCount;
QHash<QString, int> subtypeCount;
QHash<QString, int> colorCount;
QHash<QString, int> rarityCount;
QHash<int, int> manaValueCount;
QHash<QString, QHash<int, int>> manaCurveByType;
QHash<QString, QHash<int, int>> manaCurveBySubtype;
QHash<QString, QHash<int, int>> manaCurveByColor;
QHash<QString, QHash<int, int>> manaCurveByPower;
QHash<QString, QHash<int, int>> manaCurveByToughness;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByType;
QHash<QString, QHash<int, QStringList>> manaCurveCardsBySubtype;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByColor;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByPower;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByToughness;
// Not storing card info — only numeric results.
QHash<QString, QHash<int, QHash<int, double>>> probabilityExact;
QHash<QString, QHash<int, QHash<int, double>>> probabilityAtLeast;
QHash<QString, int> determineManaProduction(const QString &); QHash<QString, int> determineManaProduction(const QString &);
std::unordered_map<char, int> countManaSymbols(const QString &); std::unordered_map<char, int> countManaSymbols(const QString &);
double hypergeometric(int N, int K, int n, int k);
void clearData();
}; };
#endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H #endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H

View file

@ -1,71 +0,0 @@
#include "mana_base_widget.h"
#include "../../deck_loader/deck_loader.h"
#include "../general/display/banner_widget.h"
#include "../general/display/bar_widget.h"
#include <QHash>
#include <QRegularExpression>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer)
: QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer)
{
layout = new QVBoxLayout(this);
setLayout(layout);
bannerWidget = new BannerWidget(this, tr("Mana Base"), Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
layout->addWidget(bannerWidget);
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
layout->addWidget(barContainer);
connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaBaseWidget::updateDisplay);
retranslateUi();
}
void ManaBaseWidget::retranslateUi()
{
bannerWidget->setText(tr("Mana Base"));
}
void ManaBaseWidget::updateDisplay()
{
// Clear the layout first
QLayoutItem *item;
while ((item = barLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
auto manaBaseMap = deckStatAnalyzer->getManaBase();
int highestEntry = 0;
for (auto entry : manaBaseMap) {
if (entry > highestEntry) {
highestEntry = entry;
}
}
// Define color mapping for mana types
QHash<QString, QColor> manaColors;
manaColors.insert("W", QColor(248, 231, 185));
manaColors.insert("U", QColor(14, 104, 171));
manaColors.insert("B", QColor(21, 11, 0));
manaColors.insert("R", QColor(211, 32, 42));
manaColors.insert("G", QColor(0, 115, 62));
manaColors.insert("C", QColor(150, 150, 150));
for (auto manaColor : manaBaseMap.keys()) {
QColor barColor = manaColors.value(manaColor, Qt::gray);
BarWidget *barWidget = new BarWidget(QString(manaColor), manaBaseMap[manaColor], highestEntry, barColor, this);
barLayout->addWidget(barWidget);
}
update();
}

View file

@ -1,38 +0,0 @@
/**
* @file mana_base_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_BASE_WIDGET_H
#define MANA_BASE_WIDGET_H
#include "../general/display/banner_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaBaseWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer);
void updateDisplay();
public slots:
void retranslateUi();
private:
DeckListStatisticsAnalyzer *deckStatAnalyzer;
BannerWidget *bannerWidget;
QVBoxLayout *layout;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_BASE_WIDGET_H

View file

@ -1,68 +0,0 @@
#include "mana_curve_widget.h"
#include "../../../main.h"
#include "../../deck_loader/deck_loader.h"
#include "../general/display/banner_widget.h"
#include "../general/display/bar_widget.h"
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <unordered_map>
ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer)
: QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer)
{
layout = new QVBoxLayout(this);
setLayout(layout);
bannerWidget = new BannerWidget(this, tr("Mana Curve"), Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
layout->addWidget(bannerWidget);
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
layout->addWidget(barContainer);
connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay);
retranslateUi();
}
void ManaCurveWidget::retranslateUi()
{
bannerWidget->setText(tr("Mana Curve"));
}
void ManaCurveWidget::updateDisplay()
{
// Clear the layout first
if (barLayout != nullptr) {
QLayoutItem *item;
while ((item = barLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
}
auto manaCurveMap = deckStatAnalyzer->getManaCurve();
int highestEntry = 0;
for (const auto &entry : manaCurveMap) {
if (entry.second > highestEntry) {
highestEntry = entry.second;
}
}
// Convert unordered_map to ordered map to ensure sorting by CMC
std::map<int, int> sortedManaCurve(manaCurveMap.begin(), manaCurveMap.end());
// Add new widgets to the layout in sorted order
for (const auto &entry : sortedManaCurve) {
BarWidget *barWidget =
new BarWidget(QString::number(entry.first), entry.second, highestEntry, QColor(122, 122, 122), this);
barLayout->addWidget(barWidget);
}
update(); // Update the widget display
}

View file

@ -1,37 +0,0 @@
/**
* @file mana_curve_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_CURVE_WIDGET_H
#define MANA_CURVE_WIDGET_H
#include "../general/display/banner_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <unordered_map>
class ManaCurveWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer);
void updateDisplay();
public slots:
void retranslateUi();
private:
DeckListStatisticsAnalyzer *deckStatAnalyzer;
QVBoxLayout *layout;
BannerWidget *bannerWidget;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_CURVE_WIDGET_H

View file

@ -1,66 +0,0 @@
#include "mana_devotion_widget.h"
#include "../../deck_loader/deck_loader.h"
#include "../general/display/banner_widget.h"
#include "../general/display/bar_widget.h"
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <regex>
#include <unordered_map>
ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer)
: QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer)
{
layout = new QVBoxLayout(this);
setLayout(layout);
bannerWidget = new BannerWidget(this, tr("Mana Devotion"), Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
layout->addWidget(bannerWidget);
barLayout = new QHBoxLayout();
layout->addLayout(barLayout);
connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaDevotionWidget::updateDisplay);
retranslateUi();
}
void ManaDevotionWidget::retranslateUi()
{
bannerWidget->setText(tr("Mana Devotion"));
}
void ManaDevotionWidget::updateDisplay()
{
// Clear the layout first
QLayoutItem *item;
while ((item = barLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
auto manaDevotionMap = deckStatAnalyzer->getDevotion();
int highestEntry = 0;
for (auto entry : manaDevotionMap) {
if (highestEntry < entry.second) {
highestEntry = entry.second;
}
}
// Define color mapping for devotion bars
std::unordered_map<char, QColor> manaColors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
{'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
{'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
for (auto entry : manaDevotionMap) {
QColor barColor = manaColors.count(entry.first) ? manaColors[entry.first] : Qt::gray;
BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, highestEntry, barColor, this);
barLayout->addWidget(barWidget);
}
update(); // Update the widget display
}

View file

@ -1,37 +0,0 @@
/**
* @file mana_devotion_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_DEVOTION_WIDGET_H
#define MANA_DEVOTION_WIDGET_H
#include "../general/display/banner_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaDevotionWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer);
void updateDisplay();
public slots:
void retranslateUi();
private:
DeckListStatisticsAnalyzer *deckStatAnalyzer;
BannerWidget *bannerWidget;
QVBoxLayout *layout;
QHBoxLayout *barLayout;
};
#endif // MANA_DEVOTION_WIDGET_H

View file

@ -0,0 +1,367 @@
#include "resizable_panel.h"
#include "libcockatrice/utility/qt_utils.h"
#include <QColor>
#include <QHBoxLayout>
#include <QPixmap>
#include <QtGlobal>
ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent)
: QWidget(parent), panel(analyticsPanel), typeId(_typeId)
{
setAcceptDrops(true);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Frame for selection highlight
frame = new QFrame(this);
frame->setFrameShape(QFrame::Box);
frame->setLineWidth(2);
frame->setStyleSheet("border: none;");
auto *frameLayout = new QVBoxLayout(frame);
frameLayout->setContentsMargins(0, 0, 0, 0);
frameLayout->setSpacing(0);
// Add the analytics panel
frameLayout->addWidget(analyticsPanel);
dropIndicator = new QFrame(frame);
dropIndicator->setStyleSheet("background-color: #3daee9;");
dropIndicator->setFixedHeight(3);
dropIndicator->hide(); // hidden by default
dropIndicator->raise(); // make sure it's above children
selectionOverlay = new QFrame(frame);
selectionOverlay->setStyleSheet("background-color: rgba(61,174,233,50);"); // semi-transparent blue
selectionOverlay->hide(); // hidden by default
selectionOverlay->raise(); // make sure it is above children
selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
// Bottom bar with drag button and resize handle
auto *bottomBar = new QWidget(frame);
auto *bottomLayout = new QHBoxLayout(bottomBar);
bottomLayout->setContentsMargins(0, 0, 0, 0);
bottomLayout->setSpacing(0);
// Drag button on the left
dragButton = new QPushButton("", bottomBar);
dragButton->setFixedSize(40, 8);
dragButton->setCursor(Qt::OpenHandCursor);
dragButton->setStyleSheet("QPushButton { "
"background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a4a4a, stop:1 #3a3a3a); "
"border: none; color: #888; font-size: 10px; }"
"QPushButton:hover { background: #5a5a5a; }");
bottomLayout->addWidget(dragButton);
// Resize handle fills the rest
resizeHandle = new QWidget(bottomBar);
resizeHandle->setFixedHeight(8);
resizeHandle->setCursor(Qt::SizeVerCursor);
resizeHandle->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:0, y2:1, "
"stop:0 #3a3a3a, stop:1 #2a2a2a);");
bottomLayout->addWidget(resizeHandle, 1);
frameLayout->addWidget(bottomBar);
mainLayout->addWidget(frame);
// Set size policy
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
// Calculate initial height - use panel's size hint if available
int panelHint = analyticsPanel->sizeHint().height();
int panelMin = analyticsPanel->minimumSizeHint().height();
// Start with the larger of panel's hint and panel's minimum hint
currentHeight = qMax(panelHint + 8, panelMin + 8);
updateSizeConstraints();
// Install event filters
dragButton->installEventFilter(this);
resizeHandle->installEventFilter(this);
// Timer for auto-scroll during drag
autoScrollTimer = new QTimer(this);
autoScrollTimer->setInterval(50);
connect(autoScrollTimer, &QTimer::timeout, this, &ResizablePanel::performAutoScroll);
}
void ResizablePanel::setSelected(bool selected)
{
if (selected) {
selectionOverlay->setGeometry(0, 0, width(), height());
selectionOverlay->show();
} else {
selectionOverlay->hide();
}
}
void ResizablePanel::setHeightFromSaved(int h)
{
if (h > 0) {
currentHeight = qMax(h, getMinimumAllowedHeight());
updateSizeConstraints();
}
}
int ResizablePanel::getCurrentHeight() const
{
return currentHeight;
}
QSize ResizablePanel::sizeHint() const
{
return QSize(width(), currentHeight);
}
QSize ResizablePanel::minimumSizeHint() const
{
return QSize(0, getMinimumAllowedHeight());
}
// =====================================================================================================================
// Event Handling
// =====================================================================================================================
bool ResizablePanel::eventFilter(QObject *obj, QEvent *event)
{
if (obj == dragButton) {
if (event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
dragStartPos = mouseEvent->globalPosition().toPoint();
#else
dragStartPos = mouseEvent->globalPos();
#endif
isDraggingPanel = false;
dragButton->setCursor(Qt::ClosedHandCursor);
}
return false;
} else if (event->type() == QEvent::MouseMove) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->buttons() & Qt::LeftButton) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPoint currentPos = mouseEvent->globalPosition().toPoint();
#else
QPoint currentPos = mouseEvent->globalPos();
#endif
int distance = (currentPos - dragStartPos).manhattanLength();
if (distance >= 5 && !isDraggingPanel) {
isDraggingPanel = true;
startDrag();
return true;
}
}
return false;
} else if (event->type() == QEvent::MouseButtonRelease) {
dragButton->setCursor(Qt::OpenHandCursor);
isDraggingPanel = false;
return false;
}
}
if (obj == resizeHandle) {
if (event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
resizeStartY = mouseEvent->globalPosition().y();
#else
resizeStartY = mouseEvent->globalPos().y();
#endif
isResizing = true;
resizeStartHeight = currentHeight;
resizeHandle->grabMouse();
return true;
} else if (event->type() == QEvent::MouseMove && isResizing) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
int deltaY = mouseEvent->globalPosition().y() - resizeStartY;
#else
int deltaY = mouseEvent->globalPos().y() - resizeStartY;
#endif
int newHeight = resizeStartHeight + deltaY;
int minAllowed = getMinimumAllowedHeight();
newHeight = qMax(newHeight, minAllowed);
currentHeight = newHeight;
updateSizeConstraints();
return true;
} else if (event->type() == QEvent::MouseButtonRelease) {
isResizing = false;
resizeHandle->releaseMouse();
return true;
}
}
return QWidget::eventFilter(obj, event);
}
void ResizablePanel::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
event->acceptProposedAction();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
showDropIndicator(event->position().y());
#else
showDropIndicator(event->pos().y());
#endif
}
}
void ResizablePanel::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
event->acceptProposedAction();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
showDropIndicator(event->position().y());
lastDragPos = mapToGlobal(event->position().toPoint());
#else
showDropIndicator(event->pos().y());
lastDragPos = mapToGlobal(event->pos());
#endif
if (!autoScrollTimer->isActive()) {
autoScrollTimer->start();
}
}
}
void ResizablePanel::dragLeaveEvent(QDragLeaveEvent *event)
{
Q_UNUSED(event);
hideDropIndicator();
autoScrollTimer->stop();
}
void ResizablePanel::dropEvent(QDropEvent *event)
{
hideDropIndicator();
autoScrollTimer->stop();
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
QByteArray data = event->mimeData()->data("application/x-resizablepanel");
quintptr ptr = *reinterpret_cast<const quintptr *>(data.constData());
ResizablePanel *draggedPanel = reinterpret_cast<ResizablePanel *>(ptr);
if (draggedPanel && draggedPanel != this) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
bool insertBefore = (event->position().y() < height() / 2);
#else
bool insertBefore = (event->pos().y() < height() / 2);
#endif
emit dropRequested(draggedPanel, this, insertBefore);
event->acceptProposedAction();
}
}
}
void ResizablePanel::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (selectionOverlay->isVisible()) {
selectionOverlay->setGeometry(0, 0, width(), height());
}
if (dropIndicator->isVisible()) {
dropIndicator->setGeometry(0, dropIndicator->y(), width(), dropIndicator->height());
}
}
// =====================================================================================================================
// Private Helpers
// =====================================================================================================================
int ResizablePanel::getMinimumAllowedHeight() const
{
QSize panelMin = panel->minimumSizeHint();
int panelMinHeight = (panelMin.isValid() && panelMin.height() > 0) ? panelMin.height() : 100;
return panelMinHeight + 8;
}
void ResizablePanel::updateSizeConstraints()
{
setMinimumHeight(currentHeight);
setMaximumHeight(currentHeight);
updateGeometry();
}
void ResizablePanel::startDrag()
{
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
quintptr ptr = reinterpret_cast<quintptr>(this);
QByteArray data(reinterpret_cast<const char *>(&ptr), sizeof(ptr));
mimeData->setData("application/x-resizablepanel", data);
drag->setMimeData(mimeData);
QPixmap pixmap(width(), 40);
pixmap.fill(QColor(58, 58, 58, 200));
drag->setPixmap(pixmap);
drag->setHotSpot(QPoint(width() / 2, 20));
emit dragStarted(this);
autoScrollTimer->start();
Qt::DropAction result = drag->exec(Qt::MoveAction);
Q_UNUSED(result);
autoScrollTimer->stop();
dragButton->setCursor(Qt::OpenHandCursor);
isDraggingPanel = false;
}
void ResizablePanel::performAutoScroll()
{
QScrollArea *scrollArea = QtUtils::findParentOfType<QScrollArea>(this);
if (!scrollArea) {
return;
}
QScrollBar *scrollBar = scrollArea->verticalScrollBar();
if (!scrollBar) {
return;
}
QRect scrollRect = scrollArea->viewport()->rect();
QPoint scrollTopLeft = scrollArea->viewport()->mapToGlobal(scrollRect.topLeft());
QRect globalScrollRect(scrollTopLeft, scrollRect.size());
const int scrollMargin = 50;
int scrollSpeed = 0;
if (lastDragPos.y() < globalScrollRect.top() + scrollMargin) {
scrollSpeed = -15;
} else if (lastDragPos.y() > globalScrollRect.bottom() - scrollMargin) {
scrollSpeed = 15;
}
if (scrollSpeed != 0) {
int newValue = scrollBar->value() + scrollSpeed;
newValue = qBound(scrollBar->minimum(), newValue, scrollBar->maximum());
scrollBar->setValue(newValue);
}
}
void ResizablePanel::showDropIndicator(double y)
{
bool before = (y < height() / 2);
dropIndicator->setGeometry(0, before ? 0 : height() - 3, width(), 3);
dropIndicator->show();
}
void ResizablePanel::hideDropIndicator()
{
dropIndicator->hide();
}

View file

@ -0,0 +1,79 @@
#ifndef COCKATRICE_RESIZABLE_PANEL_H
#define COCKATRICE_RESIZABLE_PANEL_H
#include "abstract_analytics_panel_widget.h"
#include <QApplication>
#include <QDrag>
#include <QFrame>
#include <QMimeData>
#include <QMouseEvent>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
class ResizablePanel : public QWidget
{
Q_OBJECT
public:
explicit ResizablePanel(const QString &typeId,
AbstractAnalyticsPanelWidget *analyticsPanel,
QWidget *parent = nullptr);
void setSelected(bool selected);
void setHeightFromSaved(int h);
int getCurrentHeight() const;
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
QString getTypeId() const
{
return typeId;
}
AbstractAnalyticsPanelWidget *panel;
signals:
void dragStarted(ResizablePanel *panel);
void dropRequested(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
int getMinimumAllowedHeight() const;
void updateSizeConstraints();
void startDrag();
void performAutoScroll();
void showDropIndicator(double y);
void hideDropIndicator();
QString typeId;
QFrame *frame;
QFrame *selectionOverlay;
QFrame *dropIndicator;
QPushButton *dragButton;
QWidget *resizeHandle;
int currentHeight;
bool isResizing = false;
bool isDraggingPanel = false;
double resizeStartY = 0;
int resizeStartHeight = 0;
QPoint dragStartPos;
QPoint lastDragPos;
QTimer *autoScrollTimer;
};
#endif // COCKATRICE_RESIZABLE_PANEL_H

View file

@ -0,0 +1,65 @@
#include "deck_editor_card_database_dock_widget.h"
DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent)
{
setObjectName("databaseDisplayDock");
setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable);
createDatabaseDisplayDock(parent);
retranslateUi();
}
void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor)
{
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor);
auto *frame = new QVBoxLayout;
frame->setObjectName("databaseDisplayFrame");
frame->addWidget(databaseDisplayWidget);
auto *dockContents = new QWidget();
dockContents->setObjectName("databaseDisplayDockContents");
dockContents->setLayout(frame);
setWidget(dockContents);
installEventFilter(deckEditor);
connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged);
// connect signals
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, deckEditor,
&AbstractTabDeckEditor::updateCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, deckEditor,
&AbstractTabDeckEditor::actAddCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, deckEditor,
&AbstractTabDeckEditor::actAddCardToSideboard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, deckEditor,
&AbstractTabDeckEditor::actDecrementCard);
connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, deckEditor,
&AbstractTabDeckEditor::actDecrementCardFromSideboard);
}
CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const
{
return databaseDisplayWidget->databaseModel->getDatabase();
}
void DeckEditorCardDatabaseDockWidget::retranslateUi()
{
setWindowTitle(tr("Card Database"));
}
void DeckEditorCardDatabaseDockWidget::setFilterTree(FilterTree *filterTree)
{
databaseDisplayWidget->setFilterTree(filterTree);
}
void DeckEditorCardDatabaseDockWidget::clearAllDatabaseFilters()
{
databaseDisplayWidget->clearAllDatabaseFilters();
}
void DeckEditorCardDatabaseDockWidget::highlightAllSearchEdit()
{
databaseDisplayWidget->searchEdit->setSelection(0, databaseDisplayWidget->searchEdit->text().length());
}

View file

@ -0,0 +1,32 @@
#ifndef COCKATRICE_DECK_EDITOR_CARD_DATABASE_DOCK_WIDGET_H
#define COCKATRICE_DECK_EDITOR_CARD_DATABASE_DOCK_WIDGET_H
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include <QDockWidget>
class AbstractTabDeckEditor;
class CardDatabase;
class DeckEditorDatabaseDisplayWidget;
class FilterTree;
class DeckEditorCardDatabaseDockWidget : public QDockWidget
{
public:
explicit DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent);
DeckEditorDatabaseDisplayWidget *databaseDisplayWidget;
CardDatabase *getDatabase() const;
void setFilterTree(FilterTree *filterTree);
public slots:
void retranslateUi();
void clearAllDatabaseFilters();
void highlightAllSearchEdit();
private:
void createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor);
};
#endif // COCKATRICE_DECK_EDITOR_CARD_DATABASE_DOCK_WIDGET_H

View file

@ -21,10 +21,10 @@ static bool canBeCommander(const CardInfo &cardInfo)
cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive);
} }
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(AbstractTabDeckEditor *parent) DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor)
: QWidget(parent), deckEditor(parent) : QWidget(parent), deckEditor(deckEditor)
{ {
setObjectName("centralWidget"); setObjectName("databaseDisplayWidget");
centralFrame = new QVBoxLayout(this); centralFrame = new QVBoxLayout(this);
centralFrame->setObjectName("centralFrame"); centralFrame->setObjectName("centralFrame");
@ -134,13 +134,13 @@ void DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters()
void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex &current, const QModelIndex & /*previous*/) void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex &current, const QModelIndex & /*previous*/)
{ {
const QString cardName = current.sibling(current.row(), 0).data().toString();
if (!current.isValid()) { if (!current.isValid()) {
return; return;
} }
if (!current.model()->hasChildren(current.sibling(current.row(), 0))) { const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString();
if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) {
emit cardChanged(CardDatabaseManager::query()->getPreferredCard(cardName)); emit cardChanged(CardDatabaseManager::query()->getPreferredCard(cardName));
} }
} }
@ -172,7 +172,7 @@ ExactCard DeckEditorDatabaseDisplayWidget::currentCard() const
return {}; return {};
} }
const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString(); const QString cardName = currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString();
return CardDatabaseManager::query()->getPreferredCard(cardName); return CardDatabaseManager::query()->getPreferredCard(cardName);
} }

View file

@ -23,7 +23,7 @@ class DeckEditorDatabaseDisplayWidget : public QWidget
Q_OBJECT Q_OBJECT
public: public:
explicit DeckEditorDatabaseDisplayWidget(AbstractTabDeckEditor *parent); explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor);
AbstractTabDeckEditor *deckEditor; AbstractTabDeckEditor *deckEditor;
SearchLineEdit *searchEdit; SearchLineEdit *searchEdit;
CardDatabaseModel *databaseModel; CardDatabaseModel *databaseModel;

View file

@ -1,8 +1,8 @@
#include "deck_editor_deck_dock_widget.h" #include "deck_editor_deck_dock_widget.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h"
#include "deck_list_style_proxy.h" #include "deck_list_style_proxy.h"
#include "deck_state_manager.h"
#include <QComboBox> #include <QComboBox>
#include <QDockWidget> #include <QDockWidget>
@ -38,7 +38,7 @@ static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo)
} }
DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent) DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent)
: QDockWidget(parent), deckEditor(parent) : QDockWidget(parent), deckEditor(parent), deckStateManager(parent->deckStateManager)
{ {
setObjectName("deckDock"); setObjectName("deckDock");
@ -52,19 +52,21 @@ DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent
void DeckEditorDeckDockWidget::createDeckDock() void DeckEditorDeckDockWidget::createDeckDock()
{ {
deckModel = new DeckListModel(this); connect(getModel(), &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash);
deckModel->setObjectName("deckModel");
connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash);
deckLoader = new DeckLoader(this);
proxy = new DeckListStyleProxy(this); proxy = new DeckListStyleProxy(this);
proxy->setSourceModel(deckModel); proxy->setSourceModel(getModel());
historyManagerWidget = new DeckListHistoryManagerWidget(deckModel, proxy, deckEditor->getHistoryManager(), this); historyManagerWidget = new DeckListHistoryManagerWidget(deckStateManager, proxy, this);
connect(historyManagerWidget, &DeckListHistoryManagerWidget::requestDisplayWidgetSync, this, connect(historyManagerWidget, &DeckListHistoryManagerWidget::requestDisplayWidgetSync, this,
&DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel);
connect(deckStateManager, &DeckStateManager::focusIndexChanged, this, &DeckEditorDeckDockWidget::setSelectedIndex);
connect(deckStateManager, &DeckStateManager::deckReplaced, this,
&DeckEditorDeckDockWidget::syncDisplayWidgetsToModel);
connect(deckStateManager, &DeckStateManager::deckReplaced, this,
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
deckView = new QTreeView(); deckView = new QTreeView();
deckView->setObjectName("deckView"); deckView->setObjectName("deckView");
deckView->setModel(proxy); deckView->setModel(proxy);
@ -76,14 +78,14 @@ void DeckEditorDeckDockWidget::createDeckDock()
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(deckView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, connect(deckView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
&DeckEditorDeckDockWidget::updateCard); &DeckEditorDeckDockWidget::updateCard);
connect(deckView, &QTreeView::doubleClicked, this, &DeckEditorDeckDockWidget::actSwapCard); connect(deckView, &QTreeView::doubleClicked, this, &DeckEditorDeckDockWidget::actSwapSelection);
deckView->setContextMenuPolicy(Qt::CustomContextMenu); deckView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(deckView, &QTreeView::customContextMenuRequested, this, &DeckEditorDeckDockWidget::decklistCustomMenu); connect(deckView, &QTreeView::customContextMenuRequested, this, &DeckEditorDeckDockWidget::decklistCustomMenu);
connect(&deckViewKeySignals, &KeySignals::onShiftS, this, &DeckEditorDeckDockWidget::actSwapCard); connect(&deckViewKeySignals, &KeySignals::onShiftS, this, &DeckEditorDeckDockWidget::actSwapSelection);
connect(&deckViewKeySignals, &KeySignals::onEnter, this, &DeckEditorDeckDockWidget::actIncrement); connect(&deckViewKeySignals, &KeySignals::onEnter, this, &DeckEditorDeckDockWidget::actIncrementSelection);
connect(&deckViewKeySignals, &KeySignals::onCtrlAltEqual, this, &DeckEditorDeckDockWidget::actIncrement); connect(&deckViewKeySignals, &KeySignals::onCtrlAltEqual, this, &DeckEditorDeckDockWidget::actIncrementSelection);
connect(&deckViewKeySignals, &KeySignals::onCtrlAltMinus, this, &DeckEditorDeckDockWidget::actDecrementSelection); connect(&deckViewKeySignals, &KeySignals::onCtrlAltMinus, this, &DeckEditorDeckDockWidget::actDecrementSelection);
connect(&deckViewKeySignals, &KeySignals::onShiftRight, this, &DeckEditorDeckDockWidget::actIncrement); connect(&deckViewKeySignals, &KeySignals::onShiftRight, this, &DeckEditorDeckDockWidget::actIncrementSelection);
connect(&deckViewKeySignals, &KeySignals::onShiftLeft, this, &DeckEditorDeckDockWidget::actDecrementSelection); connect(&deckViewKeySignals, &KeySignals::onShiftLeft, this, &DeckEditorDeckDockWidget::actDecrementSelection);
connect(&deckViewKeySignals, &KeySignals::onDelete, this, &DeckEditorDeckDockWidget::actRemoveCard); connect(&deckViewKeySignals, &KeySignals::onDelete, this, &DeckEditorDeckDockWidget::actRemoveCard);
@ -97,7 +99,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
nameDebounceTimer = new QTimer(this); nameDebounceTimer = new QTimer(this);
nameDebounceTimer->setSingleShot(true); nameDebounceTimer->setSingleShot(true);
nameDebounceTimer->setInterval(300); // debounce duration in ms nameDebounceTimer->setInterval(300); // debounce duration in ms
connect(nameDebounceTimer, &QTimer::timeout, this, [this]() { updateName(nameEdit->text()); }); connect(nameDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeName);
connect(nameEdit, &LineEditUnfocusable::textChanged, this, [this]() { connect(nameEdit, &LineEditUnfocusable::textChanged, this, [this]() {
nameDebounceTimer->start(); // restart debounce timer nameDebounceTimer->start(); // restart debounce timer
@ -141,7 +143,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
commentsDebounceTimer = new QTimer(this); commentsDebounceTimer = new QTimer(this);
commentsDebounceTimer->setSingleShot(true); commentsDebounceTimer->setSingleShot(true);
commentsDebounceTimer->setInterval(400); // longer debounce for multi-line commentsDebounceTimer->setInterval(400); // longer debounce for multi-line
connect(commentsDebounceTimer, &QTimer::timeout, this, [this]() { updateComments(); }); connect(commentsDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeComments);
connect(commentsEdit, &QTextEdit::textChanged, this, [this]() { connect(commentsEdit, &QTextEdit::textChanged, this, [this]() {
commentsDebounceTimer->start(); // restart debounce timer commentsDebounceTimer->start(); // restart debounce timer
@ -152,18 +154,21 @@ void DeckEditorDeckDockWidget::createDeckDock()
bannerCardLabel->setText(tr("Banner Card")); bannerCardLabel->setText(tr("Banner Card"));
bannerCardLabel->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); bannerCardLabel->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
bannerCardComboBox = new QComboBox(this); bannerCardComboBox = new QComboBox(this);
connect(deckModel, &DeckListModel::dataChanged, this, [this]() { connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() {
// Delay the update to avoid race conditions // Delay the update to avoid race conditions
QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox); QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox);
}); });
connect(getModel(), &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand);
connect(getModel(), &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll);
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&DeckEditorDeckDockWidget::setBannerCard); &DeckEditorDeckDockWidget::writeBannerCard);
bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags()); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {});
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager,
&DeckEditorDeckDockWidget::setTags); &DeckStateManager::setTags);
activeGroupCriteriaLabel = new QLabel(this); activeGroupCriteriaLabel = new QLabel(this);
@ -171,17 +176,12 @@ void DeckEditorDeckDockWidget::createDeckDock()
activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE); activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE);
activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST); activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST);
activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR); activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR);
connect(activeGroupCriteriaComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() { connect(activeGroupCriteriaComboBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
deckModel->setActiveGroupCriteria(static_cast<DeckListModelGroupCriteria::Type>( &DeckEditorDeckDockWidget::applyActiveGroupCriteria);
activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckView->expandAll();
deckView->expandAll();
});
aIncrement = new QAction(QString(), this); aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment")); aIncrement->setIcon(QPixmap("theme:icons/increment"));
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
auto *tbIncrement = new QToolButton(this); auto *tbIncrement = new QToolButton(this);
tbIncrement->setDefaultAction(aIncrement); tbIncrement->setDefaultAction(aIncrement);
@ -199,7 +199,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
aSwapCard = new QAction(QString(), this); aSwapCard = new QAction(QString(), this);
aSwapCard->setIcon(QPixmap("theme:icons/swap")); aSwapCard->setIcon(QPixmap("theme:icons/swap"));
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapCard); connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
auto *tbSwapCard = new QToolButton(this); auto *tbSwapCard = new QToolButton(this);
tbSwapCard->setDefaultAction(aSwapCard); tbSwapCard->setDefaultAction(aSwapCard);
@ -283,21 +283,20 @@ void DeckEditorDeckDockWidget::createDeckDock()
void DeckEditorDeckDockWidget::initializeFormats() void DeckEditorDeckDockWidget::initializeFormats()
{ {
QMap<QString, int> allFormats = CardDatabaseManager::query()->getAllFormatsWithCount(); QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys();
formatComboBox->clear(); // Remove "Loading Database..." formatComboBox->clear(); // Remove "Loading Database..."
formatComboBox->setEnabled(true); formatComboBox->setEnabled(true);
// Populate with formats // Populate with formats
formatComboBox->addItem("", ""); formatComboBox->addItem("", "");
for (auto it = allFormats.constBegin(); it != allFormats.constEnd(); ++it) { for (auto formatName : allFormats) {
QString displayText = QString("%1").arg(it.key()); formatComboBox->addItem(formatName, formatName); // store the raw key in itemData
formatComboBox->addItem(displayText, it.key()); // store the raw key in itemData
} }
if (!deckModel->getDeckList()->getGameFormat().isEmpty()) { QString format = deckStateManager->getMetadata().gameFormat;
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat()); if (!format.isEmpty()) {
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat())); formatComboBox->setCurrentIndex(formatComboBox->findData(format));
} else { } else {
// Ensure no selection is visible initially // Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1); formatComboBox->setCurrentIndex(-1);
@ -306,11 +305,10 @@ void DeckEditorDeckDockWidget::initializeFormats()
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) { connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
if (index >= 0) { if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString(); QString formatKey = formatComboBox->itemData(index).toString();
deckModel->setActiveFormat(formatKey); deckStateManager->setFormat(formatKey);
} else { } else {
deckModel->setActiveFormat(QString()); // clear format if deselected deckStateManager->setFormat(""); // clear format if deselected
} }
emit deckModified();
}); });
} }
@ -319,17 +317,17 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
QModelIndex current = deckView->selectionModel()->currentIndex(); QModelIndex current = deckView->selectionModel()->currentIndex();
if (!current.isValid()) if (!current.isValid())
return {}; return {};
const QString cardName = current.sibling(current.row(), 1).data().toString(); const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
const QString cardProviderID = current.sibling(current.row(), 4).data().toString(); const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
const QModelIndex gparent = current.parent().parent(); const QModelIndex gparent = current.parent().parent();
if (!gparent.isValid()) { if (!gparent.isValid()) {
return {}; return {};
} }
const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString(); const QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
if (!current.model()->hasChildren(current.sibling(current.row(), 0))) { if (!current.model()->hasChildren(current.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT))) {
if (ExactCard selectedCard = CardDatabaseManager::query()->getCard({cardName, cardProviderID})) { if (ExactCard selectedCard = CardDatabaseManager::query()->getCard({cardName, cardProviderID})) {
return selectedCard; return selectedCard;
} }
@ -340,43 +338,37 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*&current*/, const QModelIndex & /*previous*/) void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*&current*/, const QModelIndex & /*previous*/)
{ {
if (ExactCard card = getCurrentCard()) { if (ExactCard card = getCurrentCard()) {
emit cardChanged(card); emit selectedCardChanged(card);
} }
} }
void DeckEditorDeckDockWidget::updateName(const QString &name) /**
* @brief Writes the contents of the name textBox to the DeckStateManager
*/
void DeckEditorDeckDockWidget::writeName()
{ {
emit requestDeckHistorySave( QString name = nameEdit->text();
QString(tr("Rename deck to \"%1\" from \"%2\"")).arg(name).arg(deckLoader->getDeck().deckList.getName())); deckStateManager->setName(name);
deckModel->getDeckList()->setName(name);
deckEditor->setModified(name.isEmpty());
emit nameChanged();
emit deckModified();
} }
void DeckEditorDeckDockWidget::updateComments() /**
* @brief Writes the contents of the comments textBox to the DeckStateManager
*/
void DeckEditorDeckDockWidget::writeComments()
{ {
emit requestDeckHistorySave(tr("Updated comments (was %1 chars, now %2 chars)") QString comments = commentsEdit->toPlainText();
.arg(deckLoader->getDeck().deckList.getComments().size()) deckStateManager->setComments(comments);
.arg(commentsEdit->toPlainText().size()));
deckModel->getDeckList()->setComments(commentsEdit->toPlainText());
deckEditor->setModified(commentsEdit->toPlainText().isEmpty());
emit commentsChanged();
emit deckModified();
} }
void DeckEditorDeckDockWidget::updateHash() void DeckEditorDeckDockWidget::updateHash()
{ {
hashLabel->setText(deckModel->getDeckList()->getDeckHash()); hashLabel->setText(deckStateManager->getDeckHash());
emit hashChanged();
emit deckModified();
} }
void DeckEditorDeckDockWidget::updateBannerCardComboBox() void DeckEditorDeckDockWidget::updateBannerCardComboBox()
{ {
// Store current banner card identity // Store current banner card identity
CardRef wanted = deckModel->getDeckList()->getBannerCard(); CardRef wanted = deckStateManager->getMetadata().bannerCard;
// Block signals temporarily // Block signals temporarily
bool wasBlocked = bannerCardComboBox->blockSignals(true); bool wasBlocked = bannerCardComboBox->blockSignals(true);
@ -386,15 +378,15 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
// Collect unique (name, providerId) pairs // Collect unique (name, providerId) pairs
QSet<QPair<QString, QString>> bannerCardSet; QSet<QPair<QString, QString>> bannerCardSet;
QList<const DecklistCardNode *> cardsInDeck = deckModel->getDeckList()->getCardNodes(); QList<CardRef> cardsInDeck = getModel()->getCardRefs();
for (auto currentCard : cardsInDeck) { for (auto cardRef : cardsInDeck) {
if (!CardDatabaseManager::query()->getCard(currentCard->toCardRef())) { if (!CardDatabaseManager::query()->getCard(cardRef)) {
continue; continue;
} }
// Insert one entry per distinct card, ignore copies // Insert one entry per distinct card, ignore copies
bannerCardSet.insert({currentCard->getName(), currentCard->getCardProviderId()}); bannerCardSet.insert({cardRef.name, cardRef.providerId});
} }
// Convert to sorted list // Convert to sorted list
@ -415,7 +407,6 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
// Handle results // Handle results
if (restoreIndex != -1) { if (restoreIndex != -1) {
bannerCardComboBox->setCurrentIndex(restoreIndex); bannerCardComboBox->setCurrentIndex(restoreIndex);
syncDeckListBannerCardWithComboBox();
} else { } else {
// Add a placeholder "-" and set it as the current selection // Add a placeholder "-" and set it as the current selection
bannerCardComboBox->insertItem(0, "-"); bannerCardComboBox->insertItem(0, "-");
@ -426,25 +417,21 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
bannerCardComboBox->blockSignals(wasBlocked); bannerCardComboBox->blockSignals(wasBlocked);
} }
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) /**
* @brief Writes the selected bannerCard to the DeckStateManager
*/
void DeckEditorDeckDockWidget::writeBannerCard(int index)
{ {
emit requestDeckHistorySave(tr("Banner card changed")); auto [name, id] = bannerCardComboBox->itemData(index).value<QPair<QString, QString>>();
syncDeckListBannerCardWithComboBox(); CardRef bannerCard = {name, id};
deckEditor->setModified(true); deckStateManager->setBannerCard(bannerCard);
emit deckModified();
} }
void DeckEditorDeckDockWidget::setTags(const QStringList &tags) void DeckEditorDeckDockWidget::applyActiveGroupCriteria()
{ {
deckModel->getDeckList()->setTags(tags); getModel()->setActiveGroupCriteria(
deckEditor->setModified(true); static_cast<DeckListModelGroupCriteria::Type>(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
emit deckModified(); getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
}
void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox()
{
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
deckModel->getDeckList()->setBannerCard({name, id});
} }
void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible) void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible)
@ -460,7 +447,7 @@ void DeckEditorDeckDockWidget::updateShowTagsWidget(const bool visible)
void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
{ {
if (deckModel->getDeckList()->getBannerCard().name == "") { if (deckStateManager->getMetadata().bannerCard.name == "") {
if (bannerCardComboBox->findText("-") != -1) { if (bannerCardComboBox->findText("-") != -1) {
bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText("-")); bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText("-"));
} else { } else {
@ -468,36 +455,29 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
bannerCardComboBox->setCurrentIndex(0); bannerCardComboBox->setCurrentIndex(0);
} }
} else { } else {
bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().name); bannerCardComboBox->setCurrentText(deckStateManager->getMetadata().bannerCard.name);
} }
} }
/** void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus)
* Sets the currently active deck for this tab
* @param _deck The deck.
*/
void DeckEditorDeckDockWidget::setDeck(const LoadedDeck &_deck)
{ {
deckLoader->setDeck(_deck); deckView->clearSelection();
deckModel->setDeckList(&deckLoader->getDeck().deckList); deckView->setCurrentIndex(newCardIndex);
connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree); recursiveExpand(newCardIndex);
emit requestDeckHistoryClear(); if (!preserveWidgetFocus) {
historyManagerWidget->setDeckListModel(deckModel); deckView->setFocus(Qt::FocusReason::MouseFocusReason);
}
syncDisplayWidgetsToModel();
emit deckChanged();
} }
void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
{ {
nameEdit->blockSignals(true); nameEdit->blockSignals(true);
nameEdit->setText(deckModel->getDeckList()->getName()); nameEdit->setText(deckStateManager->getMetadata().name);
nameEdit->blockSignals(false); nameEdit->blockSignals(false);
commentsEdit->blockSignals(true); commentsEdit->blockSignals(true);
commentsEdit->setText(deckModel->getDeckList()->getComments()); commentsEdit->setText(deckStateManager->getMetadata().comments);
commentsEdit->blockSignals(false); commentsEdit->blockSignals(false);
bannerCardComboBox->blockSignals(true); bannerCardComboBox->blockSignals(true);
@ -505,95 +485,155 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
updateBannerCardComboBox(); updateBannerCardComboBox();
bannerCardComboBox->blockSignals(false); bannerCardComboBox->blockSignals(false);
updateHash(); updateHash();
sortDeckModelToDeckView();
expandAll();
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); formatComboBox->blockSignals(true);
} formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat));
formatComboBox->blockSignals(false);
void DeckEditorDeckDockWidget::sortDeckModelToDeckView() deckTagsDisplayWidget->blockSignals(true);
{ deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags);
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); deckTagsDisplayWidget->blockSignals(false);
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
deckView->expandAll();
deckView->expandAll();
emit deckChanged();
}
DeckLoader *DeckEditorDeckDockWidget::getDeckLoader()
{
return deckLoader;
}
DeckList *DeckEditorDeckDockWidget::getDeckList()
{
return deckModel->getDeckList();
} }
/** /**
* Resets the tab to the state for a blank new tab. * @brief Convenience method to get the underlying model instance from the DeckStateManager
*/ */
void DeckEditorDeckDockWidget::cleanDeck() DeckListModel *DeckEditorDeckDockWidget::getModel() const
{ {
deckModel->cleanList(); return deckStateManager->getModel();
nameEdit->setText(QString());
emit nameChanged();
commentsEdit->setText(QString());
emit commentsChanged();
hashLabel->setText(QString());
emit hashChanged();
emit deckModified();
emit deckChanged();
updateBannerCardComboBox();
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
} }
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index) void DeckEditorDeckDockWidget::selectPrevCard()
{ {
if (index.parent().isValid()) changeSelectedCard(-1);
recursiveExpand(index.parent());
deckView->expand(index);
} }
void DeckEditorDeckDockWidget::selectNextCard()
{
changeSelectedCard(1);
}
/**
* @brief Selects a card based on the change direction.
*
* @param changeBy The direction to change, -1 for previous, 1 for next.
*/
void DeckEditorDeckDockWidget::changeSelectedCard(int changeBy)
{
if (changeBy == 0) {
return;
}
// Get the current index of the selected item
auto deckViewCurrentIndex = deckView->currentIndex();
// For some reason, if the deckModel is modified but the view is not manually reselected,
// currentIndex will return an index for the underlying deckModel instead of the proxy.
// That index will return an invalid index when indexBelow/indexAbove crosses a header node,
// causing the selection to fail to move down.
/// \todo Figure out why it's happening so we can do a proper fix instead of a hacky workaround
if (deckViewCurrentIndex.model() == proxy->sourceModel()) {
deckViewCurrentIndex = proxy->mapFromSource(deckViewCurrentIndex);
}
auto nextIndex = deckViewCurrentIndex.siblingAtRow(deckViewCurrentIndex.row() + changeBy);
if (!nextIndex.isValid()) {
nextIndex = deckViewCurrentIndex;
// Increment to the next valid index, skipping header rows
AbstractDecklistNode *node;
do {
if (changeBy > 0) {
nextIndex = deckView->indexBelow(nextIndex);
} else {
nextIndex = deckView->indexAbove(nextIndex);
}
node = static_cast<AbstractDecklistNode *>(nextIndex.internalPointer());
} while (node && node->isDeckHeader());
}
if (nextIndex.isValid()) {
deckView->setCurrentIndex(nextIndex);
deckView->setFocus(Qt::FocusReason::MouseFocusReason);
}
}
/**
* @brief Expands all parents of the given index.
* @param sourceIndex The index to expand (model source index)
*/
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &sourceIndex)
{
auto index = proxy->mapFromSource(sourceIndex);
while (index.parent().isValid()) {
index = index.parent();
deckView->expand(index);
}
}
/**
* @brief Fully expands all levels of the deck view
*/
void DeckEditorDeckDockWidget::expandAll() void DeckEditorDeckDockWidget::expandAll()
{ {
deckView->expandAll(); deckView->expandRecursively(deckView->rootIndex());
deckView->expandAll();
} }
/** /**
* Gets the index of all the currently selected card nodes in the decklist table. * Gets the source index of all the currently selected card nodes in the decklist table.
* The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them. * The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them.
* *
* @return A model index list containing all selected card nodes * @return A list containing the source indices of all selected card nodes
*/ */
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() const
{ {
auto selectedRows = deckView->selectionModel()->selectedRows(); auto selectedRows = deckView->selectionModel()->selectedRows();
const auto notLeafNode = [this](const QModelIndex &index) { const auto mapToSource = [this](const QModelIndex &index) { return proxy->mapToSource(index); };
return deckModel->hasChildren(proxy->mapToSource(index)); std::transform(selectedRows.begin(), selectedRows.end(), selectedRows.begin(), mapToSource);
};
const auto notLeafNode = [this](const QModelIndex &sourceIndex) { return getModel()->hasChildren(sourceIndex); };
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end()); selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
std::reverse(selectedRows.begin(), selectedRows.end()); std::reverse(selectedRows.begin(), selectedRows.end());
return selectedRows; return selectedRows;
} }
void DeckEditorDeckDockWidget::actIncrement() void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &zoneName)
{ {
auto selectedRows = getSelectedCardNodes(); if (!card) {
return;
}
for (const auto &index : selectedRows) { deckStateManager->addCard(card, zoneName);
offsetCountAtIndex(index, 1); }
void DeckEditorDeckDockWidget::actIncrementSelection()
{
auto selectedRows = getSelectedCardNodeSourceIndices();
for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(sourceIndex, true);
} }
} }
void DeckEditorDeckDockWidget::actSwapCard() void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString &zoneName)
{ {
auto selectedRows = getSelectedCardNodes(); QString providerId = card.getPrinting().getUuid();
QString collectorNumber = card.getPrinting().getProperty("num");
QModelIndex foundCard = getModel()->findCard(card.getName(), zoneName, providerId, collectorNumber);
if (!foundCard.isValid()) {
foundCard = getModel()->findCard(card.getName(), zoneName);
}
deckStateManager->swapCardAtIndex(foundCard);
}
void DeckEditorDeckDockWidget::actSwapSelection()
{
auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@ -601,52 +641,15 @@ void DeckEditorDeckDockWidget::actSwapCard()
deckView->setSelectionMode(QAbstractItemView::SingleSelection); deckView->setSelectionMode(QAbstractItemView::SingleSelection);
} }
bool isModified = false; for (const auto &sourceIndex : selectedRows) {
for (const auto &currentIndex : selectedRows) { deckStateManager->swapCardAtIndex(sourceIndex);
if (swapCard(currentIndex)) {
isModified = true;
}
} }
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
if (isModified) {
emit deckModified();
}
update(); update();
} }
/**
* Swaps the card at the index between the maindeck and sideboard
*
* @param currentIndex The index to swap.
* @return True if the swap was successful
*/
bool DeckEditorDeckDockWidget::swapCard(const QModelIndex &currentIndex)
{
if (!currentIndex.isValid())
return false;
const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString();
const QString cardProviderID = currentIndex.sibling(currentIndex.row(), 4).data().toString();
const QModelIndex gparent = currentIndex.parent().parent();
if (!gparent.isValid())
return false;
const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString();
offsetCountAtIndex(currentIndex, -1);
const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
ExactCard card = CardDatabaseManager::query()->getCard({cardName, cardProviderID});
QModelIndex newCardIndex = card ? deckModel->addCard(card, otherZoneName)
// Third argument (true) says create the card no matter what, even if not in DB
: deckModel->addPreferredPrintingCard(cardName, otherZoneName, true);
recursiveExpand(proxy->mapToSource(newCardIndex));
return true;
}
void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString zoneName) void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString zoneName)
{ {
if (!card) if (!card)
@ -654,22 +657,12 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z
if (card.getInfo().getIsToken()) if (card.getInfo().getIsToken())
zoneName = DECK_ZONE_TOKENS; zoneName = DECK_ZONE_TOKENS;
QString providerId = card.getPrinting().getUuid(); deckStateManager->decrementCard(card, zoneName);
QString collectorNumber = card.getPrinting().getProperty("num");
QModelIndex idx = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber);
if (!idx.isValid()) {
return;
}
deckView->clearSelection();
deckView->setCurrentIndex(proxy->mapToSource(idx));
offsetCountAtIndex(idx, -1);
} }
void DeckEditorDeckDockWidget::actDecrementSelection() void DeckEditorDeckDockWidget::actDecrementSelection()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@ -677,8 +670,8 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection); deckView->setSelectionMode(QAbstractItemView::SingleSelection);
} }
for (const auto &index : selectedRows) { for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(index, -1); offsetCountAtIndex(sourceIndex, false);
} }
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@ -686,7 +679,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
void DeckEditorDeckDockWidget::actRemoveCard() void DeckEditorDeckDockWidget::actRemoveCard()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@ -694,58 +687,29 @@ void DeckEditorDeckDockWidget::actRemoveCard()
deckView->setSelectionMode(QAbstractItemView::SingleSelection); deckView->setSelectionMode(QAbstractItemView::SingleSelection);
} }
bool isModified = false; for (const auto &sourceIndex : selectedRows) {
for (const auto &index : selectedRows) { deckStateManager->removeCardAtIndex(sourceIndex);
if (!index.isValid() || deckModel->hasChildren(index)) {
continue;
}
QModelIndex sourceIndex = proxy->mapToSource(index);
QString cardName = sourceIndex.sibling(sourceIndex.row(), 1).data().toString();
emit requestDeckHistorySave(QString(tr("Removed \"%1\" (all copies)")).arg(cardName));
deckModel->removeRow(sourceIndex.row(), sourceIndex.parent());
isModified = true;
} }
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
if (isModified) {
emit deckModified();
}
} }
void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int offset) /**
* @brief Increments or decrements the amount of the card node at the index by 1.
* @param idx The source index
* @param isIncrement If true, increments the count. If false, decrements the count
*/
void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement)
{ {
if (!idx.isValid() || deckModel->hasChildren(idx)) { if (!idx.isValid() || getModel()->hasChildren(idx)) {
return; return;
} }
QModelIndex sourceIndex = proxy->mapToSource(idx); if (isIncrement) {
deckStateManager->incrementCountAtIndex(idx);
const QModelIndex numberIndex = sourceIndex.sibling(sourceIndex.row(), 0);
const QModelIndex nameIndex = sourceIndex.sibling(sourceIndex.row(), 1);
const QString cardName = deckModel->data(nameIndex, Qt::EditRole).toString();
const int count = deckModel->data(numberIndex, Qt::EditRole).toInt();
const int new_count = count + offset;
const auto reason =
QString(tr("%1 %2 × \"%3\" (%4)"))
.arg(offset > 0 ? tr("Added") : tr("Removed"))
.arg(qAbs(offset))
.arg(cardName)
.arg(deckModel->data(sourceIndex.sibling(sourceIndex.row(), 4), Qt::DisplayRole).toString());
emit requestDeckHistorySave(reason);
if (new_count <= 0) {
deckModel->removeRow(sourceIndex.row(), sourceIndex.parent());
} else { } else {
deckModel->setData(numberIndex, new_count, Qt::EditRole); deckStateManager->decrementCountAtIndex(idx);
} }
emit deckModified();
} }
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point) void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)

View file

@ -28,22 +28,14 @@ class DeckEditorDeckDockWidget : public QDockWidget
Q_OBJECT Q_OBJECT
public: public:
explicit DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent); explicit DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent);
DeckLoader *deckLoader;
DeckListStyleProxy *proxy; DeckListStyleProxy *proxy;
DeckListModel *deckModel;
QTreeView *deckView; QTreeView *deckView;
QComboBox *bannerCardComboBox; QComboBox *bannerCardComboBox;
void createDeckDock(); void createDeckDock();
ExactCard getCurrentCard(); ExactCard getCurrentCard();
void retranslateUi(); void retranslateUi();
QString getDeckName()
{
return nameEdit->text();
}
QString getSimpleDeckName()
{
return nameEdit->text().simplified();
}
QComboBox *getGroupByComboBox() QComboBox *getGroupByComboBox()
{ {
return activeGroupCriteriaComboBox; return activeGroupCriteriaComboBox;
@ -55,35 +47,26 @@ public:
} }
public slots: public slots:
void cleanDeck(); void selectPrevCard();
void selectNextCard();
void updateBannerCardComboBox(); void updateBannerCardComboBox();
void setDeck(const LoadedDeck &_deck);
void syncDisplayWidgetsToModel(); void syncDisplayWidgetsToModel();
void sortDeckModelToDeckView(); void actAddCard(const ExactCard &card, const QString &zoneName);
DeckLoader *getDeckLoader(); void actIncrementSelection();
DeckList *getDeckList();
void actIncrement();
bool swapCard(const QModelIndex &idx);
void actDecrementCard(const ExactCard &card, QString zoneName); void actDecrementCard(const ExactCard &card, QString zoneName);
void actDecrementSelection(); void actDecrementSelection();
void actSwapCard(); void actSwapCard(const ExactCard &card, const QString &zoneName);
void actSwapSelection();
void actRemoveCard(); void actRemoveCard();
void offsetCountAtIndex(const QModelIndex &idx, int offset);
void initializeFormats(); void initializeFormats();
void expandAll();
signals: signals:
void nameChanged(); void selectedCardChanged(const ExactCard &card);
void commentsChanged();
void hashChanged();
void deckChanged();
void deckModified();
void requestDeckHistorySave(const QString &modificationReason);
void requestDeckHistoryClear();
void cardChanged(const ExactCard &_card);
private: private:
AbstractTabDeckEditor *deckEditor; AbstractTabDeckEditor *deckEditor;
DeckStateManager *deckStateManager;
DeckListHistoryManagerWidget *historyManagerWidget; DeckListHistoryManagerWidget *historyManagerWidget;
KeySignals deckViewKeySignals; KeySignals deckViewKeySignals;
QLabel *nameLabel; QLabel *nameLabel;
@ -106,22 +89,26 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
void recursiveExpand(const QModelIndex &index); DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodes() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
private slots: private slots:
void decklistCustomMenu(QPoint point); void decklistCustomMenu(QPoint point);
void updateCard(QModelIndex, const QModelIndex &current); void updateCard(QModelIndex, const QModelIndex &current);
void updateName(const QString &name); void writeName();
void updateComments(); void writeComments();
void setBannerCard(int); void writeBannerCard(int);
void setTags(const QStringList &tags); void applyActiveGroupCriteria();
void syncDeckListBannerCardWithComboBox(); void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus);
void updateHash(); void updateHash();
void refreshShortcuts(); void refreshShortcuts();
void updateShowBannerCardComboBox(bool visible); void updateShowBannerCardComboBox(bool visible);
void updateShowTagsWidget(bool visible); void updateShowTagsWidget(bool visible);
void syncBannerCardComboBoxSelectionWithDeck(); void syncBannerCardComboBoxSelectionWithDeck();
void changeSelectedCard(int changeBy);
void recursiveExpand(const QModelIndex &parent);
void expandAll();
}; };
#endif // DECK_EDITOR_DECK_DOCK_WIDGET_H #endif // DECK_EDITOR_DECK_DOCK_WIDGET_H

View file

@ -33,6 +33,10 @@ void DeckEditorPrintingSelectorDockWidget::createPrintingSelectorDock()
installEventFilter(deckEditor); installEventFilter(deckEditor);
connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged); connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged);
connect(printingSelector, &PrintingSelector::prevCardRequested, deckEditor->getDeckDockWidget(),
&DeckEditorDeckDockWidget::selectPrevCard);
connect(printingSelector, &PrintingSelector::nextCardRequested, deckEditor->getDeckDockWidget(),
&DeckEditorDeckDockWidget::selectNextCard);
} }
void DeckEditorPrintingSelectorDockWidget::retranslateUi() void DeckEditorPrintingSelectorDockWidget::retranslateUi()

View file

@ -1,10 +1,11 @@
#include "deck_list_history_manager_widget.h" #include "deck_list_history_manager_widget.h"
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckListModel, #include "deck_state_manager.h"
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
DeckListStyleProxy *_styleProxy, DeckListStyleProxy *_styleProxy,
DeckListHistoryManager *manager,
QWidget *parent) QWidget *parent)
: QWidget(parent), deckListModel(_deckListModel), styleProxy(_styleProxy), historyManager(manager) : QWidget(parent), deckStateManager(_deckStateManager), styleProxy(_styleProxy)
{ {
layout = new QHBoxLayout(this); layout = new QHBoxLayout(this);
@ -43,8 +44,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckL
connect(historyList, &QListWidget::itemClicked, this, &DeckListHistoryManagerWidget::onListClicked); connect(historyList, &QListWidget::itemClicked, this, &DeckListHistoryManagerWidget::onListClicked);
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, connect(deckStateManager, &DeckStateManager::historyChanged, this, &DeckListHistoryManagerWidget::refreshList);
&DeckListHistoryManagerWidget::refreshList);
refreshList(); refreshList();
retranslateUi(); retranslateUi();
@ -58,15 +58,12 @@ void DeckListHistoryManagerWidget::retranslateUi()
historyLabel->setText(tr("Click on an entry to revert to that point in the history.")); historyLabel->setText(tr("Click on an entry to revert to that point in the history."));
} }
void DeckListHistoryManagerWidget::setDeckListModel(DeckListModel *_deckListModel)
{
deckListModel = _deckListModel;
}
void DeckListHistoryManagerWidget::refreshList() void DeckListHistoryManagerWidget::refreshList()
{ {
historyList->clear(); historyList->clear();
DeckListHistoryManager *historyManager = deckStateManager->getHistoryManager();
// Fill redo section first (oldest redo at top, newest redo closest to divider) // Fill redo section first (oldest redo at top, newest redo closest to divider)
const auto redoStack = historyManager->getRedoStack(); const auto redoStack = historyManager->getRedoStack();
for (int i = 0; i < redoStack.size(); ++i) { // iterate forward for (int i = 0; i < redoStack.size(); ++i) { // iterate forward
@ -98,36 +95,7 @@ void DeckListHistoryManagerWidget::refreshList()
redoButton->setEnabled(historyManager->canRedo()); redoButton->setEnabled(historyManager->canRedo());
} }
void DeckListHistoryManagerWidget::doUndo() void DeckListHistoryManagerWidget::onListClicked(const QListWidgetItem *item)
{
if (!historyManager->canUndo()) {
return;
}
historyManager->undo(deckListModel->getDeckList());
deckListModel->rebuildTree();
emit deckListModel->layoutChanged();
emit requestDisplayWidgetSync();
refreshList();
}
void DeckListHistoryManagerWidget::doRedo()
{
if (!historyManager->canRedo()) {
return;
}
historyManager->redo(deckListModel->getDeckList());
deckListModel->rebuildTree();
emit deckListModel->layoutChanged();
emit requestDisplayWidgetSync();
refreshList();
}
void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item)
{ {
// Ignore non-selectable items (like divider) // Ignore non-selectable items (like divider)
if (!(item->flags() & Qt::ItemIsSelectable)) { if (!(item->flags() & Qt::ItemIsSelectable)) {
@ -138,23 +106,24 @@ void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item)
int index = item->data(Qt::UserRole + 1).toInt(); int index = item->data(Qt::UserRole + 1).toInt();
if (mode == "redo") { if (mode == "redo") {
const auto redoStack = historyManager->getRedoStack(); const auto redoStack = deckStateManager->getHistoryManager()->getRedoStack();
int steps = redoStack.size() - index; int steps = redoStack.size() - index;
for (int i = 0; i < steps; ++i) { deckStateManager->redo(steps);
historyManager->redo(deckListModel->getDeckList());
}
} else if (mode == "undo") { } else if (mode == "undo") {
const auto undoStack = historyManager->getUndoStack(); const auto undoStack = deckStateManager->getHistoryManager()->getUndoStack();
int steps = undoStack.size() - 1 - index; int steps = undoStack.size() - index;
for (int i = 0; i < steps + 1; ++i) { deckStateManager->undo(steps);
historyManager->undo(deckListModel->getDeckList());
}
} }
deckListModel->rebuildTree();
emit deckListModel->layoutChanged();
emit requestDisplayWidgetSync();
refreshList(); refreshList();
} }
void DeckListHistoryManagerWidget::doUndo()
{
deckStateManager->undo();
}
void DeckListHistoryManagerWidget::doRedo()
{
deckStateManager->redo();
}

View file

@ -14,6 +14,8 @@
#include <libcockatrice/deck_list/deck_list_history_manager.h> #include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/models/deck_list/deck_list_model.h> #include <libcockatrice/models/deck_list/deck_list_model.h>
class DeckStateManager;
class DeckListHistoryManagerWidget : public QWidget class DeckListHistoryManagerWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -25,22 +27,19 @@ public slots:
void retranslateUi(); void retranslateUi();
public: public:
explicit DeckListHistoryManagerWidget(DeckListModel *deckListModel, explicit DeckListHistoryManagerWidget(DeckStateManager *deckStateManager,
DeckListStyleProxy *styleProxy, DeckListStyleProxy *styleProxy,
DeckListHistoryManager *manager,
QWidget *parent = nullptr); QWidget *parent = nullptr);
void setDeckListModel(DeckListModel *_deckListModel);
private slots: private slots:
void refreshList(); void refreshList();
void onListClicked(QListWidgetItem *item); void onListClicked(const QListWidgetItem *item);
void doUndo(); void doUndo();
void doRedo(); void doRedo();
private: private:
DeckListModel *deckListModel; DeckStateManager *deckStateManager;
DeckListStyleProxy *styleProxy; DeckListStyleProxy *styleProxy;
DeckListHistoryManager *historyManager;
QHBoxLayout *layout; QHBoxLayout *layout;
QAction *aUndo; QAction *aUndo;

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