mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2025-12-16 01:20:43 -08:00
Compare commits
22 Commits
oracle-mem
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64bb5355ff | ||
|
|
1198db8891 | ||
|
|
9471adb4f7 | ||
|
|
b29909bdbe | ||
|
|
589e9a15a6 | ||
|
|
c218a66bcd | ||
|
|
8485bbe575 | ||
|
|
5d9d7d3aa5 | ||
|
|
ccdda39e78 | ||
|
|
2e2682aad4 | ||
|
|
a390c8ada7 | ||
|
|
da70344547 | ||
|
|
2b690f8c87 | ||
|
|
c8b419888a | ||
|
|
d3302d521f | ||
|
|
5c1bb27d5c | ||
|
|
dde36183ce | ||
|
|
7c7f2dd8d5 | ||
|
|
edb0a954e2 | ||
|
|
0a239712dd | ||
|
|
95c3434205 | ||
|
|
f0be6972cc |
4
.github/workflows/desktop-build.yml
vendored
4
.github/workflows/desktop-build.yml
vendored
@@ -166,7 +166,7 @@ jobs:
|
||||
|
||||
- name: Restore compiler cache (ccache)
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
with:
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
|
||||
- name: Save compiler cache (ccache)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{env.CACHE}}
|
||||
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||
|
||||
2
.github/workflows/translations-pull.yml
vendored
2
.github/workflows/translations-pull.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Create pull request
|
||||
if: github.event_name != 'pull_request'
|
||||
id: create_pr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
add-paths: |
|
||||
cockatrice/translations/*.ts
|
||||
|
||||
2
.github/workflows/translations-push.yml
vendored
2
.github/workflows/translations-push.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Create pull request
|
||||
if: github.event_name != 'pull_request'
|
||||
id: create_pr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
add-paths: |
|
||||
cockatrice/cockatrice_en@source.ts
|
||||
|
||||
11
README.md
11
README.md
@@ -46,7 +46,8 @@ Latest <kbd>beta</kbd> version:
|
||||
|
||||
- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice
|
||||
- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice
|
||||
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official webpage of the Cockatrice project
|
||||
- [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
|
||||
|
||||
|
||||
# Community Resources [](https://discord.gg/3Z9yzmA)
|
||||
@@ -54,6 +55,7 @@ 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.
|
||||
- [Official Website](https://cockatrice.github.io)
|
||||
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki)
|
||||
- [Official Code Documentation](https://cockatrice.github.io/docs)
|
||||
- [Official Discord](https://discord.gg/3Z9yzmA)
|
||||
- [reddit r/Cockatrice](https://reddit.com/r/cockatrice)
|
||||
|
||||
@@ -75,10 +77,15 @@ This tag is used for issues that we are looking for somebody to pick up. Often t
|
||||
For both tags, we're willing to provide help to contributors in showing them where and how they can make changes, as well as code reviews for submitted changes.<br>
|
||||
We'll happily advice on how best to implement a feature, or we can show you where the codebase is doing something similar before you get too far along - put a note on an issue you want to discuss more on!
|
||||
|
||||
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
|
||||
|
||||
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
|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>Kudos to our amazing contributors ❤️</b></summary>
|
||||
<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" />
|
||||
|
||||
@@ -19,7 +19,10 @@ set(cockatrice_SOURCES
|
||||
src/client/settings/card_counter_settings.cpp
|
||||
src/client/settings/shortcut_treeview.cpp
|
||||
src/client/settings/shortcuts_settings.cpp
|
||||
src/interface/deck_loader/card_node_function.cpp
|
||||
src/interface/deck_loader/deck_file_format.cpp
|
||||
src/interface/deck_loader/deck_loader.cpp
|
||||
src/interface/deck_loader/loaded_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_connect.cpp
|
||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
|
||||
src/interface/widgets/dialogs/dlg_create_game.cpp
|
||||
@@ -199,6 +202,7 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
#*.info = true
|
||||
#*.warning = true
|
||||
#*.critical = true
|
||||
#*.fatal = true
|
||||
|
||||
# Uncomment a rule to see debug level logs for that category,
|
||||
# or set <category> = false to disable logging
|
||||
|
||||
@@ -15,15 +15,18 @@ searches are case insensitive.
|
||||
<dd>[n:red n:deck n:wins](#n:red n:deck n:wins) <small>(Any deck with a name containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[n:"red deck wins"](#n:%22red deck wins%22) <small>(Any deck with a name containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt><u>F</u>ile Name:</dt>
|
||||
<dd>[f:aggro](#f:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
|
||||
<dd>[f:red f:deck f:wins](#f:red f:deck f:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[f:"red deck wins"](#f:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
|
||||
<dt><u>F</u>ile <u>N</u>ame:</dt>
|
||||
<dd>[fn:aggro](#fn:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
|
||||
<dd>[fn:red fn:deck fn:wins](#fn:red fn:deck fn:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[fn:"red deck wins"](#fn:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt>Relative <u>P</u>ath (starting from the deck folder):</dt>
|
||||
<dd>[p:aggro](#p:aggro) <small>(Any deck that has "aggro" somewhere in its relative path)</small></dd>
|
||||
<dd>[p:edh/](#p:edh/) <small>(Any deck with "edh/" in its relative path, A.K.A. decks in the "edh" folder)</small></dd>
|
||||
|
||||
<dt><u>F</u>ormat:</dt>
|
||||
<dd>[f:standard](#f:standard) <small>(Any deck with format set to standard)</small></dd>
|
||||
|
||||
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
|
||||
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
|
||||
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
|
||||
|
||||
@@ -350,11 +350,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;fill:url(#linearGradient3);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.77952756;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -321,11 +321,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 46.656521,12.167234 18.055171,18.054184 a 6.6081919,6.6078288 0 0 1 -0.126303,9.352065 6.6804126,6.6800456 0 0 1 -8.233169,1.011048 l -7.944268,7.943843 6.463762,6.445343 a 6.9331851,6.9328042 0 0 1 5.741536,2.022073 l 28.057729,28.092294 a 6.9962797,6.9958953 0 0 1 -9.894222,9.893685 L 50.719018,66.907526 A 7.0595711,7.0591833 0 0 1 49.18433,59.270613 l -5.741527,-5.741238 -7.944298,7.943843 A 6.716523,6.7161541 0 0 1 25.134866,69.832263 L 7.079684,51.778091 a 6.716523,6.7161541 0 0 1 8.39566,-10.345064 L 36.31101,20.59853 a 6.716523,6.7161541 0 0 1 10.345612,-8.431329 z"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -340,11 +340,11 @@
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
id="left" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -8,6 +8,7 @@
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <version_string.h>
|
||||
|
||||
DeckStatsInterface::DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent)
|
||||
: QObject(parent), cardDatabase(_cardDatabase)
|
||||
@@ -62,6 +63,7 @@ void DeckStatsInterface::analyzeDeck(DeckList *deck)
|
||||
|
||||
QNetworkRequest request(QUrl("https://deckstats.net/index.php"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
manager->post(request, data);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <version_string.h>
|
||||
|
||||
TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent)
|
||||
: QObject(parent), cardDatabase(_cardDatabase)
|
||||
@@ -87,6 +88,7 @@ void TappedOutInterface::analyzeDeck(DeckList *deck)
|
||||
|
||||
QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
manager->post(request, data);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#ifndef INTERFACE_JSON_DECK_PARSER_H
|
||||
#define INTERFACE_JSON_DECK_PARSER_H
|
||||
|
||||
#include "../../../interface/deck_loader/card_node_function.h"
|
||||
#include "../../../interface/deck_loader/deck_loader.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
@@ -48,7 +50,7 @@ public:
|
||||
}
|
||||
|
||||
loader->getDeckList()->loadFromStream_Plain(outStream, false);
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
|
||||
loader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
|
||||
return loader;
|
||||
}
|
||||
@@ -95,7 +97,7 @@ public:
|
||||
}
|
||||
|
||||
loader->getDeckList()->loadFromStream_Plain(outStream, false);
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
|
||||
loader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
|
||||
QJsonObject commandersObj = obj.value("commanders").toObject();
|
||||
if (!commandersObj.isEmpty()) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QtConcurrent>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <version_string.h>
|
||||
|
||||
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
|
||||
#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
|
||||
@@ -39,7 +40,9 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
|
||||
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
||||
{
|
||||
auto *nam = new QNetworkAccessManager(this);
|
||||
QNetworkReply *reply = nam->get(QNetworkRequest(url));
|
||||
auto request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
QNetworkReply *reply = nam->get(request);
|
||||
|
||||
if (saveResults) {
|
||||
// This will write out to the file (used for spoiler.xml)
|
||||
|
||||
@@ -13,7 +13,7 @@ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
|
||||
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
|
||||
|
||||
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
|
||||
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / GenericQuery
|
||||
|
||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||
|
||||
@@ -22,8 +22,9 @@ CardSearch <- '[[' CardFilterString ']]'
|
||||
CardFilterString <- (!']]'.)*
|
||||
|
||||
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
|
||||
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
|
||||
FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String
|
||||
PathQuery <- [Pp] 'ath'? [:] String
|
||||
FormatQuery <- [Ff] 'ormat'? [:] String
|
||||
|
||||
GenericQuery <- String
|
||||
|
||||
@@ -118,12 +119,13 @@ static void setupParserRules()
|
||||
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
|
||||
int count = 0;
|
||||
deck->deckLoader->getDeckList()->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
|
||||
auto cardNodes = deck->deckLoader->getDeckList()->getCardNodes();
|
||||
for (auto node : cardNodes) {
|
||||
auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName());
|
||||
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
||||
count += node->getNumber();
|
||||
}
|
||||
});
|
||||
}
|
||||
return numberMatcher(count);
|
||||
};
|
||||
};
|
||||
@@ -156,6 +158,14 @@ static void setupParserRules()
|
||||
};
|
||||
};
|
||||
|
||||
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto format = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto gameFormat = deck->deckLoader->getDeckList()->getGameFormat();
|
||||
return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0;
|
||||
};
|
||||
};
|
||||
|
||||
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
|
||||
@@ -343,10 +343,7 @@ void DeckViewScene::rebuildTree()
|
||||
if (!deck)
|
||||
return;
|
||||
|
||||
InnerDecklistNode *listRoot = deck->getRoot();
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
|
||||
for (auto *currentZone : deck->getZoneNodes()) {
|
||||
DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0);
|
||||
if (!container) {
|
||||
container = new DeckViewCardContainer(currentZone->getName());
|
||||
|
||||
@@ -259,7 +259,7 @@ void DeckViewContainer::loadLocalDeck()
|
||||
|
||||
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
||||
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath);
|
||||
DeckLoader deck(this);
|
||||
|
||||
bool success = deck.loadFromFile(filePath, fmt, true);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../player_actions.h"
|
||||
#include "player_menu.h"
|
||||
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
UtilityMenu::UtilityMenu(Player *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
|
||||
@@ -65,15 +66,13 @@ void UtilityMenu::populatePredefinedTokensMenu()
|
||||
return;
|
||||
}
|
||||
|
||||
InnerDecklistNode *tokenZone =
|
||||
dynamic_cast<InnerDecklistNode *>(_deck->getDeckList()->getRoot()->findChild(DECK_ZONE_TOKENS));
|
||||
auto tokenCardNodes = _deck->getDeckList()->getCardNodes({DECK_ZONE_TOKENS});
|
||||
|
||||
if (tokenZone) {
|
||||
if (!tokenZone->empty())
|
||||
setEnabled(true);
|
||||
if (!tokenCardNodes.isEmpty()) {
|
||||
setEnabled(true);
|
||||
|
||||
for (int i = 0; i < tokenZone->size(); ++i) {
|
||||
const QString tokenName = tokenZone->at(i)->getName();
|
||||
for (int i = 0; i < tokenCardNodes.size(); ++i) {
|
||||
const QString tokenName = tokenCardNodes[i]->getName();
|
||||
predefinedTokens.append(tokenName);
|
||||
QAction *a = addAction(tokenName);
|
||||
if (i < 10) {
|
||||
|
||||
@@ -263,7 +263,7 @@ void Player::deleteCard(CardItem *card)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Does a player need a DeckLoader?
|
||||
//! \todo Does a player need a DeckLoader?
|
||||
void Player::setDeck(DeckLoader &_deck)
|
||||
{
|
||||
deck = new DeckLoader(this, _deck.getDeckList());
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <utility>
|
||||
#include <version_string.h>
|
||||
|
||||
static constexpr int MAX_REQUESTS_PER_SEC = 10;
|
||||
|
||||
@@ -86,6 +87,7 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||
}
|
||||
|
||||
QNetworkRequest req(url);
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
if (!picDownload) {
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||
}
|
||||
|
||||
40
cockatrice/src/interface/deck_loader/card_node_function.cpp
Normal file
40
cockatrice/src/interface/deck_loader/card_node_function.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "card_node_function.h"
|
||||
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
|
||||
void CardNodeFunction::SetProviderIdToPreferred::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
|
||||
QString providerId = preferredPrinting.getUuid();
|
||||
QString setShortName = preferredPrinting.getSet()->getShortName();
|
||||
QString collectorNumber = preferredPrinting.getProperty("num");
|
||||
|
||||
card->setCardProviderId(providerId);
|
||||
card->setCardCollectorNumber(collectorNumber);
|
||||
card->setCardSetShortName(setShortName);
|
||||
}
|
||||
|
||||
void CardNodeFunction::ClearPrintingData::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
card->setCardSetShortName(nullptr);
|
||||
card->setCardCollectorNumber(nullptr);
|
||||
card->setCardProviderId(nullptr);
|
||||
}
|
||||
|
||||
void CardNodeFunction::ResolveProviderId::operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
// Retrieve the providerId based on setName and collectorNumber
|
||||
QString providerId =
|
||||
CardDatabaseManager::getInstance()
|
||||
->query()
|
||||
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
|
||||
.getUuid();
|
||||
|
||||
// Set the providerId on the card
|
||||
card->setCardProviderId(providerId);
|
||||
}
|
||||
39
cockatrice/src/interface/deck_loader/card_node_function.h
Normal file
39
cockatrice/src/interface/deck_loader/card_node_function.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef COCKATRICE_DECK_FUNCTION_H
|
||||
#define COCKATRICE_DECK_FUNCTION_H
|
||||
|
||||
class DecklistCardNode;
|
||||
class InnerDecklistNode;
|
||||
|
||||
/**
|
||||
* Functions to be used with DeckList::forEachCard
|
||||
*/
|
||||
namespace CardNodeFunction
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Sets the providerId of the card to the preferred printing.
|
||||
*/
|
||||
struct SetProviderIdToPreferred
|
||||
{
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Clears all fields on the card related to the printing
|
||||
*/
|
||||
struct ClearPrintingData
|
||||
{
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Sets the providerId of the card based on its set name and collector number.
|
||||
*/
|
||||
struct ResolveProviderId
|
||||
{
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const;
|
||||
};
|
||||
|
||||
} // namespace CardNodeFunction
|
||||
|
||||
#endif // COCKATRICE_DECK_FUNCTION_H
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "deck_file_format.h"
|
||||
|
||||
DeckFileFormat::Format DeckFileFormat::getFormatFromName(const QString &fileName)
|
||||
{
|
||||
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
|
||||
return Cockatrice;
|
||||
}
|
||||
return PlainText;
|
||||
}
|
||||
36
cockatrice/src/interface/deck_loader/deck_file_format.h
Normal file
36
cockatrice/src/interface/deck_loader/deck_file_format.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef COCKATRICE_DECK_FILE_FORMAT_H
|
||||
#define COCKATRICE_DECK_FILE_FORMAT_H
|
||||
#include <QString>
|
||||
|
||||
namespace DeckFileFormat
|
||||
{
|
||||
|
||||
/**
|
||||
* The deck file formats that Cockatrice supports.
|
||||
*/
|
||||
enum Format
|
||||
{
|
||||
/**
|
||||
* Plaintext deck files, a format that is intended to be widely supported among different programs.
|
||||
* This format does not support Cockatrice specific features such as banner cards or tags.
|
||||
*/
|
||||
PlainText,
|
||||
|
||||
/**
|
||||
* This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
|
||||
* Stored as .cod files.
|
||||
*/
|
||||
Cockatrice
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines what deck file format the given filename corresponds to.
|
||||
*
|
||||
* @param fileName The filename
|
||||
* @return The deck format
|
||||
*/
|
||||
Format getFormatFromName(const QString &fileName);
|
||||
|
||||
} // namespace DeckFileFormat
|
||||
|
||||
#endif // COCKATRICE_DECK_FILE_FORMAT_H
|
||||
@@ -33,7 +33,7 @@ DeckLoader::DeckLoader(QObject *parent, DeckList *_deckList) : QObject(parent),
|
||||
{
|
||||
}
|
||||
|
||||
bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest)
|
||||
bool DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
@@ -42,17 +42,17 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
|
||||
|
||||
bool result = false;
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
case DeckFileFormat::PlainText:
|
||||
result = deckList->loadFromFile_Plain(&file);
|
||||
break;
|
||||
case CockatriceFormat: {
|
||||
case DeckFileFormat::Cockatrice: {
|
||||
result = deckList->loadFromFile_Native(&file);
|
||||
qCInfo(DeckLoaderLog) << "Loaded from" << fileName << "-" << result;
|
||||
if (!result) {
|
||||
qCInfo(DeckLoaderLog) << "Retrying as plain format";
|
||||
file.seek(0);
|
||||
result = deckList->loadFromFile_Plain(&file);
|
||||
fmt = PlainTextFormat;
|
||||
fmt = DeckFileFormat::PlainText;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool user
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest)
|
||||
bool DeckLoader::loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
|
||||
{
|
||||
auto *watcher = new QFutureWatcher<bool>(this);
|
||||
|
||||
@@ -106,9 +106,9 @@ bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool
|
||||
}
|
||||
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
case DeckFileFormat::PlainText:
|
||||
return deckList->loadFromFile_Plain(&file);
|
||||
case CockatriceFormat: {
|
||||
case DeckFileFormat::Cockatrice: {
|
||||
bool result = false;
|
||||
result = deckList->loadFromFile_Native(&file);
|
||||
if (!result) {
|
||||
@@ -140,7 +140,7 @@ bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
|
||||
bool DeckLoader::saveToFile(const QString &fileName, DeckFileFormat::Format fmt)
|
||||
{
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
@@ -149,10 +149,10 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
|
||||
|
||||
bool result = false;
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
case DeckFileFormat::PlainText:
|
||||
result = deckList->saveToFile_Plain(&file);
|
||||
break;
|
||||
case CockatriceFormat:
|
||||
case DeckFileFormat::Cockatrice:
|
||||
result = deckList->saveToFile_Native(&file);
|
||||
qCInfo(DeckLoaderLog) << "Saving to " << fileName << "-" << result;
|
||||
break;
|
||||
@@ -172,7 +172,7 @@ bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt)
|
||||
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt)
|
||||
{
|
||||
QFileInfo fileInfo(fileName);
|
||||
if (!fileInfo.exists()) {
|
||||
@@ -193,10 +193,10 @@ bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat f
|
||||
|
||||
// Perform file modifications
|
||||
switch (fmt) {
|
||||
case PlainTextFormat:
|
||||
case DeckFileFormat::PlainText:
|
||||
result = deckList->saveToFile_Plain(&file);
|
||||
break;
|
||||
case CockatriceFormat:
|
||||
case DeckFileFormat::Cockatrice:
|
||||
deckList->setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
|
||||
result = deckList->saveToFile_Native(&file);
|
||||
break;
|
||||
@@ -269,6 +269,20 @@ static QString toDecklistExportString(const DecklistCardNode *card)
|
||||
return cardString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all cards in the list to their decklist export string and joins them into one string
|
||||
*/
|
||||
static QString toDecklistExportString(const QList<const DecklistCardNode *> &cardNodes)
|
||||
{
|
||||
QString result;
|
||||
|
||||
for (auto cardNode : cardNodes) {
|
||||
result += toDecklistExportString(cardNode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export deck to decklist function, called to format the deck in a way to be sent to a server
|
||||
*
|
||||
@@ -279,29 +293,11 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi
|
||||
{
|
||||
// Add the base url
|
||||
QString deckString = "https://" + getDomainForWebsite(website) + "/?";
|
||||
// Create two strings to pass to function
|
||||
QString mainBoardCards, sideBoardCards;
|
||||
|
||||
// Set up the function to call
|
||||
auto formatDeckListForExport = [&mainBoardCards, &sideBoardCards](const auto *node, const auto *card) {
|
||||
// Get the card name
|
||||
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
if (!dbCard || dbCard->getIsToken()) {
|
||||
// If it's a token, we don't care about the card.
|
||||
return;
|
||||
}
|
||||
// export all cards in zone
|
||||
QString mainBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_MAIN}));
|
||||
QString sideBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_SIDE}));
|
||||
|
||||
// Check if it's a sideboard card.
|
||||
if (node->getName() == DECK_ZONE_SIDE) {
|
||||
sideBoardCards += toDecklistExportString(card);
|
||||
} else {
|
||||
// If it's a mainboard card, do the same thing, but for the mainboard card string
|
||||
mainBoardCards += toDecklistExportString(card);
|
||||
}
|
||||
};
|
||||
|
||||
// call our struct function for each card in the deck
|
||||
deckList->forEachCard(formatDeckListForExport);
|
||||
// Remove the extra return at the end of the last cards
|
||||
mainBoardCards.chop(3);
|
||||
sideBoardCards.chop(3);
|
||||
@@ -316,112 +312,6 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi
|
||||
return deckString;
|
||||
}
|
||||
|
||||
// This struct is here to support the forEachCard function call, defined in decklist.
|
||||
// It requires a function to be called for each card, and it will set the providerId to the preferred printing.
|
||||
struct SetProviderIdToPreferred
|
||||
{
|
||||
// Main operator for struct, allowing the foreachcard to work.
|
||||
SetProviderIdToPreferred()
|
||||
{
|
||||
}
|
||||
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
|
||||
QString providerId = preferredPrinting.getUuid();
|
||||
QString setShortName = preferredPrinting.getSet()->getShortName();
|
||||
QString collectorNumber = preferredPrinting.getProperty("num");
|
||||
|
||||
card->setCardProviderId(providerId);
|
||||
card->setCardCollectorNumber(collectorNumber);
|
||||
card->setCardSetShortName(setShortName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This function iterates through each card in the decklist and sets the providerId
|
||||
* on each card based on its set name and collector number.
|
||||
*
|
||||
* @param deckList The decklist to modify
|
||||
*/
|
||||
void DeckLoader::setProviderIdToPreferredPrinting(const DeckList *deckList)
|
||||
{
|
||||
// Set up the struct to call.
|
||||
SetProviderIdToPreferred setProviderIdToPreferred;
|
||||
|
||||
// Call the forEachCard method for each card in the deck
|
||||
deckList->forEachCard(setProviderIdToPreferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the providerId on each card in the decklist based on its set name and collector number.
|
||||
*
|
||||
* @param deckList The decklist to modify
|
||||
*/
|
||||
void DeckLoader::resolveSetNameAndNumberToProviderID(const DeckList *deckList)
|
||||
{
|
||||
auto setProviderId = [](const auto node, const auto card) {
|
||||
Q_UNUSED(node);
|
||||
// Retrieve the providerId based on setName and collectorNumber
|
||||
QString providerId =
|
||||
CardDatabaseManager::getInstance()
|
||||
->query()
|
||||
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
|
||||
.getUuid();
|
||||
|
||||
// Set the providerId on the card
|
||||
card->setCardProviderId(providerId);
|
||||
};
|
||||
|
||||
deckList->forEachCard(setProviderId);
|
||||
}
|
||||
|
||||
// This struct is here to support the forEachCard function call, defined in decklist.
|
||||
// It requires a function to be called for each card, and it will set the providerId.
|
||||
struct ClearSetNameNumberAndProviderId
|
||||
{
|
||||
// Main operator for struct, allowing the foreachcard to work.
|
||||
ClearSetNameNumberAndProviderId()
|
||||
{
|
||||
}
|
||||
|
||||
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
|
||||
{
|
||||
Q_UNUSED(node);
|
||||
// Set the providerId on the card
|
||||
card->setCardSetShortName(nullptr);
|
||||
card->setCardCollectorNumber(nullptr);
|
||||
card->setCardProviderId(nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the set name and numbers on each card in the decklist.
|
||||
*
|
||||
* @param deckList The decklist to modify
|
||||
*/
|
||||
void DeckLoader::clearSetNamesAndNumbers(const DeckList *deckList)
|
||||
{
|
||||
auto clearSetNameAndNumber = [](const auto node, auto card) {
|
||||
Q_UNUSED(node)
|
||||
// Set the providerId on the card
|
||||
card->setCardSetShortName(nullptr);
|
||||
card->setCardCollectorNumber(nullptr);
|
||||
card->setCardProviderId(nullptr);
|
||||
};
|
||||
|
||||
deckList->forEachCard(clearSetNameAndNumber);
|
||||
}
|
||||
|
||||
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
|
||||
{
|
||||
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
|
||||
return CockatriceFormat;
|
||||
}
|
||||
return PlainTextFormat;
|
||||
}
|
||||
|
||||
void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, bool addSetNameAndNumber)
|
||||
{
|
||||
QString buffer;
|
||||
@@ -441,9 +331,7 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out,
|
||||
}
|
||||
|
||||
// loop zones
|
||||
for (int i = 0; i < deckList->getRoot()->size(); i++) {
|
||||
const auto *zoneNode = dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i));
|
||||
|
||||
for (auto zoneNode : deckList->getZoneNodes()) {
|
||||
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
|
||||
|
||||
// end of zone
|
||||
@@ -564,12 +452,12 @@ bool DeckLoader::convertToCockatriceFormat(QString fileName)
|
||||
bool result = false;
|
||||
|
||||
// Perform file modifications based on the detected format
|
||||
switch (getFormatFromName(fileName)) {
|
||||
case PlainTextFormat:
|
||||
switch (DeckFileFormat::getFormatFromName(fileName)) {
|
||||
case DeckFileFormat::PlainText:
|
||||
// Save in Cockatrice's native format
|
||||
result = deckList->saveToFile_Native(&file);
|
||||
break;
|
||||
case CockatriceFormat:
|
||||
case DeckFileFormat::Cockatrice:
|
||||
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
|
||||
result = true;
|
||||
break;
|
||||
@@ -590,37 +478,14 @@ bool DeckLoader::convertToCockatriceFormat(QString fileName)
|
||||
}
|
||||
lastLoadInfo = {
|
||||
.fileName = newFileName,
|
||||
.fileFormat = CockatriceFormat,
|
||||
.fileFormat = DeckFileFormat::Cockatrice,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString DeckLoader::getCardZoneFromName(const QString &cardName, QString currentZoneName)
|
||||
{
|
||||
CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
|
||||
if (card && card->getIsToken()) {
|
||||
return DECK_ZONE_TOKENS;
|
||||
}
|
||||
|
||||
return currentZoneName;
|
||||
}
|
||||
|
||||
QString DeckLoader::getCompleteCardName(const QString &cardName)
|
||||
{
|
||||
if (CardDatabaseManager::getInstance()) {
|
||||
ExactCard temp = CardDatabaseManager::query()->guessCard({cardName});
|
||||
if (temp) {
|
||||
return temp.getName();
|
||||
}
|
||||
}
|
||||
|
||||
return cardName;
|
||||
}
|
||||
|
||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node)
|
||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
||||
{
|
||||
const int totalColumns = 2;
|
||||
|
||||
@@ -703,12 +568,11 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList)
|
||||
cursor.insertText(deckList->getComments());
|
||||
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
||||
|
||||
for (int i = 0; i < deckList->getRoot()->size(); i++) {
|
||||
for (auto zoneNode : deckList->getZoneNodes()) {
|
||||
cursor.insertHtml("<br><img src=theme:hr.jpg>");
|
||||
// cursor.insertHtml("<hr>");
|
||||
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
||||
|
||||
printDeckListNode(&cursor, dynamic_cast<InnerDecklistNode *>(deckList->getRoot()->at(i)));
|
||||
printDeckListNode(&cursor, zoneNode);
|
||||
}
|
||||
|
||||
doc.print(printer);
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
#ifndef DECK_LOADER_H
|
||||
#define DECK_LOADER_H
|
||||
|
||||
#include "loaded_deck.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QPrinter>
|
||||
#include <QTextCursor>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader")
|
||||
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader");
|
||||
|
||||
class DeckLoader : public QObject
|
||||
class DeckLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
signals:
|
||||
@@ -22,27 +24,6 @@ signals:
|
||||
void loadFinished(bool success);
|
||||
|
||||
public:
|
||||
enum FileFormat
|
||||
{
|
||||
PlainTextFormat,
|
||||
CockatriceFormat
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Information about where the deck was loaded from.
|
||||
*
|
||||
* For local decks, the remoteDeckId field will always be -1.
|
||||
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
|
||||
*/
|
||||
struct LoadInfo
|
||||
{
|
||||
static constexpr int NON_REMOTE_ID = -1;
|
||||
|
||||
QString fileName = "";
|
||||
FileFormat fileFormat = CockatriceFormat;
|
||||
int remoteDeckId = NON_REMOTE_ID;
|
||||
};
|
||||
|
||||
/**
|
||||
* Supported file extensions for decklist files
|
||||
*/
|
||||
@@ -61,7 +42,7 @@ public:
|
||||
|
||||
private:
|
||||
DeckList *deckList;
|
||||
LoadInfo lastLoadInfo;
|
||||
LoadedDeck::LoadInfo lastLoadInfo;
|
||||
|
||||
public:
|
||||
DeckLoader(QObject *parent);
|
||||
@@ -69,35 +50,29 @@ public:
|
||||
DeckLoader(const DeckLoader &) = delete;
|
||||
DeckLoader &operator=(const DeckLoader &) = delete;
|
||||
|
||||
const LoadInfo &getLastLoadInfo() const
|
||||
const LoadedDeck::LoadInfo &getLastLoadInfo() const
|
||||
{
|
||||
return lastLoadInfo;
|
||||
}
|
||||
|
||||
void setLastLoadInfo(const LoadInfo &info)
|
||||
void setLastLoadInfo(const LoadedDeck::LoadInfo &info)
|
||||
{
|
||||
lastLoadInfo = info;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasNotBeenLoaded() const
|
||||
{
|
||||
return lastLoadInfo.fileName.isEmpty() && lastLoadInfo.remoteDeckId == LoadInfo::NON_REMOTE_ID;
|
||||
return lastLoadInfo.isEmpty();
|
||||
}
|
||||
|
||||
static void clearSetNamesAndNumbers(const DeckList *deckList);
|
||||
static FileFormat getFormatFromName(const QString &fileName);
|
||||
|
||||
bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false);
|
||||
bool loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest);
|
||||
bool loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest = false);
|
||||
bool loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest);
|
||||
bool loadFromRemote(const QString &nativeString, int remoteDeckId);
|
||||
bool saveToFile(const QString &fileName, FileFormat fmt);
|
||||
bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt);
|
||||
bool saveToFile(const QString &fileName, DeckFileFormat::Format fmt);
|
||||
bool updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt);
|
||||
|
||||
static QString exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website);
|
||||
|
||||
static void setProviderIdToPreferredPrinting(const DeckList *deckList);
|
||||
static void resolveSetNameAndNumberToProviderID(const DeckList *deckList);
|
||||
|
||||
static void saveToClipboard(const DeckList *deckList, bool addComments = true, bool addSetNameAndNumber = true);
|
||||
static bool saveToStream_Plain(QTextStream &out,
|
||||
const DeckList *deckList,
|
||||
@@ -119,7 +94,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static void printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node);
|
||||
static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node);
|
||||
static void saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList);
|
||||
|
||||
static void saveToStream_DeckZone(QTextStream &out,
|
||||
@@ -131,9 +106,6 @@ private:
|
||||
QList<DecklistCardNode *> cards,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
|
||||
[[nodiscard]] static QString getCardZoneFromName(const QString &cardName, QString currentZoneName);
|
||||
[[nodiscard]] static QString getCompleteCardName(const QString &cardName);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
11
cockatrice/src/interface/deck_loader/loaded_deck.cpp
Normal file
11
cockatrice/src/interface/deck_loader/loaded_deck.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "loaded_deck.h"
|
||||
|
||||
bool LoadedDeck::LoadInfo::isEmpty() const
|
||||
{
|
||||
return fileName.isEmpty() && remoteDeckId == NON_REMOTE_ID;
|
||||
}
|
||||
|
||||
bool LoadedDeck::isEmpty() const
|
||||
{
|
||||
return deckList.isEmpty() && lastLoadInfo.isEmpty();
|
||||
}
|
||||
39
cockatrice/src/interface/deck_loader/loaded_deck.h
Normal file
39
cockatrice/src/interface/deck_loader/loaded_deck.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef COCKATRICE_LOADED_DECK_H
|
||||
#define COCKATRICE_LOADED_DECK_H
|
||||
|
||||
#include "deck_file_format.h"
|
||||
#include "libcockatrice/deck_list/deck_list.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* @brief Represents a deck that was loaded from somewhere.
|
||||
* Contains the DeckList itself, as well as info about where it was loaded from.
|
||||
*/
|
||||
struct LoadedDeck
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Information about where the deck was loaded from.
|
||||
*
|
||||
* For local decks, the remoteDeckId field will always be -1.
|
||||
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
|
||||
*/
|
||||
struct LoadInfo
|
||||
{
|
||||
static constexpr int NON_REMOTE_ID = -1;
|
||||
|
||||
QString fileName = "";
|
||||
DeckFileFormat::Format fileFormat = DeckFileFormat::Cockatrice;
|
||||
int remoteDeckId = NON_REMOTE_ID;
|
||||
|
||||
bool isEmpty() const;
|
||||
};
|
||||
|
||||
DeckList deckList; ///< The decklist itself
|
||||
LoadInfo lastLoadInfo; ///< info about where the deck was loaded from
|
||||
|
||||
bool isEmpty() const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_LOADED_DECK_H
|
||||
@@ -124,6 +124,12 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||
quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox);
|
||||
quickSettingsWidget->addSettingsWidget(showTagsWidgetCheckBox);
|
||||
|
||||
formatLabel = new QLabel(this);
|
||||
|
||||
formatComboBox = new QComboBox(this);
|
||||
formatComboBox->addItem(tr("Loading Database..."));
|
||||
formatComboBox->setEnabled(false); // Disable until loaded
|
||||
|
||||
commentsLabel = new QLabel();
|
||||
commentsLabel->setObjectName("commentsLabel");
|
||||
commentsEdit = new QTextEdit;
|
||||
@@ -154,8 +160,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||
&DeckEditorDeckDockWidget::setBannerCard);
|
||||
bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
|
||||
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags());
|
||||
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this,
|
||||
&DeckEditorDeckDockWidget::setTags);
|
||||
|
||||
activeGroupCriteriaLabel = new QLabel(this);
|
||||
|
||||
@@ -206,13 +214,16 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||
upperLayout->addWidget(commentsLabel, 1, 0);
|
||||
upperLayout->addWidget(commentsEdit, 1, 1);
|
||||
|
||||
upperLayout->addWidget(bannerCardLabel, 2, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 2, 1);
|
||||
upperLayout->addWidget(formatLabel, 2, 0);
|
||||
upperLayout->addWidget(formatComboBox, 2, 1);
|
||||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
|
||||
upperLayout->addWidget(bannerCardLabel, 3, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 3, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
@@ -261,6 +272,46 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||
|
||||
refreshShortcuts();
|
||||
retranslateUi();
|
||||
|
||||
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
||||
&DeckEditorDeckDockWidget::initializeFormats);
|
||||
|
||||
if (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok) {
|
||||
initializeFormats();
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::initializeFormats()
|
||||
{
|
||||
QMap<QString, int> allFormats = CardDatabaseManager::query()->getAllFormatsWithCount();
|
||||
|
||||
formatComboBox->clear(); // Remove "Loading Database..."
|
||||
formatComboBox->setEnabled(true);
|
||||
|
||||
// Populate with formats
|
||||
formatComboBox->addItem("", "");
|
||||
for (auto it = allFormats.constBegin(); it != allFormats.constEnd(); ++it) {
|
||||
QString displayText = QString("%1").arg(it.key());
|
||||
formatComboBox->addItem(displayText, it.key()); // store the raw key in itemData
|
||||
}
|
||||
|
||||
if (!deckModel->getDeckList()->getGameFormat().isEmpty()) {
|
||||
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
|
||||
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
|
||||
} else {
|
||||
// Ensure no selection is visible initially
|
||||
formatComboBox->setCurrentIndex(-1);
|
||||
}
|
||||
|
||||
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
if (index >= 0) {
|
||||
QString formatKey = formatComboBox->itemData(index).toString();
|
||||
deckModel->setActiveFormat(formatKey);
|
||||
} else {
|
||||
deckModel->setActiveFormat(QString()); // clear format if deselected
|
||||
}
|
||||
emit deckModified();
|
||||
});
|
||||
}
|
||||
|
||||
ExactCard DeckEditorDeckDockWidget::getCurrentCard()
|
||||
@@ -335,7 +386,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
||||
|
||||
// Collect unique (name, providerId) pairs
|
||||
QSet<QPair<QString, QString>> bannerCardSet;
|
||||
QList<DecklistCardNode *> cardsInDeck = deckModel->getDeckList()->getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = deckModel->getDeckList()->getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
if (!CardDatabaseManager::query()->getCard(currentCard->toCardRef())) {
|
||||
@@ -383,6 +434,13 @@ void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
|
||||
emit deckModified();
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::setTags(const QStringList &tags)
|
||||
{
|
||||
deckModel->getDeckList()->setTags(tags);
|
||||
deckEditor->setModified(true);
|
||||
emit deckModified();
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox()
|
||||
{
|
||||
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
@@ -451,12 +509,18 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
|
||||
sortDeckModelToDeckView();
|
||||
expandAll();
|
||||
|
||||
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
|
||||
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::sortDeckModelToDeckView()
|
||||
{
|
||||
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
|
||||
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
|
||||
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
|
||||
deckView->expandAll();
|
||||
deckView->expandAll();
|
||||
|
||||
emit deckChanged();
|
||||
}
|
||||
|
||||
DeckLoader *DeckEditorDeckDockWidget::getDeckLoader()
|
||||
@@ -484,7 +548,7 @@ void DeckEditorDeckDockWidget::cleanDeck()
|
||||
emit deckModified();
|
||||
emit deckChanged();
|
||||
updateBannerCardComboBox();
|
||||
deckTagsDisplayWidget->setDeckList(deckModel->getDeckList());
|
||||
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)
|
||||
@@ -715,6 +779,8 @@ void DeckEditorDeckDockWidget::retranslateUi()
|
||||
showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
|
||||
commentsLabel->setText(tr("&Comments:"));
|
||||
activeGroupCriteriaLabel->setText(tr("Group by:"));
|
||||
formatLabel->setText(tr("Format:"));
|
||||
|
||||
hashLabel1->setText(tr("Hash:"));
|
||||
|
||||
aIncrement->setText(tr("&Increment number"));
|
||||
|
||||
@@ -69,6 +69,7 @@ public slots:
|
||||
void actSwapCard();
|
||||
void actRemoveCard();
|
||||
void offsetCountAtIndex(const QModelIndex &idx, int offset);
|
||||
void initializeFormats();
|
||||
void expandAll();
|
||||
|
||||
signals:
|
||||
@@ -100,6 +101,8 @@ private:
|
||||
LineEditUnfocusable *hashLabel;
|
||||
QLabel *activeGroupCriteriaLabel;
|
||||
QComboBox *activeGroupCriteriaComboBox;
|
||||
QLabel *formatLabel;
|
||||
QComboBox *formatComboBox;
|
||||
|
||||
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||
|
||||
@@ -112,6 +115,7 @@ private slots:
|
||||
void updateName(const QString &name);
|
||||
void updateComments();
|
||||
void setBannerCard(int);
|
||||
void setTags(const QStringList &tags);
|
||||
void syncDeckListBannerCardWithComboBox();
|
||||
void updateHash();
|
||||
void refreshShortcuts();
|
||||
|
||||
@@ -23,8 +23,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const
|
||||
|
||||
if (role == Qt::BackgroundRole) {
|
||||
if (isCard) {
|
||||
const bool legal =
|
||||
true; // TODO: Not implemented yet. QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool();
|
||||
const bool legal = QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool();
|
||||
int base = 255 - (index.row() % 2) * 30;
|
||||
return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3));
|
||||
} else {
|
||||
|
||||
@@ -109,7 +109,7 @@ void DlgCreateGame::sharedCtor()
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0);
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1);
|
||||
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0);
|
||||
if (room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
|
||||
if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
|
||||
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0);
|
||||
} else {
|
||||
createGameAsJudgeCheckBox->setChecked(false);
|
||||
|
||||
@@ -59,7 +59,7 @@ DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent)
|
||||
|
||||
void DlgEditPassword::actOk()
|
||||
{
|
||||
// TODO this stuff should be using qvalidators
|
||||
//! \todo this stuff should be using qvalidators
|
||||
if (newPasswordEdit->text().length() < 8) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Your password is too short."));
|
||||
return;
|
||||
|
||||
@@ -121,7 +121,7 @@ void DlgForgotPasswordReset::actOk()
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO this stuff should be using qvalidators
|
||||
//! \todo this stuff should be using qvalidators
|
||||
if (newpasswordEdit->text().length() < 8) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Your password is too short."));
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "dlg_load_deck_from_clipboard.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/card_node_function.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "dlg_settings.h"
|
||||
|
||||
@@ -82,9 +83,9 @@ bool AbstractDlgDeckTextEdit::loadIntoDeck(DeckLoader *deckLoader) const
|
||||
|
||||
if (deckLoader->getDeckList()->loadFromStream_Plain(stream, true)) {
|
||||
if (loadSetNameAndNumberCheckBox->isChecked()) {
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(deckLoader->getDeckList());
|
||||
deckLoader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
} else {
|
||||
DeckLoader::clearSetNamesAndNumbers(deckLoader->getDeckList());
|
||||
deckLoader->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <version_string.h>
|
||||
|
||||
DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
@@ -67,6 +68,7 @@ void DlgLoadDeckFromWebsite::accept()
|
||||
}
|
||||
|
||||
QNetworkRequest request(QUrl(info.fullUrl));
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
QNetworkReply *reply = nam->get(request);
|
||||
|
||||
QEventLoop loop;
|
||||
@@ -98,7 +100,7 @@ void DlgLoadDeckFromWebsite::accept()
|
||||
DeckLoader *loader = new DeckLoader(this);
|
||||
QTextStream stream(&deckText);
|
||||
loader->getDeckList()->loadFromStream_Plain(stream, false);
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(loader->getDeckList());
|
||||
loader->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
deck = loader;
|
||||
|
||||
QDialog::accept();
|
||||
|
||||
@@ -356,7 +356,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
||||
|
||||
void DlgRegister::actOk()
|
||||
{
|
||||
// TODO this stuff should be using qvalidators
|
||||
//! \todo this stuff should be using qvalidators
|
||||
if (passwordEdit->text().length() < 8) {
|
||||
QMessageBox::critical(this, tr("Registration Warning"), tr("Your password is too short."));
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "dlg_select_set_for_cards.h"
|
||||
|
||||
#include "../../deck_loader/card_node_function.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../interface/widgets/cards/card_info_picture_widget.h"
|
||||
#include "../interface/widgets/general/layout_containers/flow_widget.h"
|
||||
@@ -177,7 +178,7 @@ void DlgSelectSetForCards::actOK()
|
||||
void DlgSelectSetForCards::actClear()
|
||||
{
|
||||
emit deckAboutToBeModified(tr("Cleared all printing information."));
|
||||
DeckLoader::clearSetNamesAndNumbers(model->getDeckList());
|
||||
model->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData());
|
||||
emit deckModified();
|
||||
accept();
|
||||
}
|
||||
@@ -185,8 +186,8 @@ void DlgSelectSetForCards::actClear()
|
||||
void DlgSelectSetForCards::actSetAllToPreferred()
|
||||
{
|
||||
emit deckAboutToBeModified(tr("Set all printings to preferred."));
|
||||
DeckLoader::clearSetNamesAndNumbers(model->getDeckList());
|
||||
DeckLoader::setProviderIdToPreferredPrinting(model->getDeckList());
|
||||
model->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData());
|
||||
model->getDeckList()->forEachCard(CardNodeFunction::SetProviderIdToPreferred());
|
||||
emit deckModified();
|
||||
accept();
|
||||
}
|
||||
@@ -227,7 +228,7 @@ QMap<QString, int> DlgSelectSetForCards::getSetsForCards()
|
||||
if (!decklist)
|
||||
return setCounts;
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(currentCard->getName());
|
||||
@@ -270,7 +271,7 @@ void DlgSelectSetForCards::updateCardLists()
|
||||
if (!decklist)
|
||||
return;
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
bool found = false;
|
||||
@@ -359,7 +360,7 @@ QMap<QString, QStringList> DlgSelectSetForCards::getCardsForSets()
|
||||
if (!decklist)
|
||||
return setCards;
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(currentCard->getName());
|
||||
|
||||
@@ -1913,7 +1913,7 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||
}
|
||||
|
||||
if (!QDir(SettingsCache::instance().getDeckPath()).exists() || SettingsCache::instance().getDeckPath().isEmpty()) {
|
||||
// TODO: Prompt to create it
|
||||
//! \todo Prompt to create it
|
||||
if (QMessageBox::critical(
|
||||
this, tr("Error"),
|
||||
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
|
||||
@@ -1924,7 +1924,7 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||
}
|
||||
|
||||
if (!QDir(SettingsCache::instance().getPicsPath()).exists() || SettingsCache::instance().getPicsPath().isEmpty()) {
|
||||
// TODO: Prompt to create it
|
||||
//! \todo Prompt to create it
|
||||
if (QMessageBox::critical(this, tr("Error"),
|
||||
tr("The path to your card pictures directory is invalid. Would you like to go back "
|
||||
"and set the correct path?"),
|
||||
|
||||
@@ -23,7 +23,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
backgroundSourceDeck = new DeckLoader(this);
|
||||
|
||||
backgroundSourceDeck->loadFromFile(SettingsCache::instance().getDeckPath() + "background.cod",
|
||||
DeckLoader::CockatriceFormat, false);
|
||||
DeckFileFormat::Cockatrice, false);
|
||||
|
||||
gradientColors = extractDominantColors(background);
|
||||
|
||||
@@ -73,7 +73,7 @@ void HomeWidget::initializeBackgroundFromSource()
|
||||
break;
|
||||
case BackgroundSources::DeckFileArt:
|
||||
backgroundSourceDeck->loadFromFile(SettingsCache::instance().getDeckPath() + "background.cod",
|
||||
DeckLoader::CockatriceFormat, false);
|
||||
DeckFileFormat::Cockatrice, false);
|
||||
cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ int CardAmountWidget::countCardsInZone(const QString &deckZone)
|
||||
return -1;
|
||||
}
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes({deckZone});
|
||||
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes({deckZone});
|
||||
|
||||
int count = 0;
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
|
||||
@@ -206,7 +206,7 @@ void ChatView::appendMessage(QString message,
|
||||
defaultFormat = QTextCharFormat();
|
||||
if (!isUserMessage) {
|
||||
if (messageType == Event_RoomSay::ChatHistory) {
|
||||
defaultFormat.setForeground(Qt::gray); // FIXME : hardcoded color
|
||||
defaultFormat.setForeground(Qt::gray); //! \todo hardcoded color
|
||||
defaultFormat.setFontWeight(QFont::Light);
|
||||
defaultFormat.setFontItalic(true);
|
||||
static const QRegularExpression userNameRegex("^(\\[[^\\]]*\\]\\s)(\\S+):\\s");
|
||||
@@ -229,7 +229,7 @@ void ChatView::appendMessage(QString message,
|
||||
message.remove(0, pos.relativePosition - 2); // do not remove semicolon
|
||||
}
|
||||
} else {
|
||||
defaultFormat.setForeground(Qt::darkGreen); // FIXME : hardcoded color
|
||||
defaultFormat.setForeground(Qt::darkGreen); //! \todo hardcoded color
|
||||
defaultFormat.setFontWeight(QFont::Bold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ void AbstractTabDeckEditor::actOpenRecent(const QString &fileName)
|
||||
*/
|
||||
void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
|
||||
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(fileName);
|
||||
|
||||
auto *l = new DeckLoader(this);
|
||||
if (l->loadFromFile(fileName, fmt, true)) {
|
||||
@@ -406,7 +406,7 @@ void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLo
|
||||
bool AbstractTabDeckEditor::actSaveDeck()
|
||||
{
|
||||
DeckLoader *const deck = getDeckLoader();
|
||||
if (deck->getLastLoadInfo().remoteDeckId != DeckLoader::LoadInfo::NON_REMOTE_ID) {
|
||||
if (deck->getLastLoadInfo().remoteDeckId != LoadedDeck::LoadInfo::NON_REMOTE_ID) {
|
||||
QString deckString = deck->getDeckList()->writeToString_Native();
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Could not save remote deck"));
|
||||
@@ -452,7 +452,7 @@ bool AbstractTabDeckEditor::actSaveDeckAs()
|
||||
return false;
|
||||
|
||||
QString fileName = dialog.selectedFiles().at(0);
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
|
||||
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(fileName);
|
||||
|
||||
if (!getDeckLoader()->saveToFile(fileName, fmt)) {
|
||||
QMessageBox::critical(
|
||||
|
||||
@@ -21,16 +21,16 @@ void ArchidektApiResponseCard::fromJson(const QJsonObject &json)
|
||||
edition.fromJson(json.value("edition").toObject());
|
||||
|
||||
flavor = json.value("flavor").toString();
|
||||
// TODO but not really important
|
||||
// games = {""};
|
||||
// options = {""};
|
||||
//! \todo but not really important
|
||||
//! \todo games = {""};
|
||||
//! \todo options = {""};
|
||||
scryfallImageHash = json.value("scryfallImageHash").toString();
|
||||
oracleCard = json.value("oracleCard").toObject();
|
||||
owned = json.value("owned").toInt();
|
||||
pinnedStatus = json.value("pinnedStatus").toInt();
|
||||
rarity = json.value("rarity").toString();
|
||||
// TODO but not really important
|
||||
// globalCategories = {""};
|
||||
//! \todo but not really important
|
||||
//! \todo globalCategories = {""};
|
||||
}
|
||||
|
||||
void ArchidektApiResponseCard::debugPrint() const
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "archidekt_api_response_deck_display_widget.h"
|
||||
|
||||
#include "../../../../../deck_loader/card_node_function.h"
|
||||
#include "../../../../../deck_loader/deck_loader.h"
|
||||
#include "../../../../cards/card_info_picture_with_text_overlay_widget.h"
|
||||
#include "../../../../cards/card_size_widget.h"
|
||||
@@ -68,7 +69,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
|
||||
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
|
||||
model->getDeckList()->loadFromStream_Plain(deckStream, false);
|
||||
|
||||
DeckLoader::resolveSetNameAndNumberToProviderID(model->getDeckList());
|
||||
model->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId());
|
||||
|
||||
model->rebuildTree();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QPixmap>
|
||||
#include <QWidget>
|
||||
#include <version_string.h>
|
||||
|
||||
#define ARCHIDEKT_DEFAULT_IMAGE "https://storage.googleapis.com/topdekt-user/images/archidekt_deck_card_shadow.jpg"
|
||||
|
||||
@@ -82,6 +83,7 @@ ArchidektApiResponseDeckEntryDisplayWidget::ArchidektApiResponseDeckEntryDisplay
|
||||
imageUrl = response.getFeatured().isEmpty() ? QUrl(ARCHIDEKT_DEFAULT_IMAGE) : QUrl(response.getFeatured());
|
||||
|
||||
QNetworkRequest req(imageUrl);
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
QNetworkReply *reply = imageNetworkManager->get(req);
|
||||
|
||||
// tag the reply with "this" so we know it belongs to us later
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
|
||||
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||
#include <version_string.h>
|
||||
|
||||
TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
|
||||
{
|
||||
@@ -213,7 +214,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
|
||||
minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"}); // Exact = unset, ≥ = GTE, ≤ = LTE
|
||||
minDeckSizeLogicCombo->setCurrentIndex(1); // default GTE
|
||||
|
||||
connect(minDeckSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
|
||||
connect(minDeckSizeSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
|
||||
connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
|
||||
|
||||
// Page number
|
||||
@@ -223,7 +224,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
|
||||
pageSpin->setRange(1, 9999);
|
||||
pageSpin->setValue(1);
|
||||
|
||||
connect(pageSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
|
||||
connect(pageSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
|
||||
|
||||
// Page display
|
||||
currentPageDisplay = new QWidget(container);
|
||||
@@ -426,18 +427,21 @@ void TabArchidekt::doSearchImmediate()
|
||||
{
|
||||
QString url = buildSearchUrl();
|
||||
QNetworkRequest req{QUrl(url)};
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(req);
|
||||
}
|
||||
|
||||
void TabArchidekt::actNavigatePage(QString url)
|
||||
{
|
||||
QNetworkRequest request{QUrl(url)};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(request);
|
||||
}
|
||||
|
||||
void TabArchidekt::getTopDecks()
|
||||
{
|
||||
QNetworkRequest request{QUrl(buildSearchUrl())};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
|
||||
#include <libcockatrice/models/database/card/card_search_model.h>
|
||||
#include <version_string.h>
|
||||
|
||||
static bool canBeCommander(const CardInfoPtr &cardInfo)
|
||||
{
|
||||
@@ -166,6 +167,7 @@ void TabEdhRecMain::setCard(CardInfoPtr _cardToQuery, bool isCommander)
|
||||
}
|
||||
|
||||
QNetworkRequest request{QUrl(url)};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
networkManager->get(request);
|
||||
}
|
||||
@@ -173,6 +175,7 @@ void TabEdhRecMain::setCard(CardInfoPtr _cardToQuery, bool isCommander)
|
||||
void TabEdhRecMain::actNavigatePage(QString url)
|
||||
{
|
||||
QNetworkRequest request{QUrl("https://json.edhrec.com/pages" + url + ".json")};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
networkManager->get(request);
|
||||
}
|
||||
@@ -180,6 +183,7 @@ void TabEdhRecMain::actNavigatePage(QString url)
|
||||
void TabEdhRecMain::getTopCards()
|
||||
{
|
||||
QNetworkRequest request{QUrl("https://json.edhrec.com/pages/top/year.json")};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
networkManager->get(request);
|
||||
}
|
||||
@@ -187,6 +191,7 @@ void TabEdhRecMain::getTopCards()
|
||||
void TabEdhRecMain::getTopCommanders()
|
||||
{
|
||||
QNetworkRequest request{QUrl("https://json.edhrec.com/pages/commanders/year.json")};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
|
||||
networkManager->get(request);
|
||||
}
|
||||
@@ -194,7 +199,7 @@ void TabEdhRecMain::getTopCommanders()
|
||||
void TabEdhRecMain::getTopTags()
|
||||
{
|
||||
QNetworkRequest request{QUrl("https://json.edhrec.com/pages/tags.json")};
|
||||
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ void TabDeckStorage::actOpenLocalDeck()
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
auto deckLoader = new DeckLoader(this);
|
||||
if (!deckLoader->loadFromFile(filePath, DeckLoader::CockatriceFormat, true))
|
||||
if (!deckLoader->loadFromFile(filePath, DeckFileFormat::Cockatrice, true))
|
||||
continue;
|
||||
|
||||
emit openDeckEditor(deckLoader);
|
||||
@@ -308,7 +308,7 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa
|
||||
QFileInfo deckFileInfo(deckFile);
|
||||
|
||||
DeckLoader deck(this);
|
||||
if (!deck.loadFromFile(filePath, DeckLoader::CockatriceFormat)) {
|
||||
if (!deck.loadFromFile(filePath, DeckFileFormat::Cockatrice)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
@@ -493,7 +493,7 @@ void TabDeckStorage::downloadFinished(const Response &r,
|
||||
QString filePath = extraData.toString();
|
||||
|
||||
DeckLoader deck(this, new DeckList(QString::fromStdString(resp.deck())));
|
||||
deck.saveToFile(filePath, DeckLoader::CockatriceFormat);
|
||||
deck.saveToFile(filePath, DeckFileFormat::Cockatrice);
|
||||
}
|
||||
|
||||
void TabDeckStorage::actNewFolder()
|
||||
|
||||
@@ -25,7 +25,7 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
||||
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
|
||||
{
|
||||
auto deckLoader = new DeckLoader(this);
|
||||
if (!deckLoader->loadFromFile(filePath, DeckLoader::getFormatFromName(filePath), true)) {
|
||||
if (!deckLoader->loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
// Filter search input
|
||||
searchInput = new QLineEdit(this);
|
||||
layout->addWidget(searchInput);
|
||||
|
||||
connect(searchInput, &QLineEdit::textChanged, this, &VisualDatabaseDisplayFilterSaveLoadWidget::applySearchFilter);
|
||||
|
||||
// File list container
|
||||
fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
layout->addWidget(fileListWidget);
|
||||
|
||||
// Input for filter filename
|
||||
filenameInput = new QLineEdit(this);
|
||||
layout->addWidget(filenameInput);
|
||||
@@ -25,11 +35,12 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
|
||||
// Save button
|
||||
saveButton = new QPushButton(this);
|
||||
layout->addWidget(saveButton);
|
||||
connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter);
|
||||
// Disable save if empty
|
||||
saveButton->setEnabled(false);
|
||||
connect(filenameInput, &QLineEdit::textChanged, this,
|
||||
[this](const QString &text) { saveButton->setEnabled(!text.trimmed().isEmpty()); });
|
||||
|
||||
// File list container
|
||||
fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
layout->addWidget(fileListWidget);
|
||||
connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter);
|
||||
|
||||
refreshFilterList(); // Populate the file list on startup
|
||||
retranslateUi();
|
||||
@@ -37,6 +48,7 @@ VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWi
|
||||
|
||||
void VisualDatabaseDisplayFilterSaveLoadWidget::retranslateUi()
|
||||
{
|
||||
searchInput->setPlaceholderText(tr("Search filter..."));
|
||||
saveButton->setText(tr("Save Filter"));
|
||||
saveButton->setToolTip(tr("Save all currently applied filters to a file"));
|
||||
filenameInput->setPlaceholderText(tr("Enter filename..."));
|
||||
@@ -112,42 +124,36 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter(const QString &filena
|
||||
emit filterModel->layoutChanged();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFilterSaveLoadWidget::applySearchFilter(const QString &text)
|
||||
{
|
||||
fileListWidget->clearLayout();
|
||||
|
||||
QString filter = text.trimmed();
|
||||
QStringList filtered = allFilterFiles;
|
||||
|
||||
if (!filter.isEmpty()) {
|
||||
filtered = filtered.filter(QRegularExpression(filter, QRegularExpression::CaseInsensitiveOption));
|
||||
}
|
||||
|
||||
for (const QString &filename : filtered) {
|
||||
FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel);
|
||||
fileListWidget->addWidget(filterWidget);
|
||||
|
||||
connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter);
|
||||
|
||||
connect(filterWidget, &FilterDisplayWidget::filterDeleted, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
|
||||
{
|
||||
fileListWidget->clearLayout();
|
||||
// Clear existing widgets
|
||||
for (auto buttonPair : fileButtons) {
|
||||
buttonPair.first->deleteLater();
|
||||
buttonPair.second->deleteLater();
|
||||
}
|
||||
fileButtons.clear(); // Clear the list of buttons
|
||||
fileButtons.clear();
|
||||
|
||||
// Refresh the filter file list
|
||||
QDir dir(SettingsCache::instance().getFiltersPath());
|
||||
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
|
||||
allFilterFiles = dir.entryList({"*.json"}, QDir::Files, QDir::Name);
|
||||
|
||||
// Loop through the filter files and create widgets for them
|
||||
for (const QString &filename : filterFiles) {
|
||||
bool alreadyAdded = false;
|
||||
|
||||
// Check if the widget for this filter file already exists to avoid duplicates
|
||||
for (const auto &pair : fileButtons) {
|
||||
if (pair.first->text() == filename) {
|
||||
alreadyAdded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
// Create a new custom widget for the filter
|
||||
FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel);
|
||||
fileListWidget->addWidget(filterWidget);
|
||||
|
||||
// Connect signals to handle loading and deletion
|
||||
connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter);
|
||||
connect(filterWidget, &FilterDisplayWidget::filterDeleted, this,
|
||||
&VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList);
|
||||
}
|
||||
}
|
||||
applySearchFilter(searchInput->text());
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
|
||||
void saveFilter();
|
||||
void loadFilter(const QString &filename);
|
||||
void applySearchFilter(const QString &text);
|
||||
void refreshFilterList();
|
||||
void deleteFilter(const QString &filename, QPushButton *deleteButton);
|
||||
|
||||
@@ -37,9 +38,11 @@ private:
|
||||
FilterTreeModel *filterModel;
|
||||
|
||||
QVBoxLayout *layout;
|
||||
QLineEdit *searchInput;
|
||||
FlowWidget *fileListWidget;
|
||||
QLineEdit *filenameInput;
|
||||
QPushButton *saveButton;
|
||||
FlowWidget *fileListWidget;
|
||||
QStringList allFilterFiles;
|
||||
|
||||
QMap<QString, QPair<QPushButton *, QPushButton *>> fileButtons;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "visual_database_display_format_legality_filter_widget.h"
|
||||
|
||||
#include "../../../filters/filter_tree_model.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/filters/filter_tree.h>
|
||||
|
||||
VisualDatabaseDisplayFormatLegalityFilterWidget::VisualDatabaseDisplayFormatLegalityFilterWidget(
|
||||
QWidget *parent,
|
||||
FilterTreeModel *_filterModel)
|
||||
: QWidget(parent), filterModel(_filterModel)
|
||||
{
|
||||
allFormatsWithCount = CardDatabaseManager::query()->getAllFormatsWithCount();
|
||||
|
||||
setMaximumHeight(75);
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
||||
|
||||
layout = new QHBoxLayout(this);
|
||||
setLayout(layout);
|
||||
layout->setContentsMargins(0, 1, 0, 1);
|
||||
layout->setSpacing(1);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
layout->addWidget(flowWidget);
|
||||
|
||||
// Create the spinbox
|
||||
spinBox = new QSpinBox(this);
|
||||
spinBox->setMinimum(1);
|
||||
spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically
|
||||
spinBox->setValue(150);
|
||||
layout->addWidget(spinBox);
|
||||
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatButtonsVisibility);
|
||||
|
||||
// Create the toggle button for Exact Match/Includes mode
|
||||
toggleButton = new QPushButton(this);
|
||||
toggleButton->setCheckable(true);
|
||||
layout->addWidget(toggleButton);
|
||||
connect(toggleButton, &QPushButton::toggled, this,
|
||||
&VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode);
|
||||
connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() {
|
||||
QTimer::singleShot(100, this, &VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel);
|
||||
});
|
||||
|
||||
createFormatButtons(); // Populate buttons initially
|
||||
updateFilterMode(false); // Initialize toggle button text
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::retranslateUi()
|
||||
{
|
||||
spinBox->setToolTip(tr("Do not display formats with less than this amount of cards in the database"));
|
||||
toggleButton->setToolTip(tr("Filter mode (AND/OR/NOT conjunctions of filters)"));
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::createFormatButtons()
|
||||
{
|
||||
// Iterate through main types and create buttons
|
||||
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
|
||||
auto *button = new QPushButton(it.key(), flowWidget);
|
||||
button->setCheckable(true);
|
||||
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
|
||||
"QPushButton:checked { background-color: green; color: white; }");
|
||||
|
||||
flowWidget->addWidget(button);
|
||||
formatButtons[it.key()] = button;
|
||||
|
||||
// Connect toggle signal
|
||||
connect(button, &QPushButton::toggled, this,
|
||||
[this, mainType = it.key()](bool checked) { handleFormatToggled(mainType, checked); });
|
||||
}
|
||||
updateFormatButtonsVisibility(); // Ensure visibility is updated initially
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatButtonsVisibility()
|
||||
{
|
||||
int threshold = spinBox->value(); // Get the current spinbox value
|
||||
|
||||
// Iterate through buttons and hide/disable those below the threshold
|
||||
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
|
||||
bool visible = allFormatsWithCount[it.key()] >= threshold;
|
||||
it.value()->setVisible(visible);
|
||||
it.value()->setEnabled(visible);
|
||||
}
|
||||
}
|
||||
|
||||
int VisualDatabaseDisplayFormatLegalityFilterWidget::getMaxMainTypeCount() const
|
||||
{
|
||||
int maxCount = 1;
|
||||
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
|
||||
maxCount = qMax(maxCount, it.value());
|
||||
}
|
||||
return maxCount;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::handleFormatToggled(const QString &format, bool active)
|
||||
{
|
||||
activeFormats[format] = active;
|
||||
|
||||
if (formatButtons.contains(format)) {
|
||||
formatButtons[format]->setChecked(active);
|
||||
}
|
||||
|
||||
updateFormatFilter();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatFilter()
|
||||
{
|
||||
// Clear existing filters related to main type
|
||||
filterModel->blockSignals(true);
|
||||
filterModel->filterTree()->blockSignals(true);
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrFormat);
|
||||
|
||||
if (exactMatchMode) {
|
||||
// Exact Match: Only selected main types are allowed
|
||||
QSet<QString> selectedTypes;
|
||||
for (const auto &type : activeFormats.keys()) {
|
||||
if (activeFormats[type]) {
|
||||
selectedTypes.insert(type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedTypes.isEmpty()) {
|
||||
// Require all selected types (TypeAnd)
|
||||
for (const auto &type : selectedTypes) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrFormat));
|
||||
}
|
||||
|
||||
// Exclude any other types (TypeAndNot)
|
||||
for (const auto &type : formatButtons.keys()) {
|
||||
if (!selectedTypes.contains(type)) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrFormat));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default Includes Mode (TypeOr) - match any selected main types
|
||||
for (const auto &type : activeFormats.keys()) {
|
||||
if (activeFormats[type]) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrFormat));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterModel->blockSignals(false);
|
||||
filterModel->filterTree()->blockSignals(false);
|
||||
|
||||
emit filterModel->filterTree()->changed();
|
||||
emit filterModel->layoutChanged();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode(bool checked)
|
||||
{
|
||||
exactMatchMode = checked;
|
||||
toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
|
||||
updateFormatFilter();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
// Temporarily block signals for each button to prevent toggling while updating button states
|
||||
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
|
||||
it.value()->blockSignals(true);
|
||||
}
|
||||
|
||||
// Uncheck all buttons
|
||||
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
|
||||
it.value()->setChecked(false);
|
||||
}
|
||||
|
||||
// Get active filters for main types
|
||||
QSet<QString> activeTypes;
|
||||
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrFormat)) {
|
||||
if (filter->type() == CardFilter::Type::TypeAnd) {
|
||||
activeTypes.insert(filter->term());
|
||||
}
|
||||
}
|
||||
|
||||
// Check the buttons for active types
|
||||
for (const auto &type : activeTypes) {
|
||||
activeFormats[type] = true;
|
||||
if (formatButtons.contains(type)) {
|
||||
formatButtons[type]->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable signal emissions for each button
|
||||
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
|
||||
it.value()->blockSignals(false);
|
||||
}
|
||||
|
||||
// Update the visibility of buttons
|
||||
updateFormatButtonsVisibility();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
|
||||
#define COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
|
||||
|
||||
#include "../../../filters/filter_tree_model.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class VisualDatabaseDisplayFormatLegalityFilterWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VisualDatabaseDisplayFormatLegalityFilterWidget(QWidget *parent, FilterTreeModel *filterModel);
|
||||
void retranslateUi();
|
||||
void createFormatButtons();
|
||||
void updateFormatButtonsVisibility();
|
||||
int getMaxMainTypeCount() const;
|
||||
|
||||
void handleFormatToggled(const QString &format, bool active);
|
||||
void updateFormatFilter();
|
||||
void updateFilterMode(bool checked);
|
||||
void syncWithFilterModel();
|
||||
|
||||
private:
|
||||
FilterTreeModel *filterModel;
|
||||
QMap<QString, int> allFormatsWithCount;
|
||||
QSpinBox *spinBox;
|
||||
QHBoxLayout *layout;
|
||||
FlowWidget *flowWidget;
|
||||
QPushButton *toggleButton; // Mode switch button
|
||||
|
||||
QMap<QString, bool> activeFormats; // Track active filters
|
||||
QMap<QString, QPushButton *> formatButtons; // Store toggle buttons
|
||||
|
||||
bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes"
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
|
||||
@@ -33,7 +33,7 @@ VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWi
|
||||
spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically
|
||||
spinBox->setValue(150);
|
||||
layout->addWidget(spinBox);
|
||||
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeButtonsVisibility);
|
||||
|
||||
// Create the toggle button for Exact Match/Includes mode
|
||||
|
||||
@@ -51,7 +51,7 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
|
||||
|
||||
void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
|
||||
{
|
||||
searchBox->setPlaceholderText(tr("Filter by name..."));
|
||||
searchBox->setPlaceholderText(tr("Filter by name... (Exact match)"));
|
||||
loadFromDeckButton->setText(tr("Load from Deck"));
|
||||
loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters"));
|
||||
loadFromClipboardButton->setText(tr("Load from Clipboard"));
|
||||
@@ -68,7 +68,7 @@ void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
||||
if (!decklist)
|
||||
return;
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
createNameFilter(currentCard->getName());
|
||||
@@ -123,14 +123,14 @@ void VisualDatabaseDisplayNameFilterWidget::updateFilterModel()
|
||||
{
|
||||
// Clear existing name filters
|
||||
emit filterModel->layoutAboutToBeChanged();
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrName);
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrNameExact);
|
||||
|
||||
filterModel->blockSignals(true);
|
||||
filterModel->filterTree()->blockSignals(true);
|
||||
|
||||
for (const auto &name : activeFilters.keys()) {
|
||||
QString nameString = name;
|
||||
filterModel->addFilter(new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrName));
|
||||
filterModel->addFilter(new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrNameExact));
|
||||
}
|
||||
|
||||
filterModel->blockSignals(false);
|
||||
@@ -146,7 +146,7 @@ void VisualDatabaseDisplayNameFilterWidget::updateFilterModel()
|
||||
void VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
QStringList currentFilters;
|
||||
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrName)) {
|
||||
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrNameExact)) {
|
||||
if (filter->type() == CardFilter::Type::TypeOr) {
|
||||
currentFilters.append(filter->term());
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ VisualDatabaseDisplayRecentSetFilterSettingsWidget::VisualDatabaseDisplayRecentS
|
||||
filterToMostRecentSetsAmount->setMaximum(100);
|
||||
filterToMostRecentSetsAmount->setValue(
|
||||
SettingsCache::instance().getVisualDatabaseDisplayFilterToMostRecentSetsAmount());
|
||||
connect(filterToMostRecentSetsAmount, QOverload<int>::of(&QSpinBox::valueChanged), &SettingsCache::instance(),
|
||||
connect(filterToMostRecentSetsAmount, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance(),
|
||||
&SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsAmount);
|
||||
|
||||
layout->addWidget(filterToMostRecentSetsCheckBox);
|
||||
|
||||
@@ -26,7 +26,7 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg
|
||||
spinBox->setMaximum(getMaxSubTypeCount());
|
||||
spinBox->setValue(150);
|
||||
layout->addWidget(spinBox);
|
||||
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility);
|
||||
|
||||
// Create search box
|
||||
|
||||
@@ -206,6 +206,7 @@ void VisualDatabaseDisplayWidget::initialize()
|
||||
saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel);
|
||||
nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, deckEditor, filterModel);
|
||||
mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel);
|
||||
formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel);
|
||||
subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel);
|
||||
setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel);
|
||||
|
||||
@@ -223,6 +224,7 @@ void VisualDatabaseDisplayWidget::initialize()
|
||||
filterContainerLayout->addWidget(quickFilterSubTypeWidget);
|
||||
filterContainerLayout->addWidget(quickFilterSetWidget);
|
||||
filterContainerLayout->addWidget(mainTypeFilterWidget);
|
||||
filterContainerLayout->addWidget(formatLegalityWidget);
|
||||
|
||||
searchLayout->addWidget(colorFilterWidget);
|
||||
searchLayout->addWidget(clearFilterWidget);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "../utility/custom_line_edit.h"
|
||||
#include "visual_database_display_color_filter_widget.h"
|
||||
#include "visual_database_display_filter_save_load_widget.h"
|
||||
#include "visual_database_display_format_legality_filter_widget.h"
|
||||
#include "visual_database_display_main_type_filter_widget.h"
|
||||
#include "visual_database_display_name_filter_widget.h"
|
||||
#include "visual_database_display_set_filter_widget.h"
|
||||
@@ -91,6 +92,7 @@ private:
|
||||
SettingsButtonWidget *quickFilterNameWidget;
|
||||
VisualDatabaseDisplayNameFilterWidget *nameFilterWidget;
|
||||
VisualDatabaseDisplayMainTypeFilterWidget *mainTypeFilterWidget;
|
||||
VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget;
|
||||
SettingsButtonWidget *quickFilterSubTypeWidget;
|
||||
VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget;
|
||||
SettingsButtonWidget *quickFilterSetWidget;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
|
||||
|
||||
VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
|
||||
VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
groupAndSortLayout = new QHBoxLayout(this);
|
||||
groupAndSortLayout->setAlignment(Qt::AlignLeft);
|
||||
@@ -11,23 +13,19 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
|
||||
groupByLabel = new QLabel(this);
|
||||
|
||||
groupByComboBox = new QComboBox(this);
|
||||
if (auto visualDeckEditorWidget = qobject_cast<VisualDeckEditorWidget *>(parent)) {
|
||||
if (auto tabWidget = qobject_cast<TabDeckEditorVisualTabWidget *>(visualDeckEditorWidget)) {
|
||||
// Inside a central widget QWidget container inside TabDeckEditorVisual
|
||||
if (auto tab = qobject_cast<TabDeckEditorVisual *>(tabWidget->parent()->parent())) {
|
||||
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
|
||||
groupByComboBox->setModel(originalBox->model());
|
||||
groupByComboBox->setModelColumn(originalBox->modelColumn());
|
||||
|
||||
// Original -> clone
|
||||
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[this](int index) { groupByComboBox->setCurrentIndex(index); });
|
||||
if (auto tab = QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
|
||||
auto originalBox = tab->getDeckDockWidget()->getGroupByComboBox();
|
||||
groupByComboBox->setModel(originalBox->model());
|
||||
groupByComboBox->setModelColumn(originalBox->modelColumn());
|
||||
|
||||
// Clone -> original
|
||||
connect(groupByComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[originalBox](int index) { originalBox->setCurrentIndex(index); });
|
||||
}
|
||||
}
|
||||
// Original -> clone
|
||||
connect(originalBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[this](int index) { groupByComboBox->setCurrentIndex(index); });
|
||||
|
||||
// Clone -> original
|
||||
connect(groupByComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
[originalBox](int index) { originalBox->setCurrentIndex(index); });
|
||||
} else {
|
||||
groupByComboBox->addItem(
|
||||
tr(qPrintable(DeckListModelGroupCriteria::toString(DeckListModelGroupCriteria::MAIN_TYPE))),
|
||||
|
||||
@@ -24,9 +24,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare
|
||||
handSizeSpinBox = new QSpinBox(this);
|
||||
handSizeSpinBox->setValue(SettingsCache::instance().getVisualDeckEditorSampleHandSize());
|
||||
handSizeSpinBox->setMinimum(1);
|
||||
connect(handSizeSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), &SettingsCache::instance(),
|
||||
connect(handSizeSpinBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance(),
|
||||
&SettingsCache::setVisualDeckEditorSampleHandSize);
|
||||
connect(handSizeSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||
connect(handSizeSpinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&VisualDeckEditorSampleHandWidget::updateDisplay);
|
||||
resetAndHandSizeLayout->addWidget(handSizeSpinBox);
|
||||
|
||||
@@ -84,7 +84,7 @@ QList<ExactCard> VisualDeckEditorSampleHandWidget::getRandomCards(int amountToGe
|
||||
if (!decklist)
|
||||
return randomCards;
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = decklist->getCardNodes({DECK_ZONE_MAIN});
|
||||
QList<const DecklistCardNode *> cardsInDeck = decklist->getCardNodes({DECK_ZONE_MAIN});
|
||||
|
||||
// Collect all cards in the main deck, allowing duplicates based on their count
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
|
||||
: QWidget(_parent), deckList(nullptr)
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags)
|
||||
: QWidget(_parent), currentTags(_tags)
|
||||
{
|
||||
|
||||
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
@@ -27,16 +27,14 @@ DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_par
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
|
||||
if (_deckList) {
|
||||
setDeckList(_deckList);
|
||||
}
|
||||
|
||||
layout->addWidget(flowWidget);
|
||||
|
||||
refreshTags();
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::setDeckList(DeckList *_deckList)
|
||||
void DeckPreviewDeckTagsDisplayWidget::setTags(const QStringList &_tags)
|
||||
{
|
||||
deckList = _deckList;
|
||||
currentTags = _tags;
|
||||
refreshTags();
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags()
|
||||
{
|
||||
flowWidget->clearLayout();
|
||||
|
||||
for (const QString &tag : deckList->getTags()) {
|
||||
for (const QString &tag : currentTags) {
|
||||
flowWidget->addWidget(new DeckPreviewTagDisplayWidget(this, tag));
|
||||
}
|
||||
|
||||
@@ -71,7 +69,45 @@ static QStringList getAllFiles(const QString &filePath)
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
/**
|
||||
* Gets all tags that appear in the deck folder
|
||||
*/
|
||||
static QStringList findAllKnownTags()
|
||||
{
|
||||
QStringList allFiles = getAllFiles(SettingsCache::instance().getDeckPath());
|
||||
|
||||
QStringList knownTags;
|
||||
auto loader = DeckLoader(nullptr);
|
||||
for (const QString &file : allFiles) {
|
||||
loader.loadFromFile(file, DeckFileFormat::getFormatFromName(file), false);
|
||||
QStringList tags = loader.getDeckList()->getTags();
|
||||
knownTags.append(tags);
|
||||
knownTags.removeDuplicates();
|
||||
}
|
||||
|
||||
return knownTags;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
{
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
// If we're the child of a DeckPreviewWidget, then we need to handle conversion
|
||||
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
|
||||
|
||||
bool canAddTags = promptFileConversionIfRequired(deckPreviewWidget);
|
||||
|
||||
if (canAddTags) {
|
||||
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
} else {
|
||||
// If we're the child of an AbstractTabDeckEditor, then we don't bother with conversion
|
||||
QStringList knownTags = findAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
}
|
||||
|
||||
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
@@ -86,98 +122,70 @@ bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
static void convertFileToCockatriceFormat(DeckPreviewWidget *deckPreviewWidget)
|
||||
{
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
|
||||
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
|
||||
QStringList activeTags = deckList->getTags();
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
}
|
||||
|
||||
bool canAddTags = true;
|
||||
/**
|
||||
* Checks if the deck's file format supports tags.
|
||||
* If not, then prompt the user for file conversion.
|
||||
* @return whether the resulting file can support adding tags
|
||||
*/
|
||||
bool DeckPreviewDeckTagsDisplayWidget::promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget)
|
||||
{
|
||||
if (DeckFileFormat::getFormatFromName(deckPreviewWidget->filePath) == DeckFileFormat::Cockatrice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DeckLoader::getFormatFromName(deckPreviewWidget->filePath) != DeckLoader::CockatriceFormat) {
|
||||
canAddTags = false;
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
canAddTags = true;
|
||||
}
|
||||
} else {
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() == QDialog::Accepted) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastLoadInfo().fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
canAddTags = true;
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
} else {
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
|
||||
} else {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canAddTags) {
|
||||
DeckPreviewTagDialog dialog(knownTags, activeTags);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList updatedTags = dialog.getActiveTags();
|
||||
deckList->setTags(updatedTags);
|
||||
deckPreviewWidget->deckLoader->saveToFile(deckPreviewWidget->filePath, DeckLoader::CockatriceFormat);
|
||||
refreshTags();
|
||||
}
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
|
||||
return false;
|
||||
}
|
||||
} else if (parentWidget()) {
|
||||
// If we're the child of an AbstractTabDeckEditor, we are buried under a ton of childWidgets in the
|
||||
// DeckInfoDock.
|
||||
QWidget *currentParent = parentWidget();
|
||||
while (currentParent) {
|
||||
if (qobject_cast<AbstractTabDeckEditor *>(currentParent)) {
|
||||
break;
|
||||
}
|
||||
currentParent = currentParent->parentWidget();
|
||||
}
|
||||
if (qobject_cast<AbstractTabDeckEditor *>(currentParent)) {
|
||||
auto *deckEditor = qobject_cast<AbstractTabDeckEditor *>(currentParent);
|
||||
QStringList knownTags;
|
||||
QStringList allFiles = getAllFiles(SettingsCache::instance().getDeckPath());
|
||||
DeckLoader loader(this);
|
||||
for (const QString &file : allFiles) {
|
||||
loader.loadFromFile(file, DeckLoader::getFormatFromName(file), false);
|
||||
QStringList tags = loader.getDeckList()->getTags();
|
||||
knownTags.append(tags);
|
||||
knownTags.removeDuplicates();
|
||||
}
|
||||
|
||||
QStringList activeTags = deckList->getTags();
|
||||
convertFileToCockatriceFormat(deckPreviewWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
DeckPreviewTagDialog dialog(knownTags, activeTags);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList updatedTags = dialog.getActiveTags();
|
||||
deckList->setTags(updatedTags);
|
||||
deckEditor->setModified(true);
|
||||
refreshTags();
|
||||
}
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
convertFileToCockatriceFormat(deckPreviewWidget);
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags)
|
||||
{
|
||||
DeckPreviewTagDialog dialog(knownTags, currentTags);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QStringList updatedTags = dialog.getActiveTags();
|
||||
if (updatedTags != currentTags) {
|
||||
setTags(updatedTags);
|
||||
emit tagsChanged(updatedTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,31 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
inline bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath);
|
||||
|
||||
class DeckPreviewWidget;
|
||||
class DeckPreviewDeckTagsDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList);
|
||||
void setDeckList(DeckList *_deckList);
|
||||
void refreshTags();
|
||||
DeckList *deckList;
|
||||
QStringList currentTags;
|
||||
FlowWidget *flowWidget;
|
||||
|
||||
public:
|
||||
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags);
|
||||
void setTags(const QStringList &_tags);
|
||||
void refreshTags();
|
||||
|
||||
public slots:
|
||||
void openTagEditDlg();
|
||||
|
||||
private:
|
||||
bool promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget);
|
||||
void execTagDialog(const QStringList &knownTags);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when the tags have changed due to user interaction.
|
||||
* @param tags The new list of tags.
|
||||
*/
|
||||
void tagsChanged(const QStringList &tags);
|
||||
};
|
||||
#endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
|
||||
|
||||
@@ -32,7 +32,7 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||
many deck loads have finished already and if we've loaded all decks and THEN load all the tags at once. */
|
||||
connect(deckLoader, &DeckLoader::loadFinished, visualDeckStorageWidget->tagFilterWidget,
|
||||
&VisualDeckStorageTagFilterWidget::refreshTags);
|
||||
deckLoader->loadFromFileAsync(filePath, DeckLoader::getFormatFromName(filePath), false);
|
||||
deckLoader->loadFromFileAsync(filePath, DeckFileFormat::getFormatFromName(filePath), false);
|
||||
|
||||
bannerCardDisplayWidget =
|
||||
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
|
||||
@@ -83,7 +83,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
||||
setFilePath(deckLoader->getLastLoadInfo().fileName);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList());
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckLoader->getDeckList()->getTags());
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
|
||||
|
||||
bannerCardLabel = new QLabel(this);
|
||||
bannerCardLabel->setObjectName("bannerCardLabel");
|
||||
@@ -234,7 +235,7 @@ void DeckPreviewWidget::updateBannerCardComboBox()
|
||||
// Prepare the new items with deduplication
|
||||
QSet<QPair<QString, QString>> bannerCardSet;
|
||||
|
||||
QList<DecklistCardNode *> cardsInDeck = deckLoader->getDeckList()->getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = deckLoader->getDeckList()->getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
@@ -287,7 +288,7 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
||||
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
CardRef cardRef = {name, id};
|
||||
deckLoader->getDeckList()->setBannerCard(cardRef);
|
||||
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
|
||||
deckLoader->saveToFile(filePath, DeckFileFormat::getFormatFromName(filePath));
|
||||
bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef));
|
||||
}
|
||||
|
||||
@@ -307,6 +308,12 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC
|
||||
emit deckLoadRequested(filePath);
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setTags(const QStringList &tags)
|
||||
{
|
||||
deckLoader->getDeckList()->setTags(tags);
|
||||
deckLoader->saveToFile(filePath, DeckFileFormat::Cockatrice);
|
||||
}
|
||||
|
||||
QMenu *DeckPreviewWidget::createRightClickMenu()
|
||||
{
|
||||
auto *menu = new QMenu(this);
|
||||
@@ -379,7 +386,7 @@ void DeckPreviewWidget::actRenameDeck()
|
||||
|
||||
// write change
|
||||
deckLoader->getDeckList()->setName(newName);
|
||||
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
|
||||
deckLoader->saveToFile(filePath, DeckFileFormat::getFormatFromName(filePath));
|
||||
|
||||
// update VDS
|
||||
refreshBannerCardText();
|
||||
@@ -409,7 +416,7 @@ void DeckPreviewWidget::actRenameFile()
|
||||
return;
|
||||
}
|
||||
|
||||
DeckLoader::LoadInfo lastLoadInfo = deckLoader->getLastLoadInfo();
|
||||
LoadedDeck::LoadInfo lastLoadInfo = deckLoader->getLastLoadInfo();
|
||||
lastLoadInfo.fileName = newFilePath;
|
||||
deckLoader->setLastLoadInfo(lastLoadInfo);
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ private:
|
||||
void addSetBannerCardMenu(QMenu *menu);
|
||||
|
||||
private slots:
|
||||
void setTags(const QStringList &tags);
|
||||
|
||||
void actRenameDeck();
|
||||
void actRenameFile();
|
||||
void actDeleteFile();
|
||||
|
||||
@@ -61,10 +61,10 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
||||
unusedColorIdentitiesOpacitySpinBox->setMaximum(100);
|
||||
unusedColorIdentitiesOpacitySpinBox->setValue(
|
||||
SettingsCache::instance().getVisualDeckStorageUnusedColorIdentitiesOpacity());
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&VisualDeckStorageQuickSettingsWidget::unusedColorIdentitiesOpacityChanged);
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
|
||||
&SettingsCache::instance(), &SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance(),
|
||||
&SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
|
||||
unusedColorIdentitiesOpacityLabel->setBuddy(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
|
||||
|
||||
static const QList<AllowedCount> kConstructedCounts = {{4, "legal"}, {0, "banned"}};
|
||||
|
||||
static const QList<AllowedCount> kSingletonCounts = {{1, "legal"}, {0, "banned"}};
|
||||
|
||||
class CardDatabaseConverter : public CardDatabase
|
||||
{
|
||||
public:
|
||||
@@ -23,7 +27,73 @@ public:
|
||||
bool saveCardDatabase(const QString &fileName)
|
||||
{
|
||||
CockatriceXml4Parser parser(new NoopCardPreferenceProvider());
|
||||
return parser.saveToFile(sets, cards, fileName);
|
||||
|
||||
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName);
|
||||
}
|
||||
|
||||
FormatRulesNameMap createDefaultMagicFormats()
|
||||
{
|
||||
// Predefined common exceptions
|
||||
CardCondition superTypeIsBasic;
|
||||
superTypeIsBasic.field = "type";
|
||||
superTypeIsBasic.matchType = "contains";
|
||||
superTypeIsBasic.value = "Basic Land";
|
||||
|
||||
ExceptionRule basicLands;
|
||||
basicLands.conditions.append(superTypeIsBasic);
|
||||
|
||||
CardCondition anyNumberAllowed;
|
||||
anyNumberAllowed.field = "text";
|
||||
anyNumberAllowed.matchType = "contains";
|
||||
anyNumberAllowed.value = "A deck can have any number of";
|
||||
|
||||
ExceptionRule mayContainAnyNumber;
|
||||
mayContainAnyNumber.conditions.append(anyNumberAllowed);
|
||||
|
||||
// Map to store default rules
|
||||
FormatRulesNameMap defaultFormatRulesNameMap;
|
||||
|
||||
// ----------------- Helper lambda to create format -----------------
|
||||
auto makeFormat = [&](const QString &name, int minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15,
|
||||
const QList<AllowedCount> &allowedCounts = kConstructedCounts) -> FormatRulesPtr {
|
||||
FormatRulesPtr f(new FormatRules);
|
||||
f->formatName = name;
|
||||
f->allowedCounts = allowedCounts;
|
||||
f->minDeckSize = minDeck;
|
||||
f->maxDeckSize = maxDeck;
|
||||
f->maxSideboardSize = maxSideboardSize;
|
||||
f->exceptions.append(basicLands);
|
||||
f->exceptions.append(mayContainAnyNumber);
|
||||
defaultFormatRulesNameMap.insert(name.toLower(), f);
|
||||
return f;
|
||||
};
|
||||
|
||||
// ----------------- Standard formats -----------------
|
||||
makeFormat("Standard");
|
||||
makeFormat("Modern");
|
||||
makeFormat("Legacy");
|
||||
makeFormat("Pioneer");
|
||||
makeFormat("Historic");
|
||||
makeFormat("Timeless");
|
||||
makeFormat("Future");
|
||||
makeFormat("OldSchool");
|
||||
makeFormat("Premodern");
|
||||
makeFormat("Pauper");
|
||||
makeFormat("Penny");
|
||||
|
||||
// ----------------- Singleton formats -----------------
|
||||
makeFormat("Commander", 100, 100, 15, kSingletonCounts);
|
||||
makeFormat("Duel", 100, 100, 15, kSingletonCounts);
|
||||
makeFormat("Brawl", 60, 60, 15, kSingletonCounts);
|
||||
makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts);
|
||||
makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts);
|
||||
makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts);
|
||||
makeFormat("Predh", 100, 100, 15, kSingletonCounts);
|
||||
|
||||
// ----------------- Restricted formats -----------------
|
||||
makeFormat("Vintage", 60, -1, 15, {{4, "legal"}, {1, "restricted"}, {0, "banned"}});
|
||||
|
||||
return defaultFormatRulesNameMap;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@page developer_reference Developer Reference
|
||||
|
||||
- @subpage logging
|
||||
|
||||
- @subpage primer_cards
|
||||
|
||||
- @subpage card_database_schema_and_parsing
|
||||
|
||||
184
doc/doxygen/extra-pages/developer_documentation/logging.md
Normal file
184
doc/doxygen/extra-pages/developer_documentation/logging.md
Normal file
@@ -0,0 +1,184 @@
|
||||
@page logging Logging
|
||||
|
||||
Cockatrice uses QtLogging from the QtCore module for its logging. See
|
||||
the [official documentation](https://doc.qt.io/qt-6/qtlogging.html) for further details.
|
||||
|
||||
# Log Message Pattern
|
||||
|
||||
Any message logged through the QtLogging system automatically conforms to this message pattern:
|
||||
|
||||
Generic:
|
||||
|
||||
```
|
||||
[<timestamp> <log_level>] [<class:function>] - <message> [<filename>:<line_no>]
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
[2025-12-05 14:48:25.908 I] [MainWindow::startupConfigCheck] - Startup: found config with current version [window_main.cpp:951]
|
||||
```
|
||||
|
||||
For more information, see [Logging Setup](#logging-setup).
|
||||
|
||||
# Log Level and Categories
|
||||
|
||||
\note The default log level for the application is info.
|
||||
|
||||
This means that you should only use qInfo() in production-level code if you are truly sure that this message is
|
||||
beneficial to end-users and other developers. As a general rule, if your functionality logs to info more than twice in
|
||||
response to a user interaction, you are advised to consider moving some of these logs down to the debug level.
|
||||
|
||||
\warning You are strongly advised to avoid the use of the generic logging macros (e.g. qDebug(), qInfo(), qWarn()).
|
||||
|
||||
\note You should instead use the corresponding category logging macros (qCDebug(), qCInfo(), qCWarn()) and define
|
||||
logging
|
||||
categories for your log statements.
|
||||
|
||||
Example:
|
||||
|
||||
```c++
|
||||
in .h
|
||||
|
||||
inline Q_LOGGING_CATEGORY(ExampleCategory, "cockatrice_example_category");
|
||||
inline Q_LOGGING_CATEGORY(ExampleSubCategory, "cockatrice_example_category.sub_category");
|
||||
|
||||
in .cpp
|
||||
|
||||
qCInfo(ExampleCategory) << "Info level logs are usually sent through the main category"
|
||||
qCDebug(ExampleSubCategory) << "Debug level logs are permitted their own category to allow selective silencing"
|
||||
```
|
||||
|
||||
For more information on how to enable or disable logging categories,
|
||||
see [Logging Configuration](#logging-configuration).
|
||||
|
||||
# Logging Configuration
|
||||
|
||||
For configuring our logging, we use the qtlogging.ini, located under cockatrice/resources/config/qtlogging.ini, which is
|
||||
baked into the application in release version and set as the QT_LOGGING_CONF environment variable in main.cpp.
|
||||
|
||||
```c++
|
||||
#ifdef Q_OS_APPLE
|
||||
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
|
||||
const QByteArray configPath = "../../../qtlogging.ini";
|
||||
#elif defined(Q_OS_UNIX)
|
||||
// <build>/cockatrice/cockatrice
|
||||
const QByteArray configPath = "./qtlogging.ini";
|
||||
#elif defined(Q_OS_WIN)
|
||||
// <build>/cockatrice/Debug/cockatrice.exe
|
||||
const QByteArray configPath = "../qtlogging.ini";
|
||||
#else
|
||||
const QByteArray configPath = "";
|
||||
#endif
|
||||
|
||||
if (!qEnvironmentVariableIsSet(("QT_LOGGING_CONF"))) {
|
||||
// Set the QT_LOGGING_CONF environment variable
|
||||
qputenv("QT_LOGGING_CONF", configPath);
|
||||
}
|
||||
```
|
||||
|
||||
For more information on how to use this file and on how Qt evaluates which logging rules/file to use, please
|
||||
see the [official Qt documentation](https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories).
|
||||
|
||||
Some examples:
|
||||
|
||||
```
|
||||
# Turn off all logging except everything from card_picture_loader and all sub categories
|
||||
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
*.info = false
|
||||
*.warning = false
|
||||
*.critical = false
|
||||
*.fatal = false
|
||||
|
||||
card_picture_loader.* = true
|
||||
```
|
||||
|
||||
```
|
||||
# Turn off all logging except info level logs from card_picture_loader and all sub categories
|
||||
|
||||
[Rules]
|
||||
# The default log level is info
|
||||
*.debug = false
|
||||
*.info = false
|
||||
*.warning = false
|
||||
*.critical = false
|
||||
*.fatal = false
|
||||
|
||||
card_picture_loader.*.info = true
|
||||
```
|
||||
|
||||
```
|
||||
[Rules]
|
||||
# Turn on debug level logs for card_picture_loader but keep logging for sub categories suppressed
|
||||
*.debug = false
|
||||
|
||||
card_picture_loader.debug = true
|
||||
```
|
||||
|
||||
```
|
||||
[Rules]
|
||||
# Turn on all logs for worker subcategory of card_picture_loader
|
||||
*.debug = false
|
||||
|
||||
card_picture_loader.worker = true
|
||||
```
|
||||
|
||||
```
|
||||
[Rules]
|
||||
# Turn off some noisy and irrelevant startup logging for local development
|
||||
*.debug = false
|
||||
|
||||
qt_translator = false
|
||||
window_main.* = false
|
||||
release_channel = false
|
||||
spoiler_background_updater = false
|
||||
theme_manager = false
|
||||
sound_engine = false
|
||||
tapped_out_interface = false
|
||||
card_database = false
|
||||
card_database.loading = false
|
||||
card_database.loading.success_or_failure = true
|
||||
cockatrice_xml.* = false
|
||||
```
|
||||
|
||||
# Logging Setup
|
||||
|
||||
This is achieved through our logging setup in @ref main.cpp, where we set the message pattern and install a custom
|
||||
logger which replaces the full file path at the end with just the file name (Qt shows the full and quite lengthy path by
|
||||
default).
|
||||
|
||||
```c++
|
||||
qSetMessagePattern(
|
||||
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} "
|
||||
"%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{"
|
||||
"endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]");
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
|
||||
|
||||
qInstallMessageHandler(CockatriceLogger);
|
||||
```
|
||||
|
||||
```c++
|
||||
static void CockatriceLogger(QtMsgType type, const QMessageLogContext &ctx, const QString &message)
|
||||
{
|
||||
QString logMessage = qFormatLogMessage(type, ctx, message);
|
||||
|
||||
// Regular expression to match the full path in the square brackets and extract only the filename and line number
|
||||
QRegularExpression regex(R"(\[(?:.:)?[\/\\].*[\/\\]([^\/\\]+\:\d+)\])");
|
||||
QRegularExpressionMatch match = regex.match(logMessage);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
// Extract the filename and line number (e.g., "main.cpp:211")
|
||||
QString filenameLine = match.captured(1);
|
||||
|
||||
// Replace the full path in square brackets with just the filename and line number
|
||||
logMessage.replace(match.captured(0), QString("[%1]").arg(filenameLine));
|
||||
}
|
||||
|
||||
Logger::getInstance().log(type, ctx, logMessage);
|
||||
}
|
||||
```
|
||||
@@ -41,6 +41,8 @@ add_library(
|
||||
libcockatrice/card/relation/card_relation.cpp
|
||||
libcockatrice/card/set/card_set.cpp
|
||||
libcockatrice/card/set/card_set_list.cpp
|
||||
libcockatrice/card/format/format_legality_rules.cpp
|
||||
libcockatrice/card/format/format_legality_rules.h
|
||||
)
|
||||
|
||||
target_include_directories(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CARD_INFO_H
|
||||
#define CARD_INFO_H
|
||||
|
||||
#include "format/format_legality_rules.h"
|
||||
#include "printing/printing_info.h"
|
||||
|
||||
#include <QDate>
|
||||
@@ -22,10 +23,12 @@ class ICardDatabaseParser;
|
||||
|
||||
typedef QSharedPointer<CardInfo> CardInfoPtr;
|
||||
typedef QSharedPointer<CardSet> CardSetPtr;
|
||||
typedef QSharedPointer<FormatRules> FormatRulesPtr;
|
||||
typedef QMap<QString, QList<PrintingInfo>> SetToPrintingsMap;
|
||||
|
||||
typedef QHash<QString, CardInfoPtr> CardNameMap;
|
||||
typedef QHash<QString, CardSetPtr> SetNameMap;
|
||||
typedef QHash<QString, FormatRulesPtr> FormatRulesNameMap;
|
||||
|
||||
Q_DECLARE_METATYPE(CardInfoPtr)
|
||||
|
||||
|
||||
@@ -199,3 +199,8 @@ void CardDatabase::notifyEnabledSetsChanged()
|
||||
// inform the carddatabasemodels that they need to re-check their list of cards
|
||||
emit cardDatabaseEnabledSetsChanged();
|
||||
}
|
||||
|
||||
void CardDatabase::addFormat(FormatRulesPtr format)
|
||||
{
|
||||
formats.insert(format->formatName.toLower(), format);
|
||||
}
|
||||
@@ -42,6 +42,8 @@ protected:
|
||||
/// Sets indexed by short name
|
||||
SetNameMap sets;
|
||||
|
||||
FormatRulesNameMap formats;
|
||||
|
||||
/// Loader responsible for file discovery and parsing
|
||||
CardDatabaseLoader *loader;
|
||||
|
||||
@@ -141,6 +143,8 @@ public slots:
|
||||
*/
|
||||
void addSet(CardSetPtr set);
|
||||
|
||||
void addFormat(FormatRulesPtr format);
|
||||
|
||||
/** @brief Loads card databases from configured paths. */
|
||||
void loadCardDatabases();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
|
||||
// connect parser outputs to the database adders
|
||||
connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection);
|
||||
connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection);
|
||||
connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection);
|
||||
}
|
||||
|
||||
// when SettingsCache's path changes, trigger reloads
|
||||
@@ -149,6 +150,6 @@ bool CardDatabaseLoader::saveCustomTokensToFile()
|
||||
}
|
||||
}
|
||||
|
||||
availableParsers.first()->saveToFile(tmpSets, tmpCards, fileName);
|
||||
availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -341,4 +341,27 @@ QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
|
||||
}
|
||||
|
||||
return typeCounts;
|
||||
}
|
||||
|
||||
FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
|
||||
{
|
||||
return db->formats.value(formatName.toLower());
|
||||
}
|
||||
|
||||
QMap<QString, int> CardDatabaseQuerier::getAllFormatsWithCount() const
|
||||
{
|
||||
QMap<QString, int> formatCounts;
|
||||
|
||||
for (const auto &card : db->cards.values()) {
|
||||
QStringList allProps = card->getProperties();
|
||||
|
||||
for (const QString &prop : allProps) {
|
||||
if (prop.startsWith("format-")) {
|
||||
QString formatName = prop.mid(QStringLiteral("format-").size());
|
||||
formatCounts[formatName]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatCounts;
|
||||
}
|
||||
@@ -214,6 +214,8 @@ public:
|
||||
* @return Map of subtype string to count.
|
||||
*/
|
||||
[[nodiscard]] QMap<QString, int> getAllSubCardTypesWithCount() const;
|
||||
FormatRulesPtr getFormat(const QString &formatName) const;
|
||||
QMap<QString, int> getAllFormatsWithCount() const;
|
||||
|
||||
private:
|
||||
const CardDatabase *db; //!< Card database used for all lookups.
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Saves card and set data to a file.
|
||||
* @param _formats
|
||||
* @param sets Map of sets to save.
|
||||
* @param cards Map of cards to save.
|
||||
* @param fileName Target file path.
|
||||
@@ -45,7 +46,8 @@ public:
|
||||
* @param sourceVersion Optional version string of the source.
|
||||
* @return true if save succeeded.
|
||||
*/
|
||||
virtual bool saveToFile(SetNameMap sets,
|
||||
virtual bool saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl = "unknown",
|
||||
@@ -79,6 +81,8 @@ signals:
|
||||
|
||||
/** Emitted when a set is loaded from the database. */
|
||||
void addSet(CardSetPtr set);
|
||||
|
||||
void addFormat(FormatRulesPtr format);
|
||||
};
|
||||
|
||||
Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser")
|
||||
|
||||
@@ -438,12 +438,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
|
||||
return xml;
|
||||
}
|
||||
|
||||
bool CockatriceXml3Parser::saveToFile(SetNameMap _sets,
|
||||
bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl,
|
||||
const QString &sourceVersion)
|
||||
{
|
||||
Q_UNUSED(_formats);
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return false;
|
||||
|
||||
@@ -46,7 +46,8 @@ public:
|
||||
/**
|
||||
* @brief Save sets and cards back to an XML3 file.
|
||||
*/
|
||||
bool saveToFile(SetNameMap _sets,
|
||||
bool saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl = "unknown",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QXmlStreamReader>
|
||||
#include <libcockatrice/card/format/format_legality_rules.h>
|
||||
#include <version_string.h>
|
||||
|
||||
#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase"
|
||||
@@ -60,7 +61,9 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
|
||||
}
|
||||
|
||||
auto xmlName = xml.name().toString();
|
||||
if (xmlName == "sets") {
|
||||
if (xmlName == "formats") {
|
||||
loadFormats(xml);
|
||||
} else if (xmlName == "sets") {
|
||||
loadSetsFromXml(xml);
|
||||
} else if (xmlName == "cards") {
|
||||
loadCardsFromXml(xml);
|
||||
@@ -78,6 +81,116 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
|
||||
}
|
||||
}
|
||||
|
||||
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
|
||||
{
|
||||
auto rulesPtr = FormatRulesPtr(new FormatRules());
|
||||
|
||||
if (xml.attributes().hasAttribute("formatName")) {
|
||||
rulesPtr->formatName = xml.attributes().value("formatName").toString();
|
||||
}
|
||||
|
||||
while (!xml.atEnd()) {
|
||||
auto token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "format") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token != QXmlStreamReader::StartElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString xmlName = xml.name().toString();
|
||||
|
||||
if (xmlName == "minDeckSize") {
|
||||
rulesPtr->minDeckSize = xml.readElementText().toInt();
|
||||
} else if (xmlName == "maxDeckSize") {
|
||||
QString text = xml.readElementText();
|
||||
rulesPtr->maxDeckSize = text.toInt();
|
||||
} else if (xmlName == "maxSideboardSize") {
|
||||
rulesPtr->maxSideboardSize = xml.readElementText().toInt();
|
||||
} else if (xmlName == "allowedCounts") {
|
||||
while (!xml.atEnd()) {
|
||||
token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") {
|
||||
|
||||
AllowedCount c;
|
||||
|
||||
QString maxAttr = xml.attributes().value("max").toString();
|
||||
c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt();
|
||||
|
||||
c.label = xml.readElementText().trimmed();
|
||||
|
||||
rulesPtr->allowedCounts.append(c);
|
||||
}
|
||||
}
|
||||
} else if (xmlName == "exceptions") {
|
||||
while (!xml.atEnd()) {
|
||||
token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") {
|
||||
ExceptionRule ex;
|
||||
|
||||
while (!xml.atEnd()) {
|
||||
token = xml.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == QXmlStreamReader::StartElement) {
|
||||
QString ename = xml.name().toString();
|
||||
|
||||
if (ename == "maxCopies") {
|
||||
QString text = xml.readElementText();
|
||||
ex.maxCopies = (text == "unlimited") ? -1 : text.toInt();
|
||||
} else if (ename == "cardCondition") {
|
||||
CardCondition cond;
|
||||
cond.field = xml.attributes().value("field").toString();
|
||||
cond.matchType = xml.attributes().value("match").toString();
|
||||
cond.value = xml.attributes().value("value").toString();
|
||||
ex.conditions.append(cond);
|
||||
xml.skipCurrentElement();
|
||||
} else {
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rulesPtr->exceptions.append(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
return rulesPtr;
|
||||
}
|
||||
|
||||
void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
|
||||
{
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (xml.name().toString() == "format") {
|
||||
auto rulesPtr = parseFormat(xml);
|
||||
emit addFormat(rulesPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
|
||||
{
|
||||
while (!xml.atEnd()) {
|
||||
@@ -273,6 +386,59 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
||||
}
|
||||
}
|
||||
|
||||
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer<FormatRules> &rulesPtr)
|
||||
{
|
||||
if (rulesPtr.isNull()) {
|
||||
qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr";
|
||||
return xml;
|
||||
}
|
||||
|
||||
const FormatRules &rules = *rulesPtr;
|
||||
|
||||
xml.writeStartElement("format");
|
||||
if (!rules.formatName.isEmpty()) {
|
||||
xml.writeAttribute("formatName", rules.formatName);
|
||||
}
|
||||
|
||||
xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize));
|
||||
xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0");
|
||||
xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize));
|
||||
if (!rules.allowedCounts.isEmpty()) {
|
||||
xml.writeStartElement("allowedCounts");
|
||||
|
||||
for (const AllowedCount &c : rules.allowedCounts) {
|
||||
xml.writeStartElement("count");
|
||||
xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max));
|
||||
xml.writeCharacters(c.label);
|
||||
xml.writeEndElement(); // count
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // allowedCounts
|
||||
}
|
||||
|
||||
if (!rules.exceptions.isEmpty()) {
|
||||
xml.writeStartElement("exceptions");
|
||||
for (const ExceptionRule &ex : rules.exceptions) {
|
||||
xml.writeStartElement("exception");
|
||||
xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies));
|
||||
|
||||
for (const CardCondition &cond : ex.conditions) {
|
||||
xml.writeStartElement("cardCondition");
|
||||
xml.writeAttribute("field", cond.field);
|
||||
xml.writeAttribute("match", cond.matchType);
|
||||
xml.writeAttribute("value", cond.value);
|
||||
xml.writeEndElement(); // cardCondition
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // exception
|
||||
}
|
||||
xml.writeEndElement(); // exceptions
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // format
|
||||
return xml;
|
||||
}
|
||||
|
||||
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
|
||||
{
|
||||
if (set.isNull()) {
|
||||
@@ -399,7 +565,8 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
|
||||
return xml;
|
||||
}
|
||||
|
||||
bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
|
||||
bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl,
|
||||
@@ -426,6 +593,14 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
|
||||
xml.writeTextElement("sourceVersion", sourceVersion);
|
||||
xml.writeEndElement();
|
||||
|
||||
if (_formats.count() > 0) {
|
||||
xml.writeStartElement("formats");
|
||||
for (FormatRulesPtr format : _formats) {
|
||||
xml << format;
|
||||
}
|
||||
xml.writeEndElement();
|
||||
}
|
||||
|
||||
if (_sets.count() > 0) {
|
||||
xml.writeStartElement("sets");
|
||||
for (CardSetPtr set : _sets) {
|
||||
|
||||
@@ -49,7 +49,8 @@ public:
|
||||
/**
|
||||
* @brief Save sets and cards back to an XML4 file.
|
||||
*/
|
||||
bool saveToFile(SetNameMap _sets,
|
||||
bool saveToFile(FormatRulesNameMap _formats,
|
||||
SetNameMap _sets,
|
||||
CardNameMap cards,
|
||||
const QString &fileName,
|
||||
const QString &sourceUrl = "unknown",
|
||||
@@ -72,6 +73,7 @@ private:
|
||||
*/
|
||||
void loadCardsFromXml(QXmlStreamReader &xml);
|
||||
|
||||
void loadFormats(QXmlStreamReader &xml);
|
||||
/**
|
||||
* @brief Load all <set> elements from the XML stream.
|
||||
* @param xml The open QXmlStreamReader positioned at the <sets> element.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "format_legality_rules.h"
|
||||
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
||||
bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond)
|
||||
{
|
||||
CardMatchType type = matchTypeFromString(cond.matchType);
|
||||
QString fieldValue;
|
||||
if (cond.field == "name") {
|
||||
fieldValue = card.getName();
|
||||
} else if (cond.field == "text") {
|
||||
fieldValue = card.getText();
|
||||
} else {
|
||||
fieldValue = card.getProperty(cond.field);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case CardMatchType::Equals:
|
||||
return fieldValue == cond.value;
|
||||
case CardMatchType::NotEquals:
|
||||
return fieldValue != cond.value;
|
||||
case CardMatchType::Contains:
|
||||
return fieldValue.contains(cond.value, Qt::CaseInsensitive);
|
||||
case CardMatchType::NotContains:
|
||||
return !fieldValue.contains(cond.value, Qt::CaseInsensitive);
|
||||
case CardMatchType::Regex: {
|
||||
QRegularExpression re(cond.value, QRegularExpression::CaseInsensitiveOption);
|
||||
return re.match(fieldValue).hasMatch();
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule)
|
||||
{
|
||||
for (const CardCondition &cond : rule.conditions) {
|
||||
if (!cardMatchesCondition(card, cond)) {
|
||||
return false; // all conditions must match
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format)
|
||||
{
|
||||
for (const ExceptionRule &rule : format.exceptions) {
|
||||
if (exceptionAppliesToCard(card, rule)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
#define COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
|
||||
class CardInfo;
|
||||
using CardInfoPtr = QSharedPointer<CardInfo>;
|
||||
|
||||
struct CardCondition
|
||||
{
|
||||
QString field; // e.g. "type", "maintype", "text"
|
||||
QString matchType; // "contains", "equals", "regex", "notContains", etc.
|
||||
QString value; // e.g. "Basic Land"
|
||||
};
|
||||
|
||||
struct AllowedCount
|
||||
{
|
||||
int max = 0; // 4, 1, 0, or -1 for unlimited
|
||||
QString label; // "legal", "restricted", "banned"
|
||||
};
|
||||
|
||||
struct ExceptionRule
|
||||
{
|
||||
QList<CardCondition> conditions; // All must match
|
||||
int maxCopies = -1; // -1 = unlimited
|
||||
};
|
||||
|
||||
struct FormatRules
|
||||
{
|
||||
QString formatName;
|
||||
int minDeckSize = 60;
|
||||
int maxDeckSize = -1; // -1 = unlimited
|
||||
int maxSideboardSize = 15;
|
||||
|
||||
QList<AllowedCount> allowedCounts;
|
||||
|
||||
QList<ExceptionRule> exceptions; // Cards allowed to break maxCopies
|
||||
};
|
||||
|
||||
enum class CardMatchType
|
||||
{
|
||||
Equals,
|
||||
NotEquals,
|
||||
Contains,
|
||||
NotContains,
|
||||
Regex
|
||||
};
|
||||
|
||||
// convert string to enum
|
||||
inline CardMatchType matchTypeFromString(const QString &str)
|
||||
{
|
||||
if (str == "equals")
|
||||
return CardMatchType::Equals;
|
||||
if (str == "notEquals")
|
||||
return CardMatchType::NotEquals;
|
||||
if (str == "contains")
|
||||
return CardMatchType::Contains;
|
||||
if (str == "notContains")
|
||||
return CardMatchType::NotContains;
|
||||
if (str == "regex")
|
||||
return CardMatchType::Regex;
|
||||
return CardMatchType::Equals; // fallback default
|
||||
}
|
||||
|
||||
bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond);
|
||||
|
||||
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule);
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format);
|
||||
|
||||
#endif // COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
@@ -27,6 +27,8 @@ add_library(
|
||||
libcockatrice/deck_list/tree/inner_deck_list_node.cpp
|
||||
libcockatrice/deck_list/deck_list.cpp
|
||||
libcockatrice/deck_list/deck_list_history_manager.cpp
|
||||
libcockatrice/deck_list/deck_list_node_tree.cpp
|
||||
libcockatrice/deck_list/deck_list_node_tree.h
|
||||
)
|
||||
|
||||
add_dependencies(libcockatrice_deck_list libcockatrice_protocol)
|
||||
|
||||
@@ -83,26 +83,23 @@ bool DeckList::Metadata::isEmpty() const
|
||||
}
|
||||
|
||||
DeckList::DeckList()
|
||||
{
|
||||
root = new InnerDecklistNode;
|
||||
}
|
||||
|
||||
DeckList::DeckList(const DeckList &other)
|
||||
: metadata(other.metadata), sideboardPlans(other.sideboardPlans), root(new InnerDecklistNode(other.getRoot())),
|
||||
cachedDeckHash(other.cachedDeckHash)
|
||||
{
|
||||
}
|
||||
|
||||
DeckList::DeckList(const QString &nativeString)
|
||||
{
|
||||
root = new InnerDecklistNode;
|
||||
loadFromString_Native(nativeString);
|
||||
}
|
||||
|
||||
DeckList::DeckList(const Metadata &metadata,
|
||||
const DecklistNodeTree &tree,
|
||||
const QMap<QString, SideboardPlan *> &sideboardPlans)
|
||||
: metadata(metadata), sideboardPlans(sideboardPlans), tree(tree)
|
||||
{
|
||||
}
|
||||
|
||||
DeckList::~DeckList()
|
||||
{
|
||||
delete root;
|
||||
|
||||
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
|
||||
while (i.hasNext())
|
||||
delete i.next().value();
|
||||
@@ -136,6 +133,8 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
||||
metadata.lastLoadedTimestamp = xml->readElementText();
|
||||
} else if (childName == "deckname") {
|
||||
metadata.name = xml->readElementText();
|
||||
} else if (childName == "format") {
|
||||
metadata.gameFormat = xml->readElementText();
|
||||
} else if (childName == "comments") {
|
||||
metadata.comments = xml->readElementText();
|
||||
} else if (childName == "bannerCard") {
|
||||
@@ -150,8 +149,7 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
||||
}
|
||||
}
|
||||
} else if (childName == "zone") {
|
||||
InnerDecklistNode *newZone = getZoneObjFromName(xml->attributes().value("name").toString());
|
||||
newZone->readElement(xml);
|
||||
tree.readZoneElement(xml);
|
||||
} else if (childName == "sideboard_plan") {
|
||||
SideboardPlan *newSideboardPlan = new SideboardPlan;
|
||||
if (newSideboardPlan->readElement(xml)) {
|
||||
@@ -170,6 +168,7 @@ void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata)
|
||||
{
|
||||
xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp);
|
||||
xml->writeTextElement("deckname", metadata.name);
|
||||
xml->writeTextElement("format", metadata.gameFormat);
|
||||
xml->writeStartElement("bannerCard");
|
||||
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
|
||||
xml->writeCharacters(metadata.bannerCard.name);
|
||||
@@ -192,9 +191,7 @@ void DeckList::write(QXmlStreamWriter *xml) const
|
||||
writeMetadata(xml, metadata);
|
||||
|
||||
// Write zones
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
root->at(i)->writeElement(xml);
|
||||
}
|
||||
tree.write(xml);
|
||||
|
||||
// Write sideboard plans
|
||||
QMapIterator<QString, SideboardPlan *> i(sideboardPlans);
|
||||
@@ -453,25 +450,13 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
|
||||
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
|
||||
|
||||
// make new entry in decklist
|
||||
new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName), -1, setCode, collectorNumber);
|
||||
tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber);
|
||||
}
|
||||
|
||||
refreshDeckHash();
|
||||
return true;
|
||||
}
|
||||
|
||||
InnerDecklistNode *DeckList::getZoneObjFromName(const QString &zoneName)
|
||||
{
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
if (node->getName() == zoneName) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return new InnerDecklistNode(zoneName, root);
|
||||
}
|
||||
|
||||
bool DeckList::loadFromFile_Plain(QIODevice *device)
|
||||
{
|
||||
QTextStream in(device);
|
||||
@@ -516,184 +501,70 @@ QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappe
|
||||
*/
|
||||
void DeckList::cleanList(bool preserveMetadata)
|
||||
{
|
||||
root->clearTree();
|
||||
tree.clear();
|
||||
if (!preserveMetadata) {
|
||||
metadata = {};
|
||||
}
|
||||
refreshDeckHash();
|
||||
}
|
||||
|
||||
void DeckList::getCardListHelper(InnerDecklistNode *item, QSet<QString> &result)
|
||||
{
|
||||
for (int i = 0; i < item->size(); ++i) {
|
||||
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
|
||||
|
||||
if (node) {
|
||||
result.insert(node->getName());
|
||||
} else {
|
||||
getCardListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DeckList::getCardRefListHelper(InnerDecklistNode *item, QList<CardRef> &result)
|
||||
{
|
||||
for (int i = 0; i < item->size(); ++i) {
|
||||
auto *node = dynamic_cast<DecklistCardNode *>(item->at(i));
|
||||
|
||||
if (node) {
|
||||
result.append(node->toCardRef());
|
||||
} else {
|
||||
getCardRefListHelper(dynamic_cast<InnerDecklistNode *>(item->at(i)), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList DeckList::getCardList() const
|
||||
{
|
||||
QSet<QString> result;
|
||||
getCardListHelper(root, result);
|
||||
return result.values();
|
||||
auto nodes = tree.getCardNodes();
|
||||
|
||||
QStringList result;
|
||||
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), [](auto node) { return node->getName(); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<CardRef> DeckList::getCardRefList() const
|
||||
{
|
||||
auto nodes = tree.getCardNodes();
|
||||
|
||||
QList<CardRef> result;
|
||||
getCardRefListHelper(root, result);
|
||||
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result),
|
||||
[](auto node) { return node->toCardRef(); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<DecklistCardNode *> DeckList::getCardNodes(const QStringList &restrictToZones) const
|
||||
QList<const DecklistCardNode *> DeckList::getCardNodes(const QSet<QString> &restrictToZones) const
|
||||
{
|
||||
QList<DecklistCardNode *> result;
|
||||
return tree.getCardNodes(restrictToZones);
|
||||
}
|
||||
|
||||
for (auto *node : *root) {
|
||||
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(node);
|
||||
if (zoneNode == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (!restrictToZones.isEmpty() && !restrictToZones.contains(node->getName())) {
|
||||
continue;
|
||||
}
|
||||
for (auto *cardNode : *zoneNode) {
|
||||
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
|
||||
if (cardCardNode != nullptr) {
|
||||
result.append(cardCardNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
QList<const InnerDecklistNode *> DeckList::getZoneNodes() const
|
||||
{
|
||||
return tree.getZoneNodes();
|
||||
}
|
||||
|
||||
int DeckList::getSideboardSize() const
|
||||
{
|
||||
int size = 0;
|
||||
for (int i = 0; i < root->size(); ++i) {
|
||||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
if (node->getName() != DECK_ZONE_SIDE) {
|
||||
continue;
|
||||
}
|
||||
auto cards = tree.getCardNodes({DECK_ZONE_SIDE});
|
||||
|
||||
for (int j = 0; j < node->size(); j++) {
|
||||
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||||
size += card->getNumber();
|
||||
}
|
||||
int size = 0;
|
||||
for (auto card : cards) {
|
||||
size += card->getNumber();
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
DecklistCardNode *DeckList::addCard(const QString &cardName,
|
||||
const QString &zoneName,
|
||||
const int position,
|
||||
int position,
|
||||
const QString &cardSetName,
|
||||
const QString &cardSetCollectorNumber,
|
||||
const QString &cardProviderId)
|
||||
const QString &cardProviderId,
|
||||
bool formatLegal)
|
||||
{
|
||||
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
|
||||
if (zoneNode == nullptr) {
|
||||
zoneNode = new InnerDecklistNode(zoneName, root);
|
||||
}
|
||||
|
||||
auto *node =
|
||||
new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber, cardProviderId);
|
||||
auto node =
|
||||
tree.addCard(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal);
|
||||
refreshDeckHash();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
bool DeckList::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
|
||||
{
|
||||
if (node == root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateHash = false;
|
||||
if (rootNode == nullptr) {
|
||||
rootNode = root;
|
||||
updateHash = true;
|
||||
}
|
||||
|
||||
int index = rootNode->indexOf(node);
|
||||
if (index != -1) {
|
||||
delete rootNode->takeAt(index);
|
||||
|
||||
if (rootNode->empty()) {
|
||||
deleteNode(rootNode, rootNode->getParent());
|
||||
}
|
||||
|
||||
if (updateHash) {
|
||||
refreshDeckHash();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rootNode->size(); i++) {
|
||||
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
|
||||
if (inner) {
|
||||
if (deleteNode(node, inner)) {
|
||||
if (updateHash) {
|
||||
refreshDeckHash();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static QString computeDeckHash(const InnerDecklistNode *root)
|
||||
{
|
||||
QStringList cardList;
|
||||
QSet<QString> hashZones, optionalZones;
|
||||
|
||||
hashZones << DECK_ZONE_MAIN << DECK_ZONE_SIDE; // Zones in deck to be included in hashing process
|
||||
optionalZones << DECK_ZONE_TOKENS; // Optional zones in deck not included in hashing process
|
||||
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
for (int j = 0; j < node->size(); j++) {
|
||||
if (hashZones.contains(node->getName())) // Mainboard or Sideboard
|
||||
{
|
||||
auto *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||||
for (int k = 0; k < card->getNumber(); ++k) {
|
||||
cardList.append((node->getName() == DECK_ZONE_SIDE ? "SB:" : "") + card->getName().toLower());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cardList.sort();
|
||||
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
|
||||
quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) +
|
||||
(((quint64)(unsigned char)deckHashArray[1]) << 24) +
|
||||
(((quint64)(unsigned char)deckHashArray[2] << 16)) +
|
||||
(((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4];
|
||||
return QString::number(number, 32).rightJustified(8, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the deck hash.
|
||||
* The hash is computed on the first call to this method, and is cached until the decklist is modified.
|
||||
@@ -706,7 +577,7 @@ QString DeckList::getDeckHash() const
|
||||
return cachedDeckHash;
|
||||
}
|
||||
|
||||
cachedDeckHash = computeDeckHash(root);
|
||||
cachedDeckHash = tree.computeDeckHash();
|
||||
return cachedDeckHash;
|
||||
}
|
||||
|
||||
@@ -723,15 +594,7 @@ void DeckList::refreshDeckHash()
|
||||
*/
|
||||
void DeckList::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
|
||||
{
|
||||
// Support for this is only possible if the internal structure
|
||||
// doesn't get more complicated.
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
for (int j = 0; j < node->size(); j++) {
|
||||
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||||
func(node, card);
|
||||
}
|
||||
}
|
||||
tree.forEachCard(func);
|
||||
}
|
||||
|
||||
DeckListMemento DeckList::createMemento(const QString &reason) const
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#define DECKLIST_H
|
||||
|
||||
#include "deck_list_memento.h"
|
||||
#include "deck_list_node_tree.h"
|
||||
#include "tree/inner_deck_list_node.h"
|
||||
|
||||
#include <QMap>
|
||||
@@ -107,14 +108,14 @@ public:
|
||||
* - Provide hashing for deck identity (deck hash).
|
||||
*
|
||||
* ### Ownership:
|
||||
* - Owns the root `InnerDecklistNode` tree.
|
||||
* - Owns the `DecklistNodeTree`.
|
||||
* - Owns `SideboardPlan` instances stored in `sideboardPlans`.
|
||||
*
|
||||
* ### Example workflow:
|
||||
* ```
|
||||
* DeckList deck;
|
||||
* deck.setName("Mono Red Aggro");
|
||||
* deck.addCard("Lightning Bolt", "main", -1);
|
||||
* deck.addCard("Lightning Bolt", "main");
|
||||
* deck.addTag("Aggro");
|
||||
* deck.saveToFile_Native(device);
|
||||
* ```
|
||||
@@ -126,6 +127,7 @@ public:
|
||||
{
|
||||
QString name; ///< User-defined deck name.
|
||||
QString comments; ///< Free-form comments or notes.
|
||||
QString gameFormat; ///< The name of the game format this deck contains legal cards for
|
||||
CardRef bannerCard; ///< Optional representative card for the deck.
|
||||
QStringList tags; ///< User-defined tags for deck classification.
|
||||
QString lastLoadedTimestamp; ///< Timestamp string of last load.
|
||||
@@ -139,7 +141,7 @@ public:
|
||||
private:
|
||||
Metadata metadata; ///< Deck metadata that is stored in the deck file
|
||||
QMap<QString, SideboardPlan *> sideboardPlans; ///< Named sideboard plans.
|
||||
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
|
||||
DecklistNodeTree tree; ///< The deck tree (zones + cards).
|
||||
|
||||
/**
|
||||
* @brief Cached deck hash, recalculated lazily.
|
||||
@@ -147,11 +149,6 @@ private:
|
||||
*/
|
||||
mutable QString cachedDeckHash;
|
||||
|
||||
// Helpers for traversing the tree
|
||||
static void getCardListHelper(InnerDecklistNode *node, QSet<QString> &result);
|
||||
static void getCardRefListHelper(InnerDecklistNode *item, QList<CardRef> &result);
|
||||
InnerDecklistNode *getZoneObjFromName(const QString &zoneName);
|
||||
|
||||
public:
|
||||
/// @name Metadata setters
|
||||
///@{
|
||||
@@ -183,20 +180,36 @@ public:
|
||||
{
|
||||
metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
|
||||
}
|
||||
void setGameFormat(const QString &_gameFormat = QString())
|
||||
{
|
||||
metadata.gameFormat = _gameFormat;
|
||||
}
|
||||
///@}
|
||||
|
||||
/// @brief Construct an empty deck.
|
||||
explicit DeckList();
|
||||
/// @brief Copy constructor (deep copies the node tree)
|
||||
DeckList(const DeckList &other);
|
||||
/// @brief Construct from a serialized native-format string.
|
||||
explicit DeckList(const QString &nativeString);
|
||||
/// @brief Construct from components
|
||||
DeckList(const Metadata &metadata,
|
||||
const DecklistNodeTree &tree,
|
||||
const QMap<QString, SideboardPlan *> &sideboardPlans = {});
|
||||
virtual ~DeckList();
|
||||
|
||||
/**
|
||||
* @brief Gets a pointer to the underlying node tree.
|
||||
* Note: DO NOT call this method unless the object needs to have access to the underlying model.
|
||||
* For now, only the DeckListModel should be calling this.
|
||||
*/
|
||||
DecklistNodeTree *getTree()
|
||||
{
|
||||
return &tree;
|
||||
}
|
||||
|
||||
/// @name Metadata getters
|
||||
/// The individual metadata getters still exist for backwards compatibility.
|
||||
/// TODO: Figure out when we can remove them.
|
||||
///@{
|
||||
//! \todo Figure out when we can remove them.
|
||||
const Metadata &getMetadata() const
|
||||
{
|
||||
return metadata;
|
||||
@@ -221,6 +234,10 @@ public:
|
||||
{
|
||||
return metadata.lastLoadedTimestamp;
|
||||
}
|
||||
QString getGameFormat() const
|
||||
{
|
||||
return metadata.gameFormat;
|
||||
}
|
||||
///@}
|
||||
|
||||
bool isBlankDeck() const
|
||||
@@ -263,23 +280,21 @@ public:
|
||||
void cleanList(bool preserveMetadata = false);
|
||||
bool isEmpty() const
|
||||
{
|
||||
return root->isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty();
|
||||
return tree.isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty();
|
||||
}
|
||||
QStringList getCardList() const;
|
||||
QList<CardRef> getCardRefList() const;
|
||||
QList<DecklistCardNode *> getCardNodes(const QStringList &restrictToZones = QStringList()) const;
|
||||
QList<const DecklistCardNode *> getCardNodes(const QSet<QString> &restrictToZones = {}) const;
|
||||
QList<const InnerDecklistNode *> getZoneNodes() const;
|
||||
int getSideboardSize() const;
|
||||
InnerDecklistNode *getRoot() const
|
||||
{
|
||||
return root;
|
||||
}
|
||||
|
||||
DecklistCardNode *addCard(const QString &cardName,
|
||||
const QString &zoneName,
|
||||
int position,
|
||||
int position = -1,
|
||||
const QString &cardSetName = QString(),
|
||||
const QString &cardSetCollectorNumber = QString(),
|
||||
const QString &cardProviderId = QString());
|
||||
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
|
||||
const QString &cardProviderId = QString(),
|
||||
const bool formatLegal = true);
|
||||
///@}
|
||||
|
||||
/// @name Deck identity
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "deck_list_node_tree.h"
|
||||
|
||||
#include "tree/deck_list_card_node.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QSet>
|
||||
|
||||
DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode())
|
||||
{
|
||||
}
|
||||
|
||||
DecklistNodeTree::DecklistNodeTree(const DecklistNodeTree &other) : root(new InnerDecklistNode(other.root))
|
||||
{
|
||||
}
|
||||
|
||||
DecklistNodeTree &DecklistNodeTree::operator=(const DecklistNodeTree &other)
|
||||
{
|
||||
if (this != &other) {
|
||||
delete root;
|
||||
root = new InnerDecklistNode(other.root);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
DecklistNodeTree::~DecklistNodeTree()
|
||||
{
|
||||
delete root;
|
||||
}
|
||||
|
||||
bool DecklistNodeTree::isEmpty() const
|
||||
{
|
||||
return root->isEmpty();
|
||||
}
|
||||
|
||||
void DecklistNodeTree::clear()
|
||||
{
|
||||
root->clearTree();
|
||||
}
|
||||
|
||||
QList<const DecklistCardNode *> DecklistNodeTree::getCardNodes(const QSet<QString> &restrictToZones) const
|
||||
{
|
||||
QList<const DecklistCardNode *> result;
|
||||
|
||||
for (auto *zoneNode : getZoneNodes()) {
|
||||
if (!restrictToZones.isEmpty() && !restrictToZones.contains(zoneNode->getName())) {
|
||||
continue;
|
||||
}
|
||||
for (auto *cardNode : *zoneNode) {
|
||||
auto *cardCardNode = dynamic_cast<DecklistCardNode *>(cardNode);
|
||||
if (cardCardNode) {
|
||||
result.append(cardCardNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<const InnerDecklistNode *> DecklistNodeTree::getZoneNodes() const
|
||||
{
|
||||
QList<const InnerDecklistNode *> zones;
|
||||
for (auto *node : *root) {
|
||||
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(node);
|
||||
if (!currentZone)
|
||||
continue;
|
||||
zones.append(currentZone);
|
||||
}
|
||||
|
||||
return zones;
|
||||
}
|
||||
|
||||
QString DecklistNodeTree::computeDeckHash() const
|
||||
{
|
||||
auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN});
|
||||
auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE});
|
||||
|
||||
static auto nodesToCardList = [](const QList<const DecklistCardNode *> &nodes, const QString &prefix = {}) {
|
||||
QStringList result;
|
||||
for (auto node : nodes) {
|
||||
for (int i = 0; i < node->getNumber(); ++i) {
|
||||
result.append(prefix + node->getName().toLower());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:");
|
||||
|
||||
cardList.sort();
|
||||
QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1);
|
||||
quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) +
|
||||
(((quint64)(unsigned char)deckHashArray[1]) << 24) +
|
||||
(((quint64)(unsigned char)deckHashArray[2] << 16)) +
|
||||
(((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4];
|
||||
return QString::number(number, 32).rightJustified(8, '0');
|
||||
}
|
||||
|
||||
void DecklistNodeTree::write(QXmlStreamWriter *xml) const
|
||||
{
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
root->at(i)->writeElement(xml);
|
||||
}
|
||||
}
|
||||
|
||||
void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml)
|
||||
{
|
||||
QString zoneName = xml->attributes().value("name").toString();
|
||||
InnerDecklistNode *newZone = getZoneObjFromName(zoneName);
|
||||
newZone->readElement(xml);
|
||||
}
|
||||
|
||||
DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName,
|
||||
int amount,
|
||||
const QString &zoneName,
|
||||
int position,
|
||||
const QString &cardSetName,
|
||||
const QString &cardSetCollectorNumber,
|
||||
const QString &cardProviderId,
|
||||
const bool formatLegal)
|
||||
{
|
||||
auto *zoneNode = getZoneObjFromName(zoneName);
|
||||
auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber,
|
||||
cardProviderId, formatLegal);
|
||||
return node;
|
||||
}
|
||||
|
||||
bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode)
|
||||
{
|
||||
if (node == root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rootNode == nullptr) {
|
||||
rootNode = root;
|
||||
}
|
||||
|
||||
int index = rootNode->indexOf(node);
|
||||
if (index != -1) {
|
||||
delete rootNode->takeAt(index);
|
||||
|
||||
if (rootNode->empty()) {
|
||||
deleteNode(rootNode, rootNode->getParent());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rootNode->size(); i++) {
|
||||
auto *inner = dynamic_cast<InnerDecklistNode *>(rootNode->at(i));
|
||||
if (inner) {
|
||||
if (deleteNode(node, inner)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void DecklistNodeTree::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const
|
||||
{
|
||||
// Support for this is only possible if the internal structure
|
||||
// doesn't get more complicated.
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
InnerDecklistNode *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
for (int j = 0; j < node->size(); j++) {
|
||||
DecklistCardNode *card = dynamic_cast<DecklistCardNode *>(node->at(j));
|
||||
func(node, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist.
|
||||
*/
|
||||
InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const
|
||||
{
|
||||
for (int i = 0; i < root->size(); i++) {
|
||||
auto *node = dynamic_cast<InnerDecklistNode *>(root->at(i));
|
||||
if (node->getName() == zoneName) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return new InnerDecklistNode(zoneName, root);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#ifndef COCKATRICE_DECKLIST_NODE_TREE_H
|
||||
#define COCKATRICE_DECKLIST_NODE_TREE_H
|
||||
|
||||
#include "libcockatrice/utility/card_ref.h"
|
||||
#include "tree/deck_list_card_node.h"
|
||||
#include "tree/inner_deck_list_node.h"
|
||||
|
||||
#include <QSet>
|
||||
|
||||
class DecklistNodeTree
|
||||
{
|
||||
InnerDecklistNode *root; ///< Root of the deck tree (zones + cards).
|
||||
|
||||
public:
|
||||
/// @brief Constructs an empty DecklistNodeTree
|
||||
explicit DecklistNodeTree();
|
||||
/// @brief Copy constructor. Deep copies the tree
|
||||
explicit DecklistNodeTree(const DecklistNodeTree &other);
|
||||
/// @brief Copy-assignment operator. Deep copies the tree
|
||||
DecklistNodeTree &operator=(const DecklistNodeTree &other);
|
||||
|
||||
virtual ~DecklistNodeTree();
|
||||
|
||||
/**
|
||||
* @brief Gets a pointer to the underlying root node.
|
||||
* Note: DO NOT call this method unless the object needs to have access to the underlying model.
|
||||
* For now, only the DeckListModel should be calling this.
|
||||
*/
|
||||
InnerDecklistNode *getRoot() const
|
||||
{
|
||||
return root;
|
||||
}
|
||||
|
||||
bool isEmpty() const;
|
||||
|
||||
/**
|
||||
* @brief Deletes all nodes except the root.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* Gets all card nodes in the tree
|
||||
* @param restrictToZones Only get the nodes in these zones
|
||||
* @return A QList containing all the card nodes in the zone.
|
||||
*/
|
||||
QList<const DecklistCardNode *> getCardNodes(const QSet<QString> &restrictToZones = {}) const;
|
||||
|
||||
QList<const InnerDecklistNode *> getZoneNodes() const;
|
||||
|
||||
/**
|
||||
* @brief Computes the deck hash
|
||||
*/
|
||||
QString computeDeckHash() const;
|
||||
|
||||
/**
|
||||
*@brief Writes the contents of the deck to xml
|
||||
*/
|
||||
void write(QXmlStreamWriter *xml) const;
|
||||
|
||||
/**
|
||||
* @brief Reads a "zone" section of the xml to this tree
|
||||
*/
|
||||
void readZoneElement(QXmlStreamReader *xml);
|
||||
|
||||
DecklistCardNode *addCard(const QString &cardName,
|
||||
int amount,
|
||||
const QString &zoneName,
|
||||
int position,
|
||||
const QString &cardSetName = QString(),
|
||||
const QString &cardSetCollectorNumber = QString(),
|
||||
const QString &cardProviderId = QString(),
|
||||
const bool formatLegal = true);
|
||||
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Apply a function to every card in the deck tree. This can modify the cards.
|
||||
*
|
||||
* @param func Function taking (zone node, card node).
|
||||
*/
|
||||
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func) const;
|
||||
|
||||
private:
|
||||
// Helpers for traversing the tree
|
||||
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DECKLIST_NODE_TREE_H
|
||||
@@ -88,6 +88,12 @@ public:
|
||||
/// @param _cardSetNumber Set the collector number.
|
||||
virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0;
|
||||
|
||||
/// @return The format legality of the card
|
||||
virtual bool getFormatLegality() const = 0;
|
||||
|
||||
/// @param _formatLegal If the card is considered legal
|
||||
virtual void setFormatLegality(const bool _formatLegal) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the height of this node in the tree.
|
||||
*
|
||||
|
||||
@@ -51,6 +51,7 @@ class DecklistCardNode : public AbstractDecklistCardNode
|
||||
QString cardSetShortName; ///< Short set code (e.g., "NEO").
|
||||
QString cardSetNumber; ///< Collector number within the set.
|
||||
QString cardProviderId; ///< External provider identifier (e.g., UUID).
|
||||
bool formatLegal; ///< Format legality
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -63,6 +64,7 @@ public:
|
||||
* @param _cardSetShortName Short set code (e.g., "NEO").
|
||||
* @param _cardSetNumber Collector number within the set.
|
||||
* @param _cardProviderId External provider ID (e.g., UUID).
|
||||
* @param _formatLegality If the card is legal in the format
|
||||
*
|
||||
* On construction, if a parent is provided, this node is inserted into
|
||||
* the parent’s children list automatically.
|
||||
@@ -73,10 +75,11 @@ public:
|
||||
int position = -1,
|
||||
QString _cardSetShortName = QString(),
|
||||
QString _cardSetNumber = QString(),
|
||||
QString _cardProviderId = QString())
|
||||
QString _cardProviderId = QString(),
|
||||
bool _formatLegality = true)
|
||||
: AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number),
|
||||
cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)),
|
||||
cardProviderId(std::move(_cardProviderId))
|
||||
cardProviderId(std::move(_cardProviderId)), formatLegal(_formatLegality)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -150,6 +153,18 @@ public:
|
||||
cardSetNumber = _cardSetNumber;
|
||||
}
|
||||
|
||||
/// @return The format legality of the card
|
||||
[[nodiscard]] bool getFormatLegality() const override
|
||||
{
|
||||
return formatLegal;
|
||||
}
|
||||
|
||||
/// @param _formatLegal If the card is considered legal
|
||||
void setFormatLegality(const bool _formatLegal) override
|
||||
{
|
||||
formatLegal = _formatLegal;
|
||||
}
|
||||
|
||||
/// @return Always false; card nodes are not deck headers.
|
||||
[[nodiscard]] bool isDeckHeader() const override
|
||||
{
|
||||
|
||||
@@ -60,6 +60,8 @@ const QString CardFilter::attrName(Attr a)
|
||||
switch (a) {
|
||||
case AttrName:
|
||||
return tr("Name");
|
||||
case AttrNameExact:
|
||||
return tr("Name (Exact)");
|
||||
case AttrType:
|
||||
return tr("Type");
|
||||
case AttrColor:
|
||||
|
||||
@@ -33,6 +33,7 @@ public:
|
||||
AttrLoyalty,
|
||||
AttrManaCost,
|
||||
AttrName,
|
||||
AttrNameExact,
|
||||
AttrPow,
|
||||
AttrRarity,
|
||||
AttrSet,
|
||||
|
||||
@@ -153,6 +153,11 @@ bool FilterItem::acceptName(const CardInfoPtr info) const
|
||||
return info->getName().contains(term, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool FilterItem::acceptNameExact(const CardInfoPtr info) const
|
||||
{
|
||||
return info->getName() == term;
|
||||
}
|
||||
|
||||
bool FilterItem::acceptType(const CardInfoPtr info) const
|
||||
{
|
||||
return info->getCardType().contains(term, Qt::CaseInsensitive);
|
||||
@@ -401,6 +406,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) c
|
||||
switch (attr) {
|
||||
case CardFilter::AttrName:
|
||||
return acceptName(info);
|
||||
case CardFilter::AttrNameExact:
|
||||
return acceptNameExact(info);
|
||||
case CardFilter::AttrType:
|
||||
return acceptType(info);
|
||||
case CardFilter::AttrColor:
|
||||
|
||||
@@ -203,6 +203,7 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] bool acceptName(CardInfoPtr info) const;
|
||||
[[nodiscard]] bool acceptNameExact(CardInfoPtr info) const;
|
||||
[[nodiscard]] bool acceptType(CardInfoPtr info) const;
|
||||
[[nodiscard]] bool acceptMainType(CardInfoPtr info) const;
|
||||
[[nodiscard]] bool acceptSubType(CardInfoPtr info) const;
|
||||
|
||||
@@ -41,7 +41,7 @@ void DeckListModel::rebuildTree()
|
||||
beginResetModel();
|
||||
root->clearTree();
|
||||
|
||||
InnerDecklistNode *listRoot = deckList->getRoot();
|
||||
InnerDecklistNode *listRoot = deckList->getTree()->getRoot();
|
||||
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
@@ -168,6 +168,10 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
|
||||
return card->depth();
|
||||
}
|
||||
|
||||
case DeckRoles::IsLegalRole: {
|
||||
return card->getFormatLegality();
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
@@ -268,6 +272,7 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
|
||||
switch (index.column()) {
|
||||
case DeckListModelColumns::CARD_AMOUNT:
|
||||
node->setNumber(value.toInt());
|
||||
refreshCardFormatLegalities();
|
||||
break;
|
||||
case DeckListModelColumns::CARD_NAME:
|
||||
node->setName(value.toString());
|
||||
@@ -308,7 +313,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||
for (int i = 0; i < count; i++) {
|
||||
AbstractDecklistNode *toDelete = node->takeAt(row);
|
||||
if (auto *temp = dynamic_cast<DecklistModelCardNode *>(toDelete)) {
|
||||
deckList->deleteNode(temp->getDataNode());
|
||||
deckList->getTree()->deleteNode(temp->getDataNode());
|
||||
}
|
||||
delete toDelete;
|
||||
}
|
||||
@@ -414,8 +419,9 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam
|
||||
// Determine the correct index
|
||||
int insertRow = findSortedInsertRow(groupNode, cardInfo);
|
||||
|
||||
auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName,
|
||||
printingInfo.getProperty("num"), printingInfo.getProperty("uuid"));
|
||||
auto *decklistCard =
|
||||
deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"),
|
||||
printingInfo.getProperty("uuid"), isCardLegalForCurrentFormat(cardInfo));
|
||||
|
||||
beginInsertRows(parentIndex, insertRow, insertRow);
|
||||
cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow);
|
||||
@@ -532,6 +538,13 @@ void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newC
|
||||
rebuildTree();
|
||||
}
|
||||
|
||||
void DeckListModel::setActiveFormat(const QString &_format)
|
||||
{
|
||||
deckList->setGameFormat(_format);
|
||||
refreshCardFormatLegalities();
|
||||
emitBackgroundUpdates(QModelIndex()); // start from root
|
||||
}
|
||||
|
||||
void DeckListModel::cleanList()
|
||||
{
|
||||
setDeckList(new DeckList);
|
||||
@@ -550,86 +563,135 @@ void DeckListModel::setDeckList(DeckList *_deck)
|
||||
|
||||
QList<ExactCard> DeckListModel::getCards() const
|
||||
{
|
||||
QList<ExactCard> cards;
|
||||
DeckList *decklist = getDeckList();
|
||||
if (!decklist) {
|
||||
return cards;
|
||||
}
|
||||
InnerDecklistNode *listRoot = decklist->getRoot();
|
||||
if (!listRoot)
|
||||
return cards;
|
||||
auto nodes = deckList->getCardNodes();
|
||||
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
if (!currentZone)
|
||||
continue;
|
||||
for (int j = 0; j < currentZone->size(); j++) {
|
||||
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||
if (!currentCard)
|
||||
continue;
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
ExactCard card = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
|
||||
if (card) {
|
||||
cards.append(card);
|
||||
} else {
|
||||
qDebug() << "Card not found in database!";
|
||||
}
|
||||
QList<ExactCard> cards;
|
||||
for (auto node : nodes) {
|
||||
ExactCard card = CardDatabaseManager::query()->getCard(node->toCardRef());
|
||||
if (card) {
|
||||
for (int k = 0; k < node->getNumber(); ++k) {
|
||||
cards.append(card);
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Card not found in database!";
|
||||
}
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
QList<ExactCard> DeckListModel::getCardsForZone(const QString &zoneName) const
|
||||
{
|
||||
QList<ExactCard> cards;
|
||||
DeckList *decklist = getDeckList();
|
||||
if (!decklist) {
|
||||
return cards;
|
||||
}
|
||||
InnerDecklistNode *listRoot = decklist->getRoot();
|
||||
if (!listRoot)
|
||||
return cards;
|
||||
auto nodes = deckList->getCardNodes({zoneName});
|
||||
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
if (!currentZone)
|
||||
continue;
|
||||
if (currentZone->getName() == zoneName) {
|
||||
for (int j = 0; j < currentZone->size(); j++) {
|
||||
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||
if (!currentCard)
|
||||
continue;
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
ExactCard card = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
|
||||
if (card) {
|
||||
cards.append(card);
|
||||
} else {
|
||||
qDebug() << "Card not found in database!";
|
||||
}
|
||||
}
|
||||
QList<ExactCard> cards;
|
||||
for (auto node : nodes) {
|
||||
ExactCard card = CardDatabaseManager::query()->getCard(node->toCardRef());
|
||||
if (card) {
|
||||
for (int k = 0; k < node->getNumber(); ++k) {
|
||||
cards.append(card);
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Card not found in database!";
|
||||
}
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
QList<QString> *DeckListModel::getZones() const
|
||||
QList<QString> DeckListModel::getZones() const
|
||||
{
|
||||
QList<QString> *zones = new QList<QString>();
|
||||
DeckList *decklist = getDeckList();
|
||||
if (!decklist) {
|
||||
return zones;
|
||||
auto zoneNodes = deckList->getZoneNodes();
|
||||
|
||||
QList<QString> zones;
|
||||
std::transform(zoneNodes.cbegin(), zoneNodes.cend(), std::back_inserter(zones),
|
||||
[](auto zoneNode) { return zoneNode->getName(); });
|
||||
|
||||
return zones;
|
||||
}
|
||||
|
||||
bool DeckListModel::isCardLegalForCurrentFormat(const CardInfoPtr cardInfo)
|
||||
{
|
||||
if (!deckList->getGameFormat().isEmpty()) {
|
||||
if (cardInfo->getProperties().contains("format-" + deckList->getGameFormat())) {
|
||||
QString formatLegality = cardInfo->getProperty("format-" + deckList->getGameFormat());
|
||||
return formatLegality == "legal" || formatLegality == "restricted";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
InnerDecklistNode *listRoot = decklist->getRoot();
|
||||
if (!listRoot)
|
||||
return zones;
|
||||
return true;
|
||||
}
|
||||
|
||||
int maxAllowedForLegality(const FormatRules &format, const QString &legality)
|
||||
{
|
||||
for (const AllowedCount &c : format.allowedCounts) {
|
||||
if (c.label == legality) {
|
||||
return c.max;
|
||||
}
|
||||
}
|
||||
return -1; // unknown legality → treat as illegal
|
||||
}
|
||||
|
||||
|
||||
bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity)
|
||||
{
|
||||
auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat());
|
||||
|
||||
if (!formatRules) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exceptions always win
|
||||
if (cardHasAnyException(*cardInfo, *formatRules)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString legalityProp = "format-" + deckList->getGameFormat();
|
||||
if (!cardInfo->getProperties().contains(legalityProp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString legality = cardInfo->getProperty(legalityProp);
|
||||
|
||||
int maxAllowed = maxAllowedForLegality(*formatRules, legality);
|
||||
|
||||
if (maxAllowed == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxAllowed < 0) { // unlimited
|
||||
return true;
|
||||
}
|
||||
|
||||
return quantity <= maxAllowed;
|
||||
}
|
||||
|
||||
void DeckListModel::refreshCardFormatLegalities()
|
||||
{
|
||||
InnerDecklistNode *listRoot = deckList->getTree()->getRoot();
|
||||
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
if (!currentZone)
|
||||
continue;
|
||||
zones->append(currentZone->getName());
|
||||
auto *currentZone = static_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
for (int j = 0; j < currentZone->size(); j++) {
|
||||
auto *currentCard = static_cast<DecklistCardNode *>(currentZone->at(j));
|
||||
|
||||
// TODO: better sanity checking
|
||||
if (currentCard == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ExactCard exactCard = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
|
||||
if (!exactCard) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool legal = isCardLegalForCurrentFormat(exactCard.getCardPtr());
|
||||
|
||||
if (legal) {
|
||||
legal = isCardQuantityLegalForCurrentFormat(exactCard.getCardPtr(), currentCard->getNumber());
|
||||
}
|
||||
|
||||
currentCard->setFormatLegality(legal);
|
||||
}
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,14 @@ public:
|
||||
{
|
||||
dataNode->setCardCollectorNumber(_cardSetNumber);
|
||||
}
|
||||
bool getFormatLegality() const override
|
||||
{
|
||||
return dataNode->getFormatLegality();
|
||||
}
|
||||
void setFormatLegality(const bool _formatLegal) override
|
||||
{
|
||||
dataNode->setFormatLegality(_formatLegal);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the underlying data node.
|
||||
@@ -194,7 +202,7 @@ public:
|
||||
* affects its hash.
|
||||
*
|
||||
* Slots:
|
||||
* - rebuildTree(): rebuilds the model structure from the underlying DeckLoader.
|
||||
* - rebuildTree(): rebuilds the model structure from the underlying node tree.
|
||||
*/
|
||||
class DeckListModel : public QAbstractItemModel
|
||||
{
|
||||
@@ -202,13 +210,16 @@ class DeckListModel : public QAbstractItemModel
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Rebuilds the model tree from the underlying DeckLoader.
|
||||
* @brief Rebuilds the model tree from the underlying node tree.
|
||||
*
|
||||
* This updates all indices and ensures the model reflects the current
|
||||
* state of the deck.
|
||||
*/
|
||||
void rebuildTree();
|
||||
|
||||
public slots:
|
||||
void setActiveFormat(const QString &_format);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted whenever the deck hash changes due to modifications in the model.
|
||||
@@ -300,7 +311,10 @@ public:
|
||||
|
||||
[[nodiscard]] QList<ExactCard> getCards() const;
|
||||
[[nodiscard]] QList<ExactCard> getCardsForZone(const QString &zoneName) const;
|
||||
[[nodiscard]] QList<QString> *getZones() const;
|
||||
[[nodiscard]] QList<QString> getZones() const;
|
||||
bool isCardLegalForCurrentFormat(CardInfoPtr cardInfo);
|
||||
bool isCardQuantityLegalForCurrentFormat(CardInfoPtr cardInfo, int quantity);
|
||||
void refreshCardFormatLegalities();
|
||||
|
||||
/**
|
||||
* @brief Sets the criteria used to group cards in the model.
|
||||
|
||||
@@ -501,7 +501,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||
allPlayersEver.insert(playerName);
|
||||
|
||||
// if the original creator of the game joins, give them host status back
|
||||
// FIXME: transferring host to spectators has side effects
|
||||
//! \todo transferring host to spectators has side effects
|
||||
if (newParticipant->getUserInfo()->name() == creatorInfo->name()) {
|
||||
hostId = newParticipant->getPlayerId();
|
||||
sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId));
|
||||
|
||||
@@ -102,28 +102,16 @@ void Server_Player::setupZones()
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// Assign card ids and create deck from deck list
|
||||
InnerDecklistNode *listRoot = deck->getRoot();
|
||||
for (int i = 0; i < listRoot->size(); ++i) {
|
||||
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
Server_CardZone *z;
|
||||
if (currentZone->getName() == DECK_ZONE_MAIN) {
|
||||
z = deckZone;
|
||||
} else if (currentZone->getName() == DECK_ZONE_SIDE) {
|
||||
z = sbZone;
|
||||
} else {
|
||||
continue;
|
||||
auto insertCardsIntoZone = [this](auto cards, auto *zone) {
|
||||
for (auto card : cards) {
|
||||
for (int k = 0; k < card->getNumber(); ++k) {
|
||||
zone->insertCard(new Server_Card(card->toCardRef(), nextCardId++, 0, 0, zone), -1, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (int j = 0; j < currentZone->size(); ++j) {
|
||||
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||
if (!currentCard) {
|
||||
continue;
|
||||
}
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
z->insertCard(new Server_Card(currentCard->toCardRef(), nextCardId++, 0, 0, z), -1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_MAIN}), deckZone);
|
||||
insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_SIDE}), sbZone);
|
||||
|
||||
const QList<MoveCard_ToZone> &sideboardPlan = deck->getCurrentSideboardPlan();
|
||||
for (const auto &m : sideboardPlan) {
|
||||
|
||||
@@ -6,17 +6,12 @@ set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
|
||||
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/system_memory_querier.cpp
|
||||
libcockatrice/utility/passwordhasher.cpp
|
||||
)
|
||||
|
||||
set(UTILITY_HEADERS
|
||||
libcockatrice/utility/color.h
|
||||
libcockatrice/utility/expression.h
|
||||
libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/macros.h
|
||||
libcockatrice/utility/passwordhasher.h
|
||||
libcockatrice/utility/system_memory_querier.h
|
||||
libcockatrice/utility/trice_limits.h
|
||||
libcockatrice/utility/color.h libcockatrice/utility/expression.h libcockatrice/utility/levenshtein.h
|
||||
libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h libcockatrice/utility/trice_limits.h
|
||||
)
|
||||
|
||||
add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user